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
2 changes: 2 additions & 0 deletions app/(tabs)/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,13 +144,15 @@ export default function SettingsScreen() {
system: t("settings.language.system"),
en: t("settings.language.en"),
"zh-Hans": t("settings.language.zhHans"),
it: t("settings.language.it"),
}

const handleLanguagePress = useCallback(() => {
Alert.alert(t("settings.language.title"), undefined, [
{ text: localeLabels.system, onPress: () => setLocale("system") },
{ text: localeLabels.en, onPress: () => setLocale("en") },
{ text: localeLabels["zh-Hans"], onPress: () => setLocale("zh-Hans") },
{ text: localeLabels.it, onPress: () => setLocale("it") },
{ text: t("common.cancel"), style: "cancel" },
])
}, [t, setLocale, localeLabels])
Expand Down
38 changes: 25 additions & 13 deletions src/lib/i18n/catalog-parity.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
// Guards against locale drift: en.json and zh-Hans.json must expose exactly
// the same set of translation keys, or i18next silently falls back to the
// key path (en) / nothing sensible (missing zh copy) at runtime. Run with
// plain `node --test` — no i18next/expo-localization imports needed, same
// as locale-resolve.test.ts.
// Guards against locale drift: every catalog must expose exactly the same
// set of translation keys, or i18next silently falls back to the key path
// (en) / nothing sensible (missing catalog copy) at runtime. Run with plain
// `node --test` — no i18next/expo-localization imports needed, same as
// locale-resolve.test.ts.
import { test } from "node:test"
import assert from "node:assert/strict"
import en from "./en.json" with { type: "json" }
import zhHans from "./zh-Hans.json" with { type: "json" }
import it from "./it.json" with { type: "json" }

const CATALOGS = [
["en", en],
["zh-Hans", zhHans],
["it", it],
] as const

// Flattens a nested translation object into dotted leaf-key paths, e.g.
// { settings: { language: { label: "..." } } } -> ["settings.language.label"]
Expand All @@ -24,19 +31,24 @@ function flattenKeys(obj: unknown, prefix = ""): string[] {
return keys
}

test("en.json and zh-Hans.json expose identical translation keys", () => {
const enKeys = new Set(flattenKeys(en))
const zhKeys = new Set(flattenKeys(zhHans))
test("all catalogs expose identical translation keys", () => {
for (const [nameA, catalogA] of CATALOGS) {
for (const [nameB, catalogB] of CATALOGS) {
if (nameA >= nameB) continue
const keysA = new Set(flattenKeys(catalogA))
const keysB = new Set(flattenKeys(catalogB))

const missingFromZh = [...enKeys].filter((k) => !zhKeys.has(k)).sort()
const missingFromEn = [...zhKeys].filter((k) => !enKeys.has(k)).sort()
const missingFromB = [...keysA].filter((k) => !keysB.has(k)).sort()
const missingFromA = [...keysB].filter((k) => !keysA.has(k)).sort()

assert.deepEqual(missingFromZh, [], `keys present in en.json but missing from zh-Hans.json: ${missingFromZh.join(", ")}`)
assert.deepEqual(missingFromEn, [], `keys present in zh-Hans.json but missing from en.json: ${missingFromEn.join(", ")}`)
assert.deepEqual(missingFromB, [], `keys present in ${nameA}.json but missing from ${nameB}.json: ${missingFromB.join(", ")}`)
assert.deepEqual(missingFromA, [], `keys present in ${nameB}.json but missing from ${nameA}.json: ${missingFromA.join(", ")}`)
}
}
})

test("no translation value is an empty string", () => {
for (const [name, catalog] of [["en", en], ["zh-Hans", zhHans]] as const) {
for (const [name, catalog] of CATALOGS) {
const keys = flattenKeys(catalog)
for (const key of keys) {
const value = key.split(".").reduce<unknown>((acc, part) => (acc as Record<string, unknown>)?.[part], catalog)
Expand Down
2 changes: 2 additions & 0 deletions src/lib/i18n/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import { initReactI18next } from "react-i18next"
import * as Localization from "expo-localization"
import en from "./en.json"
import zhHans from "./zh-Hans.json"
import it from "./it.json"
import { resolveLocale, FALLBACK_LOCALE, type LocalePreference } from "./locale-resolve"

const resources = {
en: { translation: en },
"zh-Hans": { translation: zhHans },
it: { translation: it },
}

/** Device locale tags in priority order, e.g. ["zh-Hans-CN", "en-US"]. */
Expand Down
3 changes: 2 additions & 1 deletion src/lib/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@
"title": "Choose Language",
"system": "System Default",
"en": "English",
"zhHans": "Simplified Chinese"
"zhHans": "Simplified Chinese",
"it": "Italian"
},
"footer": {
"appName": "OpenCode Mobile",
Expand Down
Loading