Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 16 additions & 7 deletions src/i18n/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,23 @@ import es from './locales/es/translations.json'
import zh from './locales/zh/translations.json'
import ptBR from './locales/pt-BR/translations.json'

import { getStoredLanguage, getBrowserLanguage, resolveLanguage, DEFAULT_LANGUAGE } from './language'

const LANGUAGE_STORAGE_KEY = 'ocl.language'
const SUPPORTED_LANGUAGES = ['en', 'es', 'zh', 'pt-BR']

// Priority: manual choice (localStorage) > browser preference > default.
const getInitialLanguage = () => {
if(typeof window === 'undefined')
return 'en'
return DEFAULT_LANGUAGE

try {
const storedLanguage = window.localStorage.getItem(LANGUAGE_STORAGE_KEY)
return SUPPORTED_LANGUAGES.includes(storedLanguage) ? storedLanguage : 'en'
} catch(_) {
return 'en'
}
return resolveLanguage([
() => getStoredLanguage(window.localStorage, LANGUAGE_STORAGE_KEY, SUPPORTED_LANGUAGES),
() => getBrowserLanguage(window.navigator, SUPPORTED_LANGUAGES)
])
}

// Registered after init, so only user-triggered changes are persisted, not the detected initial language.
const persistLanguage = language => {
try {
window.localStorage.setItem(LANGUAGE_STORAGE_KEY, language)
Expand All @@ -28,6 +30,11 @@ const persistLanguage = language => {
}
}

const applyDocumentLanguage = language => {
if(typeof document !== 'undefined')
document.documentElement.lang = language
}

i18n.use(initReactI18next).init({
fallbackLng: 'en',
lng: getInitialLanguage(),
Expand All @@ -50,6 +57,8 @@ i18n.use(initReactI18next).init({
});

i18n.languages = SUPPORTED_LANGUAGES;
applyDocumentLanguage(i18n.language)
i18n.on('languageChanged', persistLanguage)
i18n.on('languageChanged', applyDocumentLanguage)

export default i18n;
51 changes: 51 additions & 0 deletions src/i18n/language.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
export const DEFAULT_LANGUAGE = 'en'

const baseOf = tag => tag.split('-')[0]

/*
* Maps a BCP 47 tag (e.g. "pt-PT", "es_MX", "en-US") to one of the supported locales.
* An exact match wins; otherwise the primary language code is matched, so any regional
* variant of a supported language (pt-PT, es-AR, zh-TW, ...) resolves to it.
*/
export const matchLanguage = (tag, supported) => {
if(typeof tag !== 'string' || !tag)
return undefined

const normalized = tag.trim().replace('_', '-').toLowerCase()
return supported.find(locale => locale.toLowerCase() === normalized) ||
supported.find(locale => baseOf(locale.toLowerCase()) === baseOf(normalized))
}

export const getStoredLanguage = (storage, key, supported) => {
try {
const stored = storage.getItem(key)
return supported.includes(stored) ? stored : undefined
} catch(_) {
return undefined // storage unavailable (e.g. blocked or private mode)
}
}

// navigator.languages is ordered by preference; the first tag that maps to a supported locale wins.
export const getBrowserLanguage = (nav, supported) => {
const tags = nav?.languages?.length ? nav.languages : [nav?.language]
for(const tag of tags) {
const match = matchLanguage(tag, supported)
if(match)
return match
}
return undefined
}

/*
* Runs the sources in priority order and returns the first locale found.
* Each source is a function returning a supported locale or undefined, so a new
* signal is added by appending one entry to the list.
*/
export const resolveLanguage = (sources, fallback = DEFAULT_LANGUAGE) => {
for(const source of sources) {
const language = source()
if(language)
return language
}
return fallback
}
Loading