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