feat: add language picker to logged-in footer and editor settings
Build and Deploy Verso / deploy (push) Successful in 18m46s
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:
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
+52
@@ -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" />
|
||||
|
||||
<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[]
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user