feat: add modular i18n foundation
This commit is contained in:
190
docs/superpowers/plans/2026-05-26-i18n-modules.md
Normal file
190
docs/superpowers/plans/2026-05-26-i18n-modules.md
Normal file
@@ -0,0 +1,190 @@
|
||||
# I18n Modules Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Add maintainable feature-module internationalization for Chinese, English, and Thai.
|
||||
|
||||
**Architecture:** Use `vue-i18n` as the app i18n runtime and keep message ownership under `src/i18n/modules/<feature>/<locale>.ts`. Locale resolution, persistence, document language, and Vant locale synchronization are centralized behind `resolveInitialLocale` and `setLocale`.
|
||||
|
||||
**Tech Stack:** Vue 3.4, Vite 5, Vant 4.9, Yarn 1.22, Node test runner, TypeScript.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- Create: `src/i18n/types.ts` for locale constants, locale types, and type guards.
|
||||
- Create: `src/i18n/storage.ts` for safe browser storage reads and writes.
|
||||
- Create: `src/i18n/locales.ts` for initial locale resolution and browser language mapping.
|
||||
- Create: `src/i18n/vant.ts` for mapping app locales to Vant locale packs.
|
||||
- Create: `src/i18n/index.ts` for `vue-i18n` creation, message aggregation, and `setLocale`.
|
||||
- Create: `src/i18n/modules/common/{zh-CN,en-US,th-TH,index}.ts`.
|
||||
- Create: `src/i18n/modules/home/{zh-CN,en-US,th-TH,index}.ts`.
|
||||
- Create: `src/i18n/modules/quick/{zh-CN,en-US,th-TH,index}.ts`.
|
||||
- Create: `src/i18n/messages.ts` for top-level message aggregation.
|
||||
- Create: `src/i18n/i18n.test.ts` for locale and message consistency tests.
|
||||
- Modify: `src/main.ts` to install the i18n plugin.
|
||||
- Modify: `package.json` and `yarn.lock` by adding `vue-i18n@^11.4.4`.
|
||||
|
||||
## Task 1: Add Locale Logic Tests
|
||||
|
||||
**Files:**
|
||||
- Create: `src/i18n/i18n.test.ts`
|
||||
|
||||
- [ ] **Step 1: Write failing tests for locale resolution and message key consistency**
|
||||
|
||||
```ts
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import { defaultLocale, supportedLocales } from "./types.ts";
|
||||
import { isSupportedLocale, resolveInitialLocale, resolveLocaleFromNavigator } from "./locales.ts";
|
||||
import { messages } from "./messages.ts";
|
||||
|
||||
function flattenKeys(value: unknown, prefix = ""): string[] {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
return [prefix];
|
||||
}
|
||||
|
||||
return Object.entries(value as Record<string, unknown>).flatMap(([key, nestedValue]) =>
|
||||
flattenKeys(nestedValue, prefix ? `${prefix}.${key}` : key),
|
||||
);
|
||||
}
|
||||
|
||||
describe("i18n locale model", () => {
|
||||
it("supports Chinese, English, and Thai", () => {
|
||||
assert.deepEqual([...supportedLocales], ["zh-CN", "en-US", "th-TH"]);
|
||||
assert.equal(defaultLocale, "zh-CN");
|
||||
});
|
||||
|
||||
it("accepts only supported locale codes", () => {
|
||||
assert.equal(isSupportedLocale("zh-CN"), true);
|
||||
assert.equal(isSupportedLocale("en-US"), true);
|
||||
assert.equal(isSupportedLocale("th-TH"), true);
|
||||
assert.equal(isSupportedLocale("en"), false);
|
||||
assert.equal(isSupportedLocale("fr-FR"), false);
|
||||
});
|
||||
|
||||
it("maps browser languages to supported locales", () => {
|
||||
assert.equal(resolveLocaleFromNavigator(["zh-Hans-CN"]), "zh-CN");
|
||||
assert.equal(resolveLocaleFromNavigator(["en-GB"]), "en-US");
|
||||
assert.equal(resolveLocaleFromNavigator(["th"]), "th-TH");
|
||||
assert.equal(resolveLocaleFromNavigator(["fr-FR"]), defaultLocale);
|
||||
});
|
||||
|
||||
it("prefers stored locale over browser language", () => {
|
||||
assert.equal(resolveInitialLocale({ storedLocale: "th-TH", navigatorLanguages: ["en-US"] }), "th-TH");
|
||||
});
|
||||
|
||||
it("falls back to browser language when stored locale is invalid", () => {
|
||||
assert.equal(resolveInitialLocale({ storedLocale: "invalid", navigatorLanguages: ["en-US"] }), "en-US");
|
||||
});
|
||||
|
||||
it("keeps translation key structure consistent across locales", () => {
|
||||
const referenceKeys = flattenKeys(messages["zh-CN"]).sort();
|
||||
|
||||
for (const locale of supportedLocales) {
|
||||
assert.deepEqual(flattenKeys(messages[locale]).sort(), referenceKeys, locale);
|
||||
}
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run tests and verify the expected failure**
|
||||
|
||||
Run: `corepack yarn test`
|
||||
|
||||
Expected: FAIL because `src/i18n/types.ts`, `src/i18n/locales.ts`, and `src/i18n/messages.ts` do not exist yet.
|
||||
|
||||
## Task 2: Implement Locale Model and Message Modules
|
||||
|
||||
**Files:**
|
||||
- Create: all `src/i18n` files except `index.ts` and `vant.ts`
|
||||
|
||||
- [ ] **Step 1: Implement locale constants and resolution**
|
||||
|
||||
Add `supportedLocales`, `defaultLocale`, `isSupportedLocale`, `resolveLocaleFromNavigator`, and `resolveInitialLocale` exactly as tested.
|
||||
|
||||
- [ ] **Step 2: Implement safe storage helpers**
|
||||
|
||||
Add `localeStorageKey`, `readStoredLocale`, and `writeStoredLocale` so unavailable browser storage never blocks startup.
|
||||
Cover storage getter failures with a regression test before final verification.
|
||||
|
||||
- [ ] **Step 3: Implement message modules**
|
||||
|
||||
Create `common`, `home`, and `quick` modules with matching key shapes for `zh-CN`, `en-US`, and `th-TH`.
|
||||
|
||||
- [ ] **Step 4: Run tests and verify locale tests pass**
|
||||
|
||||
Run: `corepack yarn test`
|
||||
|
||||
Expected: PASS for locale model and message key consistency.
|
||||
|
||||
## Task 3: Install and Wire Runtime I18n
|
||||
|
||||
**Files:**
|
||||
- Modify: `package.json`
|
||||
- Modify: `yarn.lock`
|
||||
- Create: `src/i18n/vant.ts`
|
||||
- Create: `src/i18n/index.ts`
|
||||
- Modify: `src/main.ts`
|
||||
|
||||
- [ ] **Step 1: Add `vue-i18n` dependency**
|
||||
|
||||
Run: `corepack yarn add vue-i18n@^11.4.4`
|
||||
|
||||
Expected: `package.json` and `yarn.lock` include `vue-i18n`.
|
||||
|
||||
- [ ] **Step 2: Implement Vant locale synchronization**
|
||||
|
||||
Use Vant's `Locale.use(locale, messages)` API with local language packs from `vant/es/locale/lang/*.mjs`.
|
||||
|
||||
- [ ] **Step 3: Create the i18n plugin**
|
||||
|
||||
Use `createI18n({ legacy: false, locale, fallbackLocale: defaultLocale, messages })`, export `i18n`, `setLocale`, and `getCurrentLocale`.
|
||||
|
||||
- [ ] **Step 4: Register i18n in the app**
|
||||
|
||||
Modify `src/main.ts` to call `.use(i18n)` before mounting.
|
||||
|
||||
- [ ] **Step 5: Run typecheck**
|
||||
|
||||
Run: `corepack yarn typecheck`
|
||||
|
||||
Expected: PASS with no TypeScript errors.
|
||||
|
||||
## Task 4: Final Verification and Commit
|
||||
|
||||
**Files:**
|
||||
- Verify all changed files.
|
||||
|
||||
- [ ] **Step 1: Run unit tests**
|
||||
|
||||
Run: `corepack yarn test`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 2: Run production build**
|
||||
|
||||
Run: `corepack yarn build`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 3: Review diff for scope**
|
||||
|
||||
Run: `git status --short` and `git diff -- src package.json yarn.lock docs/superpowers/plans/2026-05-26-i18n-modules.md`.
|
||||
|
||||
Expected: Only i18n implementation files, dependency metadata, the approved plan, and pre-existing user changes appear.
|
||||
|
||||
- [ ] **Step 4: Commit only implementation-owned files**
|
||||
|
||||
Stage:
|
||||
|
||||
```bash
|
||||
git add -f docs/superpowers/plans/2026-05-26-i18n-modules.md
|
||||
git add package.json yarn.lock src/main.ts src/i18n
|
||||
```
|
||||
|
||||
Commit:
|
||||
|
||||
```bash
|
||||
git commit -m "feat: add modular i18n foundation"
|
||||
```
|
||||
Reference in New Issue
Block a user