Localization¶
Tomoda ships in four locales, wired up through i18next + react-i18next with system-locale detection from expo-localization.
Supported locales¶
| BCP 47 tag | Language | File |
|---|---|---|
en-US |
English (US) — source of truth | frontend/i18n/locales/en-US.json |
ja-JP |
Japanese | frontend/i18n/locales/ja-JP.json |
zh-CN |
Simplified Chinese | frontend/i18n/locales/zh-CN.json |
zh-TW |
Traditional Chinese | frontend/i18n/locales/zh-TW.json |
en-US is the fallback — any missing key in another locale resolves against the English file.
Bootstrap¶
frontend/i18n/index.ts is imported once from app/_layout.tsx:
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import * as Localization from 'expo-localization';
i18n.use(initReactI18next).init({
resources: {
'en-US': { translation: enUS },
'ja-JP': { translation: jaJP },
'zh-CN': { translation: zhCN },
'zh-TW': { translation: zhTW },
},
lng: getSystemLanguage(),
fallbackLng: 'en-US',
interpolation: { escapeValue: false },
compatibilityJSON: 'v4',
});
Fallback chain¶
getSystemLanguage() walks the device locale list returned by Localization.getLocales():
- If the primary tag is one of the four supported keys, use it.
- Otherwise, fall back by language code:
zh+ regionCN→zh-CNzh+ any other region →zh-TWja→ja-JPen→en-US- Anything else →
en-US.
The AuthContext syncs i18n.changeLanguage() whenever the persisted server profile's user.language changes — so the server is the source of truth once the user has signed in, and the device locale is only used pre-login.
Usage¶
Component-side, just call the hook:
import { useTranslation } from 'react-i18next';
export function LoginScreen() {
const { t } = useTranslation();
return (
<Button title={t('auth.login.submit')} />
);
}
Imperative reads (outside components) go through the singleton:
import i18n from '@/i18n';
i18n.t('errors.network');
i18n.changeLanguage('ja-JP');
Interpolation uses the i18next default {{var}} syntax:
{ "discover.nearbyCount": "{{count}} events nearby" }
t('discover.nearbyCount', { count: 12 })
Validation script¶
frontend/scripts/compare_i18n.js diffs each locale against en-US.json and prints missing and extra keys:
node frontend/scripts/compare_i18n.js
The script resolves localesDir relative to its own location, so it works on any checkout without configuration.
Tip
For substantive i18n maintenance (extracting hardcoded strings, removing orphan keys, autofilling missing translations), use the /locale-reconcile skill rather than this script.
/locale-reconcile skill¶
The user-defined /locale-reconcile skill is the recommended way to keep locales in sync. It:
- Audits the four locale files against
en-US. - Finds keys referenced in code but missing from one or more locale files.
- Detects unused locale keys (defined but not referenced).
- Surfaces hardcoded user-facing strings that should be wired through
t(). - Optionally fills in missing translations.
Invoke it whenever you add or remove i18n keys.
Conventions¶
- Key style: dot-separated lowercase with subdomains (
auth.login.submit,discover.nearbyCount). - No raw strings in JSX: every user-facing string goes through
t(). The reconcile skill flags violations. - Plurals: use i18next's
_one/_othersuffixes when needed. - Dates: format with
date-fns(already a dependency) using the activei18n.language— do not put dates in locale files.
Cross-links¶
- State Management —
AuthContextsyncsuser.languageto i18n - Components — feature folders consume
useTranslation()throughout