From 319ccc32ee90a43d54a0e71f3605d2d4e0f02e29 Mon Sep 17 00:00:00 2001 From: claude Date: Mon, 15 Jun 2026 12:22:55 +0000 Subject: [PATCH] feat: add language picker to logged-in footer and editor settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../app/src/Features/User/UserController.mjs | 6 ++- services/web/app/views/layout-react.pug | 1 + .../interface-language-setting.tsx | 52 +++++++++++++++++++ .../context/settings-modal-context.tsx | 5 ++ .../shared/components/footer/thin-footer.tsx | 10 ++-- .../js/shared/components/language-picker.tsx | 28 +++++----- .../components/types/footer-metadata.ts | 1 + services/web/locales/de.json | 1 + services/web/locales/en.json | 1 + services/web/locales/es.json | 1 + services/web/locales/fr.json | 1 + services/web/locales/it.json | 1 + 12 files changed, 87 insertions(+), 21 deletions(-) create mode 100644 services/web/frontend/js/features/settings/components/editor-settings/interface-language-setting.tsx diff --git a/services/web/app/src/Features/User/UserController.mjs b/services/web/app/src/Features/User/UserController.mjs index 6f7437899e..bc82875183 100644 --- a/services/web/app/src/Features/User/UserController.mjs +++ b/services/web/app/src/Features/User/UserController.mjs @@ -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) } diff --git a/services/web/app/views/layout-react.pug b/services/web/app/views/layout-react.pug index 6054e6cb39..810a29decf 100644 --- a/services/web/app/views/layout-react.pug +++ b/services/web/app/views/layout-react.pug @@ -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, } diff --git a/services/web/frontend/js/features/settings/components/editor-settings/interface-language-setting.tsx b/services/web/frontend/js/features/settings/components/editor-settings/interface-language-setting.tsx new file mode 100644 index 0000000000..7ffb7e3119 --- /dev/null +++ b/services/web/frontend/js/features/settings/components/editor-settings/interface-language-setting.tsx @@ -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 = useMemo( + () => footerMeta?.translatedLanguages ?? {}, + [footerMeta] + ) + + const handleChange = useCallback( + (event: React.ChangeEvent) => { + 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 ( + + + {availableLanguages + .filter(lng => translatedLanguages[lng]) + .map(lng => ( + + ))} + + + ) +} diff --git a/services/web/frontend/js/features/settings/context/settings-modal-context.tsx b/services/web/frontend/js/features/settings/context/settings-modal-context.tsx index bf3da53043..06a5fa49c8 100644 --- a/services/web/frontend/js/features/settings/context/settings-modal-context.tsx +++ b/services/web/frontend/js/features/settings/context/settings-modal-context.tsx @@ -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 = ({ key: 'dictionary-settings', component: , }, + { + key: 'interfaceLanguage', + component: , + }, ], }, ...spellcheckExtraSections, diff --git a/services/web/frontend/js/shared/components/footer/thin-footer.tsx b/services/web/frontend/js/shared/components/footer/thin-footer.tsx index 414c9d3ad1..2ca8378240 100644 --- a/services/web/frontend/js/shared/components/footer/thin-footer.tsx +++ b/services/web/frontend/js/shared/components/footer/thin-footer.tsx @@ -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 (