---
id: w5/m3/t001
title: i18n foundation — i18next core + provider
worker: worker5
status: done
estimate: 90m
depends_on: []
---

## Objective

Stand up the i18n runtime for the dashboard: add `i18next` + `react-i18next`, a
`src/i18n/` core (supported-language constants, resource aggregation, init), a
feature-based `locales/` convention, and a type-safe `useTranslations()` hook.
Wire the provider into the TanStack Start root so every route can call `t()`.
This is the substrate the other tasks build on; no strings need to be extracted
yet (that's t004) — just prove `t("common.appName")` renders in a page.

## Context

- Reference architecture (read-only, not in this repo):
  `~/projects/web-beancount/beancount-dashboard/docs/i18n.md`,
  `src/i18n/{config,init,index}.ts`, `src/i18n/locales/index.ts` (aggregation
  layer), `src/common/hooks/use-translations.ts` (dev-time key validation).
- Reference uses **feature-based locales**: `src/features/<feature>/locales/{en,zh,…}.ts`
  each exporting `Record<string, TranslationEntry>` where
  `TranslationEntry = { message: string; description: string }`, aggregated and
  flattened (`extractMessages`) into i18next's flat `{ key: message }` resources.
  Keys are namespace-prefixed (`common.*`, `auth.*`, `metrics.*`).
- This repo is TanStack **Start** (SSR), not the reference's vite-ssr entry
  files — the wiring point is `src/routes/__root.tsx` /
  `src/common/providers/root-provider.tsx`, not `entry-client/server.tsx`. SSR
  language detection is t002; here just initialize with a default and provide
  `I18nextProvider`.
- Keep the scope small: `common` namespace + one feature namespace to prove the
  pattern. Do **not** copy the reference's 13 languages — `en` only here; `zh`
  lands in t006.

## Steps

1. `yarn add i18next react-i18next`.
2. Create `src/i18n/`: `config.ts` (`SUPPORTED_LANGUAGES`, `LANGUAGE_NAMES`,
   `DEFAULT_LANGUAGE`, `SupportedLanguage` type), `index.ts` (`TranslationEntry`
   type + `extractMessages` + resource aggregation), `init.ts` (create/init an
   i18next instance with `initReactI18next`, `en` resources, no `LanguageDetector`).
3. Add `src/common/locales/en.ts` with a couple of shared keys (`common.appName`, etc.).
4. Add `src/common/hooks/use-translations.ts` — wraps `useTranslation`, extracts
   `message`, and in `import.meta.env.DEV` warns on unprefixed / unknown keys.
5. Wrap the app in `I18nextProvider` inside `RootProvider` (or `__root.tsx`),
   passing the initialized instance.
6. Prove it: render one `t("common.appName")` somewhere and confirm it shows.

## Files

- `dashboard/package.json` (deps)
- `dashboard/src/i18n/{config.ts,index.ts,init.ts}`
- `dashboard/src/common/locales/en.ts`
- `dashboard/src/common/hooks/use-translations.ts`
- `dashboard/src/common/providers/root-provider.tsx` (or `src/routes/__root.tsx`)

## Acceptance criteria

- [ ] `t("common.appName")` renders its English message in a live page.
- [ ] Unprefixed key (e.g. `t("appName")`) logs a dev-only console error; unknown
      key logs a dev-only warning.
- [ ] `yarn typecheck` and `yarn lint` pass.
- [ ] No `LanguageDetector` plugin is used (SSR handles detection in t002).

## Out of scope

- SSR language detection / persistence (t002).
- Extracting existing hardcoded strings (t004).
- Ory Elements localization (t005).
- Any language other than `en` (t006).
</content>
