feat: add language picker to logged-in footer and editor settings
Build and Deploy Verso / deploy (push) Successful in 18m46s

- Rewrites LanguagePicker to use availableLanguages from ol-footer meta
  instead of subdomainLang (which is always empty in single-domain setup)
- Passes availableLanguages through layout-react.pug → ol-footer meta so
  React footer picks it up
- Adds InterfaceLanguageSetting component to the editor settings modal
  ("Spelling and language" tab) for use when no footer is present
- Adds interface_language key to all five locale files (en/fr/de/es/it)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
claude
2026-06-15 12:22:55 +00:00
parent 1a0197812d
commit 319ccc32ee
12 changed files with 87 additions and 21 deletions
@@ -580,7 +580,11 @@ async function setLanguage(req, res) {
if (userId) {
await User.findByIdAndUpdate(userId, { languageCode: lngCode }).exec()
}
const redir = req.get('Referer') || '/project'
const returnTo = req.query.return_to
const redir =
typeof returnTo === 'string' && returnTo.startsWith('/')
? returnTo
: req.get('Referer') || '/project'
res.redirect(302, redir)
}
+1
View File
@@ -49,6 +49,7 @@ block append meta
showPoweredBy: !hasFeature('saas') && !settings.nav.hide_powered_by,
subdomainLang: settings.i18n.subdomainLang,
translatedLanguages: settings.translatedLanguages,
availableLanguages: availableLanguages,
leftItems: cloneAndTranslateText(settings.nav.left_footer),
rightItems: settings.nav.right_footer,
}
@@ -0,0 +1,52 @@
import { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import getMeta from '@/utils/meta'
import Setting from '../setting'
import OLFormSelect from '@/shared/components/ol/ol-form-select'
export default function InterfaceLanguageSetting() {
const { t } = useTranslation()
const currentLangCode = getMeta('ol-i18n').currentLangCode
const footerMeta = getMeta('ol-footer')
const availableLanguages: string[] = useMemo(
() => footerMeta?.availableLanguages ?? [],
[footerMeta]
)
const translatedLanguages: Record<string, string> = useMemo(
() => footerMeta?.translatedLanguages ?? {},
[footerMeta]
)
const handleChange = useCallback(
(event: React.ChangeEvent<HTMLSelectElement>) => {
const lng = event.target.value
const returnTo = encodeURIComponent(window.location.pathname)
window.location.href = `/set-language?lng=${encodeURIComponent(lng)}&return_to=${returnTo}`
},
[]
)
if (availableLanguages.length <= 1) return null
return (
<Setting controlId="interfaceLanguage" label={t('interface_language')}>
<OLFormSelect
id="interfaceLanguage"
className="ide-dropdown-setting ide-dropdown-setting-wide"
size="sm"
value={currentLangCode}
onChange={handleChange}
translate="no"
>
{availableLanguages
.filter(lng => translatedLanguages[lng])
.map(lng => (
<option key={lng} value={lng}>
{translatedLanguages[lng]}
</option>
))}
</OLFormSelect>
</Setting>
)
}
@@ -9,6 +9,7 @@ import PDFViewerSetting from '@/features/settings/components/editor-settings/pdf
import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
import SpellCheckSetting from '@/features/settings/components/editor-settings/spell-check-setting'
import DictionarySetting from '@/features/settings/components/editor-settings/dictionary-setting'
import InterfaceLanguageSetting from '@/features/settings/components/editor-settings/interface-language-setting'
import { useTranslation } from 'react-i18next'
import BreadcrumbsSetting from '@/features/settings/components/editor-settings/breadcrumbs-setting'
import NonBlinkingCursorSetting from '@/features/settings/components/editor-settings/non-blinking-cursor-setting'
@@ -165,6 +166,10 @@ export const SettingsModalProvider: FC<React.PropsWithChildren> = ({
key: 'dictionary-settings',
component: <DictionarySetting />,
},
{
key: 'interfaceLanguage',
component: <InterfaceLanguageSetting />,
},
],
},
...spellcheckExtraSections,
@@ -41,11 +41,13 @@ function Separator() {
)
}
function ThinFooter({ subdomainLang, leftItems, rightItems }: FooterMetadata) {
function ThinFooter({
availableLanguages,
leftItems,
rightItems,
}: FooterMetadata) {
const { t } = useTranslation()
const showLanguagePicker = Boolean(
subdomainLang && Object.keys(subdomainLang).length > 1
)
const showLanguagePicker = (availableLanguages?.length ?? 0) > 1
return (
<footer className="site-footer">
@@ -14,9 +14,10 @@ function LanguagePicker({ showHeader } = { showHeader: false }) {
const { t } = useTranslation()
const currentLangCode = getMeta('ol-i18n').currentLangCode
const translatedLanguages = getMeta('ol-footer').translatedLanguages
const subdomainLang = getMeta('ol-footer').subdomainLang
const currentUrlWithQueryParams = window.location.pathname
const footerMeta = getMeta('ol-footer')
const translatedLanguages = footerMeta?.translatedLanguages
const availableLanguages: string[] = footerMeta?.availableLanguages ?? []
const currentUrl = window.location.pathname
return (
<Dropdown drop="up">
@@ -30,7 +31,7 @@ function LanguagePicker({ showHeader } = { showHeader: false }) {
<MaterialIcon type="translate" />
&nbsp;
<span className="language-picker-text">
{translatedLanguages?.[currentLangCode]}
{translatedLanguages?.[currentLangCode] ?? currentLangCode}
</span>
</DropdownToggle>
@@ -39,24 +40,19 @@ function LanguagePicker({ showHeader } = { showHeader: false }) {
aria-labelledby="language-picker-toggle"
>
{showHeader ? <DropdownHeader>{t('language')}</DropdownHeader> : null}
{subdomainLang &&
Object.entries(subdomainLang).map(([subdomain, subdomainDetails]) => {
if (
!subdomainDetails ||
!subdomainDetails.lngCode ||
subdomainDetails.hide
)
return null
const isActive = subdomainDetails.lngCode === currentLangCode
{availableLanguages
.filter(lng => translatedLanguages?.[lng])
.map(lng => {
const isActive = lng === currentLangCode
return (
<li role="none" key={subdomain} translate="no">
<li role="none" key={lng} translate="no">
<DropdownItem
href={`${subdomainDetails.url}${currentUrlWithQueryParams}`}
href={`/set-language?lng=${encodeURIComponent(lng)}&return_to=${encodeURIComponent(currentUrl)}`}
active={isActive}
aria-current={isActive ? 'true' : false}
trailingIcon={isActive ? 'check' : null}
>
{translatedLanguages?.[subdomainDetails.lngCode]}
{translatedLanguages![lng]}
</DropdownItem>
</li>
)
@@ -13,6 +13,7 @@ export type FooterMetadata = {
translatedLanguages: { [key: string]: string }
showPoweredBy?: boolean
subdomainLang?: SubdomainLang
availableLanguages?: string[]
leftItems?: FooterItem[]
rightItems?: FooterItem[]
}
+1
View File
@@ -2502,6 +2502,7 @@
"speak": "Sprechen",
"speech_input_not_available": "Die Spracheingabe ist in diesem Browser noch nicht verfügbar",
"spellcheck": "Rechtschreibprüfung",
"interface_language": "Oberflächensprache",
"spellcheck_language": "Sprache der Rechtschreibprüfung",
"spelling_and_language": "Rechtschreibung und Sprache",
"split_view": "Geteilte Ansicht",
+1
View File
@@ -2481,6 +2481,7 @@
"speak": "Speak",
"speech_input_not_available": "Speech input is not yet available in this browser",
"spellcheck": "Spellcheck",
"interface_language": "Interface language",
"spellcheck_language": "Spellcheck language",
"spelling_and_language": "Spelling and language",
"split_view": "Split view",
+1
View File
@@ -2482,6 +2482,7 @@
"speak": "Hablar",
"speech_input_not_available": "La entrada de voz aún no está disponible en este navegador",
"spellcheck": "corrector ortográfico",
"interface_language": "Idioma de la interfaz",
"spellcheck_language": "Idioma del corrector ortográfico",
"spelling_and_language": "Ortografía y lenguaje",
"split_view": "Vista dividida",
+1
View File
@@ -2486,6 +2486,7 @@
"speak": "Parler",
"speech_input_not_available": "La saisie vocale n'est pas encore disponible dans ce navigateur",
"spellcheck": "Vérification orthographique",
"interface_language": "Langue de l'interface",
"spellcheck_language": "Langue de vérification orthographique",
"spelling_and_language": "Orthographe et langue",
"split_view": "Vue fractionnée",
+1
View File
@@ -2481,6 +2481,7 @@
"speak": "Parla",
"speech_input_not_available": "L'input vocale non è ancora disponibile in questo browser",
"spellcheck": "Controllo ortografico",
"interface_language": "Lingua dell'interfaccia",
"spellcheck_language": "Lingua del controllo ortografico",
"spelling_and_language": "Ortografia e lingua",
"split_view": "Visualizzazione divisa",