From b05d5a72cddf45203e9255518acdc43f45be951a Mon Sep 17 00:00:00 2001 From: duanshuwen Date: Tue, 26 May 2026 14:37:32 +0800 Subject: [PATCH] feat: add modular i18n foundation --- .../plans/2026-05-26-i18n-modules.md | 190 ++++++++++++++++++ index.html | 2 +- package.json | 1 + src/i18n/i18n.test.ts | 76 +++++++ src/i18n/index.ts | 60 ++++++ src/i18n/locales.ts | 39 ++++ src/i18n/messages.ts | 25 +++ src/i18n/modules/common/en-US.ts | 14 ++ src/i18n/modules/common/index.ts | 12 ++ src/i18n/modules/common/th-TH.ts | 14 ++ src/i18n/modules/common/zh-CN.ts | 14 ++ src/i18n/modules/home/en-US.ts | 4 + src/i18n/modules/home/index.ts | 12 ++ src/i18n/modules/home/th-TH.ts | 4 + src/i18n/modules/home/zh-CN.ts | 4 + src/i18n/modules/quick/en-US.ts | 7 + src/i18n/modules/quick/index.ts | 12 ++ src/i18n/modules/quick/th-TH.ts | 7 + src/i18n/modules/quick/zh-CN.ts | 7 + src/i18n/storage.ts | 35 ++++ src/i18n/types.ts | 9 + src/i18n/vant.ts | 15 ++ src/main.ts | 3 +- yarn.lock | 44 +++- 24 files changed, 606 insertions(+), 4 deletions(-) create mode 100644 docs/superpowers/plans/2026-05-26-i18n-modules.md create mode 100644 src/i18n/i18n.test.ts create mode 100644 src/i18n/index.ts create mode 100644 src/i18n/locales.ts create mode 100644 src/i18n/messages.ts create mode 100644 src/i18n/modules/common/en-US.ts create mode 100644 src/i18n/modules/common/index.ts create mode 100644 src/i18n/modules/common/th-TH.ts create mode 100644 src/i18n/modules/common/zh-CN.ts create mode 100644 src/i18n/modules/home/en-US.ts create mode 100644 src/i18n/modules/home/index.ts create mode 100644 src/i18n/modules/home/th-TH.ts create mode 100644 src/i18n/modules/home/zh-CN.ts create mode 100644 src/i18n/modules/quick/en-US.ts create mode 100644 src/i18n/modules/quick/index.ts create mode 100644 src/i18n/modules/quick/th-TH.ts create mode 100644 src/i18n/modules/quick/zh-CN.ts create mode 100644 src/i18n/storage.ts create mode 100644 src/i18n/types.ts create mode 100644 src/i18n/vant.ts diff --git a/docs/superpowers/plans/2026-05-26-i18n-modules.md b/docs/superpowers/plans/2026-05-26-i18n-modules.md new file mode 100644 index 0000000..f2dd905 --- /dev/null +++ b/docs/superpowers/plans/2026-05-26-i18n-modules.md @@ -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//.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).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" +``` diff --git a/index.html b/index.html index d3c9202..bedd4a2 100644 --- a/index.html +++ b/index.html @@ -8,6 +8,6 @@
- + diff --git a/package.json b/package.json index f1f651a..8bc2ef4 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "pinia": "^3.0.3", "vant": "^4.9.24", "vue": "^3.4.21", + "vue-i18n": "11.4.4", "vue-router": "^4.5.1" }, "devDependencies": { diff --git a/src/i18n/i18n.test.ts b/src/i18n/i18n.test.ts new file mode 100644 index 0000000..f4463c5 --- /dev/null +++ b/src/i18n/i18n.test.ts @@ -0,0 +1,76 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { isSupportedLocale, resolveInitialLocale, resolveLocaleFromNavigator } from "./locales.ts"; +import { messages } from "./messages.ts"; +import { readStoredLocale, writeStoredLocale } from "./storage.ts"; +import { defaultLocale, supportedLocales } from "./types.ts"; + +function flattenKeys(value: unknown, prefix = ""): string[] { + if (!value || typeof value !== "object" || Array.isArray(value)) { + return [prefix]; + } + + return Object.entries(value as Record).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); + } + }); + + it("does not throw when browser storage is unavailable", () => { + const descriptor = Object.getOwnPropertyDescriptor(globalThis, "localStorage"); + + Object.defineProperty(globalThis, "localStorage", { + configurable: true, + get() { + throw new Error("storage blocked"); + }, + }); + + try { + assert.equal(readStoredLocale(), null); + assert.doesNotThrow(() => writeStoredLocale("zh-CN")); + } finally { + if (descriptor) { + Object.defineProperty(globalThis, "localStorage", descriptor); + } else { + Reflect.deleteProperty(globalThis, "localStorage"); + } + } + }); +}); diff --git a/src/i18n/index.ts b/src/i18n/index.ts new file mode 100644 index 0000000..9d18ad7 --- /dev/null +++ b/src/i18n/index.ts @@ -0,0 +1,60 @@ +import { createI18n } from "vue-i18n"; +import { isSupportedLocale, resolveInitialLocale } from "./locales.ts"; +import { messages } from "./messages.ts"; +import { readStoredLocale, writeStoredLocale } from "./storage.ts"; +import { defaultLocale } from "./types.ts"; +import type { SupportedLocale } from "./types.ts"; +import { syncVantLocale } from "./vant.ts"; + +function getNavigatorLanguages(): string[] { + if (typeof navigator === "undefined") { + return []; + } + + const languages = navigator.languages ? [...navigator.languages] : []; + + if (languages.length > 0) { + return languages; + } + + return navigator.language ? [navigator.language] : []; +} + +function setDocumentLanguage(locale: SupportedLocale): void { + if (typeof document === "undefined") { + return; + } + + document.documentElement.lang = locale; +} + +const initialLocale = resolveInitialLocale({ + storedLocale: readStoredLocale(), + navigatorLanguages: getNavigatorLanguages(), +}); + +syncVantLocale(initialLocale); +setDocumentLanguage(initialLocale); + +export const i18n = createI18n({ + legacy: false, + locale: initialLocale, + fallbackLocale: defaultLocale, + messages, +}); + +export function getCurrentLocale(): SupportedLocale { + const locale = i18n.global.locale.value; + return isSupportedLocale(locale) ? locale : defaultLocale; +} + +export function setLocale(locale: string): void { + if (!isSupportedLocale(locale)) { + return; + } + + i18n.global.locale.value = locale; + writeStoredLocale(locale); + syncVantLocale(locale); + setDocumentLanguage(locale); +} diff --git a/src/i18n/locales.ts b/src/i18n/locales.ts new file mode 100644 index 0000000..7a10160 --- /dev/null +++ b/src/i18n/locales.ts @@ -0,0 +1,39 @@ +import { defaultLocale, supportedLocales } from "./types.ts"; +import type { SupportedLocale } from "./types.ts"; + +export function isSupportedLocale(locale: string | null | undefined): locale is SupportedLocale { + return supportedLocales.includes(locale as SupportedLocale); +} + +export function resolveLocaleFromNavigator(languages: readonly string[] = []): SupportedLocale { + for (const language of languages) { + const normalizedLanguage = language.toLowerCase(); + + if (normalizedLanguage.startsWith("zh")) { + return "zh-CN"; + } + + if (normalizedLanguage.startsWith("en")) { + return "en-US"; + } + + if (normalizedLanguage.startsWith("th")) { + return "th-TH"; + } + } + + return defaultLocale; +} + +type ResolveInitialLocaleOptions = { + storedLocale?: string | null; + navigatorLanguages?: readonly string[]; +}; + +export function resolveInitialLocale(options: ResolveInitialLocaleOptions = {}): SupportedLocale { + if (isSupportedLocale(options.storedLocale)) { + return options.storedLocale; + } + + return resolveLocaleFromNavigator(options.navigatorLanguages); +} diff --git a/src/i18n/messages.ts b/src/i18n/messages.ts new file mode 100644 index 0000000..6ee353e --- /dev/null +++ b/src/i18n/messages.ts @@ -0,0 +1,25 @@ +import { supportedLocales } from "./types.ts"; +import type { SupportedLocale } from "./types.ts"; +import { commonMessages } from "./modules/common/index.ts"; +import type { CommonMessages } from "./modules/common/index.ts"; +import { homeMessages } from "./modules/home/index.ts"; +import type { HomeMessages } from "./modules/home/index.ts"; +import { quickMessages } from "./modules/quick/index.ts"; +import type { QuickMessages } from "./modules/quick/index.ts"; + +export type AppMessages = { + common: CommonMessages; + home: HomeMessages; + quick: QuickMessages; +}; + +export const messages = Object.fromEntries( + supportedLocales.map((locale) => [ + locale, + { + common: commonMessages[locale], + home: homeMessages[locale], + quick: quickMessages[locale], + }, + ]), +) as Record; diff --git a/src/i18n/modules/common/en-US.ts b/src/i18n/modules/common/en-US.ts new file mode 100644 index 0000000..acc6294 --- /dev/null +++ b/src/i18n/modules/common/en-US.ts @@ -0,0 +1,14 @@ +export default { + actions: { + cancel: "Cancel", + confirm: "Confirm", + retry: "Retry", + }, + errors: { + network: "Network error. Please try again later.", + }, + state: { + empty: "No data", + loading: "Loading...", + }, +} as const; diff --git a/src/i18n/modules/common/index.ts b/src/i18n/modules/common/index.ts new file mode 100644 index 0000000..2fa1bd9 --- /dev/null +++ b/src/i18n/modules/common/index.ts @@ -0,0 +1,12 @@ +import type { LocaleMessageSchema, SupportedLocale } from "../../types.ts"; +import enUS from "./en-US.ts"; +import thTH from "./th-TH.ts"; +import zhCN from "./zh-CN.ts"; + +export type CommonMessages = LocaleMessageSchema; + +export const commonMessages = { + "zh-CN": zhCN, + "en-US": enUS, + "th-TH": thTH, +} satisfies Record; diff --git a/src/i18n/modules/common/th-TH.ts b/src/i18n/modules/common/th-TH.ts new file mode 100644 index 0000000..62cd7a5 --- /dev/null +++ b/src/i18n/modules/common/th-TH.ts @@ -0,0 +1,14 @@ +export default { + actions: { + cancel: "ยกเลิก", + confirm: "ยืนยัน", + retry: "ลองอีกครั้ง", + }, + errors: { + network: "เครือข่ายผิดพลาด โปรดลองอีกครั้งในภายหลัง", + }, + state: { + empty: "ไม่มีข้อมูล", + loading: "กำลังโหลด...", + }, +} as const; diff --git a/src/i18n/modules/common/zh-CN.ts b/src/i18n/modules/common/zh-CN.ts new file mode 100644 index 0000000..0bc3ed8 --- /dev/null +++ b/src/i18n/modules/common/zh-CN.ts @@ -0,0 +1,14 @@ +export default { + actions: { + cancel: "取消", + confirm: "确认", + retry: "重试", + }, + errors: { + network: "网络异常,请稍后重试", + }, + state: { + empty: "暂无数据", + loading: "加载中...", + }, +} as const; diff --git a/src/i18n/modules/home/en-US.ts b/src/i18n/modules/home/en-US.ts new file mode 100644 index 0000000..d85c3ca --- /dev/null +++ b/src/i18n/modules/home/en-US.ts @@ -0,0 +1,4 @@ +export default { + subtitle: "Welcome to the mobile app", + title: "Home", +} as const; diff --git a/src/i18n/modules/home/index.ts b/src/i18n/modules/home/index.ts new file mode 100644 index 0000000..2592df0 --- /dev/null +++ b/src/i18n/modules/home/index.ts @@ -0,0 +1,12 @@ +import type { LocaleMessageSchema, SupportedLocale } from "../../types.ts"; +import enUS from "./en-US.ts"; +import thTH from "./th-TH.ts"; +import zhCN from "./zh-CN.ts"; + +export type HomeMessages = LocaleMessageSchema; + +export const homeMessages = { + "zh-CN": zhCN, + "en-US": enUS, + "th-TH": thTH, +} satisfies Record; diff --git a/src/i18n/modules/home/th-TH.ts b/src/i18n/modules/home/th-TH.ts new file mode 100644 index 0000000..703232b --- /dev/null +++ b/src/i18n/modules/home/th-TH.ts @@ -0,0 +1,4 @@ +export default { + subtitle: "ยินดีต้อนรับสู่แอปมือถือ", + title: "หน้าแรก", +} as const; diff --git a/src/i18n/modules/home/zh-CN.ts b/src/i18n/modules/home/zh-CN.ts new file mode 100644 index 0000000..c05411e --- /dev/null +++ b/src/i18n/modules/home/zh-CN.ts @@ -0,0 +1,4 @@ +export default { + subtitle: "欢迎使用移动端应用", + title: "首页", +} as const; diff --git a/src/i18n/modules/quick/en-US.ts b/src/i18n/modules/quick/en-US.ts new file mode 100644 index 0000000..3e8f35d --- /dev/null +++ b/src/i18n/modules/quick/en-US.ts @@ -0,0 +1,7 @@ +export default { + form: { + placeholder: "Enter content", + submit: "Submit", + }, + title: "Quick Actions", +} as const; diff --git a/src/i18n/modules/quick/index.ts b/src/i18n/modules/quick/index.ts new file mode 100644 index 0000000..2593c2a --- /dev/null +++ b/src/i18n/modules/quick/index.ts @@ -0,0 +1,12 @@ +import type { LocaleMessageSchema, SupportedLocale } from "../../types.ts"; +import enUS from "./en-US.ts"; +import thTH from "./th-TH.ts"; +import zhCN from "./zh-CN.ts"; + +export type QuickMessages = LocaleMessageSchema; + +export const quickMessages = { + "zh-CN": zhCN, + "en-US": enUS, + "th-TH": thTH, +} satisfies Record; diff --git a/src/i18n/modules/quick/th-TH.ts b/src/i18n/modules/quick/th-TH.ts new file mode 100644 index 0000000..c3c6018 --- /dev/null +++ b/src/i18n/modules/quick/th-TH.ts @@ -0,0 +1,7 @@ +export default { + form: { + placeholder: "กรอกเนื้อหา", + submit: "ส่ง", + }, + title: "การดำเนินการด่วน", +} as const; diff --git a/src/i18n/modules/quick/zh-CN.ts b/src/i18n/modules/quick/zh-CN.ts new file mode 100644 index 0000000..e72ffa2 --- /dev/null +++ b/src/i18n/modules/quick/zh-CN.ts @@ -0,0 +1,7 @@ +export default { + form: { + placeholder: "请输入内容", + submit: "提交", + }, + title: "快捷操作", +} as const; diff --git a/src/i18n/storage.ts b/src/i18n/storage.ts new file mode 100644 index 0000000..8a2b45a --- /dev/null +++ b/src/i18n/storage.ts @@ -0,0 +1,35 @@ +import { isSupportedLocale } from "./locales.ts"; +import type { SupportedLocale } from "./types.ts"; + +export const localeStorageKey = "nianxx.locale"; + +type LocaleStorageLike = Pick; + +function resolveStorage(storage?: LocaleStorageLike): LocaleStorageLike | undefined { + if (storage) { + return storage; + } + + try { + return globalThis.localStorage; + } catch { + return undefined; + } +} + +export function readStoredLocale(storage?: LocaleStorageLike): SupportedLocale | null { + try { + const locale = resolveStorage(storage)?.getItem(localeStorageKey); + return isSupportedLocale(locale) ? locale : null; + } catch { + return null; + } +} + +export function writeStoredLocale(locale: SupportedLocale, storage?: LocaleStorageLike): void { + try { + resolveStorage(storage)?.setItem(localeStorageKey, locale); + } catch { + // Storage can be unavailable in private or embedded browser contexts. + } +} diff --git a/src/i18n/types.ts b/src/i18n/types.ts new file mode 100644 index 0000000..225287f --- /dev/null +++ b/src/i18n/types.ts @@ -0,0 +1,9 @@ +export const supportedLocales = ["zh-CN", "en-US", "th-TH"] as const; + +export type SupportedLocale = (typeof supportedLocales)[number]; + +export const defaultLocale: SupportedLocale = "zh-CN"; + +export type LocaleMessageSchema = { + readonly [Key in keyof T]: T[Key] extends string ? string : LocaleMessageSchema; +}; diff --git a/src/i18n/vant.ts b/src/i18n/vant.ts new file mode 100644 index 0000000..e7212b7 --- /dev/null +++ b/src/i18n/vant.ts @@ -0,0 +1,15 @@ +import { Locale } from "vant"; +import enUS from "vant/es/locale/lang/en-US"; +import thTH from "vant/es/locale/lang/th-TH"; +import zhCN from "vant/es/locale/lang/zh-CN"; +import type { SupportedLocale } from "./types.ts"; + +const vantMessages = { + "zh-CN": zhCN, + "en-US": enUS, + "th-TH": thTH, +} satisfies Record>; + +export function syncVantLocale(locale: SupportedLocale): void { + Locale.use(locale, vantMessages[locale]); +} diff --git a/src/main.ts b/src/main.ts index a63de9d..f7d96d0 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,8 +1,9 @@ import { createApp } from "vue"; import App from "./App.vue"; +import { i18n } from "./i18n"; import { pinia } from "./stores"; import { router } from "./router"; import "vant/lib/index.css"; import "@/styles/main.css"; -createApp(App).use(pinia).use(router).mount("#app"); +createApp(App).use(pinia).use(i18n).use(router).mount("#app"); diff --git a/yarn.lock b/yarn.lock index 7c4d6da..2e4b51c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -164,6 +164,36 @@ resolved "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz#9c907b21e30a52db959ba4f80bb01a0cc403d5cc" integrity sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ== +"@intlify/core-base@11.4.4": + version "11.4.4" + resolved "https://registry.npmmirror.com/@intlify/core-base/-/core-base-11.4.4.tgz#ea20ab23eae5e96657fdd006e6b7fd57851fbbc8" + integrity sha512-w/vItlylrAmhebkIbVl5YY8XMCtj8Mb2g70ttxktMYuf5AuRahgEHL2iLgLIsZBIbTSgs4hkUo7ucCL0uTJvOg== + dependencies: + "@intlify/devtools-types" "11.4.4" + "@intlify/message-compiler" "11.4.4" + "@intlify/shared" "11.4.4" + +"@intlify/devtools-types@11.4.4": + version "11.4.4" + resolved "https://registry.npmmirror.com/@intlify/devtools-types/-/devtools-types-11.4.4.tgz#a64d9e135c04c5f00e430fff86ca5467e8fbd9cf" + integrity sha512-PcBLmGmDQsTSVV911P8upzpcLJO1CNVYi/IH6bGnLR2nA+0L963+kXN1ZrisTEnbtw2ewN6HMMSldqzjronA0Q== + dependencies: + "@intlify/core-base" "11.4.4" + "@intlify/shared" "11.4.4" + +"@intlify/message-compiler@11.4.4": + version "11.4.4" + resolved "https://registry.npmmirror.com/@intlify/message-compiler/-/message-compiler-11.4.4.tgz#946d5de62a9129a5009382def22cc4e0e2532df9" + integrity sha512-vn0OAV9pYkJlPPmgnsSm5eAG3mL0+9C/oaded2JY9jmxBbhmUXT3TcAUY8WRgLY9Hte7lkUJKpXrVlYjMXBD2w== + dependencies: + "@intlify/shared" "11.4.4" + source-map-js "^1.0.2" + +"@intlify/shared@11.4.4": + version "11.4.4" + resolved "https://registry.npmmirror.com/@intlify/shared/-/shared-11.4.4.tgz#4ccb2816bd0a41de288048f87bd946a8d629e906" + integrity sha512-QRUCHqda1U6aR14FR0vvXD4+4gj6+fm0AhAozvSuRCw0fCvrmCugWpgiR4xH2NI6s8am6N9p5OhirplsX8ZS3g== + "@jridgewell/gen-mapping@^0.3.5": version "0.3.13" resolved "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz#6342a19f44347518c93e43b1ac69deb3c4656a1f" @@ -547,7 +577,7 @@ de-indent "^1.0.2" he "^1.2.0" -"@vue/devtools-api@^6.6.4": +"@vue/devtools-api@^6.5.0", "@vue/devtools-api@^6.6.4": version "6.6.4" resolved "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz#cbe97fe0162b365edc1dba80e173f90492535343" integrity sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g== @@ -1119,7 +1149,7 @@ rollup@^4.13.0: "@rollup/rollup-win32-x64-msvc" "4.60.4" fsevents "~2.3.2" -source-map-js@^1.2.1: +source-map-js@^1.0.2, source-map-js@^1.2.1: version "1.2.1" resolved "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== @@ -1186,6 +1216,16 @@ vscode-uri@^3.0.8: resolved "https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.1.0.tgz#dd09ec5a66a38b5c3fffc774015713496d14e09c" integrity sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ== +vue-i18n@11.4.4: + version "11.4.4" + resolved "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-11.4.4.tgz#92de5050e582074168f115458358a123b915debd" + integrity sha512-gIbXVSFQV4jcSJxfwdZ5zSZmZ+12CnX0K3vBkRSd6Zn+HSzCp+QwUgPwpD/uN0oKNKI9RzlUXPKVedEuMgNG0A== + dependencies: + "@intlify/core-base" "11.4.4" + "@intlify/devtools-types" "11.4.4" + "@intlify/shared" "11.4.4" + "@vue/devtools-api" "^6.5.0" + vue-router@^4.5.1: version "4.6.4" resolved "https://registry.npmmirror.com/vue-router/-/vue-router-4.6.4.tgz#a0a9cb9ef811a106d249e4bb9313d286718020d8"