feat: add French and Spanish locales and update language selector
This commit is contained in:
@@ -63,6 +63,21 @@ function FlagIcon({ lang, className, style }: { lang: string; className?: string
|
||||
<path d="M7,0 h2 v16 h-2 z M0,7 h22 v2 h-22 z" fill="#00205B"/>
|
||||
</svg>
|
||||
)
|
||||
case 'fr':
|
||||
return (
|
||||
<svg viewBox="0 0 3 2" className={className} style={baseStyle}>
|
||||
<rect width="3" height="2" fill="#FFFFFF"/>
|
||||
<rect width="1" height="2" fill="#002395"/>
|
||||
<rect x="2" width="1" height="2" fill="#ED2939"/>
|
||||
</svg>
|
||||
)
|
||||
case 'es':
|
||||
return (
|
||||
<svg viewBox="0 0 3 2" className={className} style={baseStyle}>
|
||||
<rect width="3" height="2" fill="#C1272D"/>
|
||||
<rect y="0.5" width="3" height="1" fill="#FEE100"/>
|
||||
</svg>
|
||||
)
|
||||
default:
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import deJson from './locales/de.json'
|
||||
import daJson from './locales/da.json'
|
||||
import svJson from './locales/sv.json'
|
||||
import nbJson from './locales/nb.json'
|
||||
import frJson from './locales/fr.json'
|
||||
import esJson from './locales/es.json'
|
||||
import { initSeo } from '../utils/seo.js'
|
||||
import { SUPPORTED_LANGUAGES } from '../utils/i18nLanguages.js'
|
||||
|
||||
@@ -15,7 +17,9 @@ const resources = {
|
||||
de: { translation: deJson.translation },
|
||||
da: { translation: daJson.translation },
|
||||
sv: { translation: svJson.translation },
|
||||
nb: { translation: nbJson.translation }
|
||||
nb: { translation: nbJson.translation },
|
||||
fr: { translation: frJson.translation },
|
||||
es: { translation: esJson.translation }
|
||||
}
|
||||
|
||||
i18n
|
||||
|
||||
@@ -4,6 +4,8 @@ import enJson from '../i18n/locales/en.json'
|
||||
import daJson from '../i18n/locales/da.json'
|
||||
import svJson from '../i18n/locales/sv.json'
|
||||
import nbJson from '../i18n/locales/nb.json'
|
||||
import frJson from '../i18n/locales/fr.json'
|
||||
import esJson from '../i18n/locales/es.json'
|
||||
|
||||
function collectKeys(obj: Record<string, unknown>, prefix = ''): string[] {
|
||||
const keys: string[] = []
|
||||
@@ -23,7 +25,9 @@ const bundles = {
|
||||
en: enJson.translation,
|
||||
da: daJson.translation,
|
||||
sv: svJson.translation,
|
||||
nb: nbJson.translation
|
||||
nb: nbJson.translation,
|
||||
fr: frJson.translation,
|
||||
es: esJson.translation
|
||||
} as const
|
||||
|
||||
describe('i18n locale key parity', () => {
|
||||
|
||||
+639
-637
File diff suppressed because it is too large
Load Diff
@@ -15,7 +15,9 @@
|
||||
"en": "English",
|
||||
"da": "Dansk",
|
||||
"sv": "Svenska",
|
||||
"nb": "Norsk"
|
||||
"nb": "Norsk",
|
||||
"fr": "Français",
|
||||
"es": "Español"
|
||||
},
|
||||
"dialog": {
|
||||
"ok": "OK",
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
"en": "English",
|
||||
"da": "Dansk",
|
||||
"sv": "Svenska",
|
||||
"nb": "Norsk"
|
||||
"nb": "Norsk",
|
||||
"fr": "French",
|
||||
"es": "Spanish"
|
||||
},
|
||||
"dialog": {
|
||||
"ok": "OK",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+595
-593
File diff suppressed because it is too large
Load Diff
+647
-645
File diff suppressed because it is too large
Load Diff
@@ -20,14 +20,13 @@ vi.mock('../services/analytics.js', async (importOriginal) => {
|
||||
})
|
||||
|
||||
function createMockI18n(language: string): I18nInstance {
|
||||
let current = language
|
||||
return {
|
||||
language: current,
|
||||
const mock = {
|
||||
language,
|
||||
changeLanguage: vi.fn(async (lng: string) => {
|
||||
current = lng
|
||||
;(this as { language: string }).language = lng
|
||||
mock.language = lng
|
||||
})
|
||||
} as unknown as I18nInstance
|
||||
return mock
|
||||
}
|
||||
|
||||
describe('i18nLanguages', () => {
|
||||
@@ -72,11 +71,11 @@ describe('i18nLanguages', () => {
|
||||
})
|
||||
|
||||
it('cycleAppLanguage tracks the next language', () => {
|
||||
const i18n = createMockI18n('nb')
|
||||
const i18n = createMockI18n('es')
|
||||
cycleAppLanguage(i18n)
|
||||
|
||||
expect(trackPlausibleEvent).toHaveBeenCalledWith(PlausibleEvents.LANGUAGE_CHANGED, {
|
||||
from: 'nb',
|
||||
from: 'es',
|
||||
to: 'de'
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { i18n as I18nInstance } from 'i18next'
|
||||
import { PlausibleEvents, trackPlausibleEvent } from '../services/analytics.js'
|
||||
|
||||
/** Supported UI languages (ISO 639-1, language-only). */
|
||||
export const SUPPORTED_LANGUAGES = ['de', 'en', 'da', 'sv', 'nb'] as const
|
||||
export const SUPPORTED_LANGUAGES = ['de', 'en', 'da', 'sv', 'nb', 'fr', 'es'] as const
|
||||
|
||||
export type AppLanguage = (typeof SUPPORTED_LANGUAGES)[number]
|
||||
|
||||
@@ -11,7 +11,9 @@ export const LANGUAGE_FLAGS: Record<AppLanguage, string> = {
|
||||
en: '🇬🇧',
|
||||
da: '🇩🇰',
|
||||
sv: '🇸🇪',
|
||||
nb: '🇳🇴'
|
||||
nb: '🇳🇴',
|
||||
fr: '🇫🇷',
|
||||
es: '🇪🇸'
|
||||
}
|
||||
|
||||
export function normalizeAppLanguage(language?: string): AppLanguage {
|
||||
|
||||
@@ -10,7 +10,9 @@ const OG_LOCALES: Record<SeoLang, string> = {
|
||||
en: 'en_GB',
|
||||
da: 'da_DK',
|
||||
sv: 'sv_SE',
|
||||
nb: 'nb_NO'
|
||||
nb: 'nb_NO',
|
||||
fr: 'fr_FR',
|
||||
es: 'es_ES'
|
||||
}
|
||||
|
||||
let i18nRef: I18nInstance | null = null
|
||||
|
||||
@@ -23,7 +23,9 @@ const defaultSource = resolve(repoRoot, 'client/src/i18n/locales/de.json')
|
||||
const TARGETS = {
|
||||
da: 'DA',
|
||||
sv: 'SV',
|
||||
nb: 'NB'
|
||||
nb: 'NB',
|
||||
fr: 'FR',
|
||||
es: 'ES'
|
||||
}
|
||||
|
||||
/** Keys whose values stay identical to source (language names, brand). */
|
||||
|
||||
@@ -11,7 +11,7 @@ import { flattenTranslation } from './lib/deepl-translate.mjs'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const localesDir = resolve(__dirname, '../client/src/i18n/locales')
|
||||
const localeFiles = ['de.json', 'en.json', 'da.json', 'sv.json', 'nb.json']
|
||||
const localeFiles = ['de.json', 'en.json', 'da.json', 'sv.json', 'nb.json', 'fr.json', 'es.json']
|
||||
|
||||
async function loadKeys(filename) {
|
||||
const raw = await readFile(resolve(localesDir, filename), 'utf8')
|
||||
|
||||
Reference in New Issue
Block a user