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"
|
||||
```
|
||||
@@ -8,6 +8,6 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/app/main.ts"></script>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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": {
|
||||
|
||||
76
src/i18n/i18n.test.ts
Normal file
76
src/i18n/i18n.test.ts
Normal file
@@ -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<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);
|
||||
}
|
||||
});
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
60
src/i18n/index.ts
Normal file
60
src/i18n/index.ts
Normal file
@@ -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);
|
||||
}
|
||||
39
src/i18n/locales.ts
Normal file
39
src/i18n/locales.ts
Normal file
@@ -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);
|
||||
}
|
||||
25
src/i18n/messages.ts
Normal file
25
src/i18n/messages.ts
Normal file
@@ -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<SupportedLocale, AppMessages>;
|
||||
14
src/i18n/modules/common/en-US.ts
Normal file
14
src/i18n/modules/common/en-US.ts
Normal file
@@ -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;
|
||||
12
src/i18n/modules/common/index.ts
Normal file
12
src/i18n/modules/common/index.ts
Normal file
@@ -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<typeof zhCN>;
|
||||
|
||||
export const commonMessages = {
|
||||
"zh-CN": zhCN,
|
||||
"en-US": enUS,
|
||||
"th-TH": thTH,
|
||||
} satisfies Record<SupportedLocale, CommonMessages>;
|
||||
14
src/i18n/modules/common/th-TH.ts
Normal file
14
src/i18n/modules/common/th-TH.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export default {
|
||||
actions: {
|
||||
cancel: "ยกเลิก",
|
||||
confirm: "ยืนยัน",
|
||||
retry: "ลองอีกครั้ง",
|
||||
},
|
||||
errors: {
|
||||
network: "เครือข่ายผิดพลาด โปรดลองอีกครั้งในภายหลัง",
|
||||
},
|
||||
state: {
|
||||
empty: "ไม่มีข้อมูล",
|
||||
loading: "กำลังโหลด...",
|
||||
},
|
||||
} as const;
|
||||
14
src/i18n/modules/common/zh-CN.ts
Normal file
14
src/i18n/modules/common/zh-CN.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export default {
|
||||
actions: {
|
||||
cancel: "取消",
|
||||
confirm: "确认",
|
||||
retry: "重试",
|
||||
},
|
||||
errors: {
|
||||
network: "网络异常,请稍后重试",
|
||||
},
|
||||
state: {
|
||||
empty: "暂无数据",
|
||||
loading: "加载中...",
|
||||
},
|
||||
} as const;
|
||||
4
src/i18n/modules/home/en-US.ts
Normal file
4
src/i18n/modules/home/en-US.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default {
|
||||
subtitle: "Welcome to the mobile app",
|
||||
title: "Home",
|
||||
} as const;
|
||||
12
src/i18n/modules/home/index.ts
Normal file
12
src/i18n/modules/home/index.ts
Normal file
@@ -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<typeof zhCN>;
|
||||
|
||||
export const homeMessages = {
|
||||
"zh-CN": zhCN,
|
||||
"en-US": enUS,
|
||||
"th-TH": thTH,
|
||||
} satisfies Record<SupportedLocale, HomeMessages>;
|
||||
4
src/i18n/modules/home/th-TH.ts
Normal file
4
src/i18n/modules/home/th-TH.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default {
|
||||
subtitle: "ยินดีต้อนรับสู่แอปมือถือ",
|
||||
title: "หน้าแรก",
|
||||
} as const;
|
||||
4
src/i18n/modules/home/zh-CN.ts
Normal file
4
src/i18n/modules/home/zh-CN.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default {
|
||||
subtitle: "欢迎使用移动端应用",
|
||||
title: "首页",
|
||||
} as const;
|
||||
7
src/i18n/modules/quick/en-US.ts
Normal file
7
src/i18n/modules/quick/en-US.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export default {
|
||||
form: {
|
||||
placeholder: "Enter content",
|
||||
submit: "Submit",
|
||||
},
|
||||
title: "Quick Actions",
|
||||
} as const;
|
||||
12
src/i18n/modules/quick/index.ts
Normal file
12
src/i18n/modules/quick/index.ts
Normal file
@@ -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<typeof zhCN>;
|
||||
|
||||
export const quickMessages = {
|
||||
"zh-CN": zhCN,
|
||||
"en-US": enUS,
|
||||
"th-TH": thTH,
|
||||
} satisfies Record<SupportedLocale, QuickMessages>;
|
||||
7
src/i18n/modules/quick/th-TH.ts
Normal file
7
src/i18n/modules/quick/th-TH.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export default {
|
||||
form: {
|
||||
placeholder: "กรอกเนื้อหา",
|
||||
submit: "ส่ง",
|
||||
},
|
||||
title: "การดำเนินการด่วน",
|
||||
} as const;
|
||||
7
src/i18n/modules/quick/zh-CN.ts
Normal file
7
src/i18n/modules/quick/zh-CN.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export default {
|
||||
form: {
|
||||
placeholder: "请输入内容",
|
||||
submit: "提交",
|
||||
},
|
||||
title: "快捷操作",
|
||||
} as const;
|
||||
35
src/i18n/storage.ts
Normal file
35
src/i18n/storage.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { isSupportedLocale } from "./locales.ts";
|
||||
import type { SupportedLocale } from "./types.ts";
|
||||
|
||||
export const localeStorageKey = "nianxx.locale";
|
||||
|
||||
type LocaleStorageLike = Pick<Storage, "getItem" | "setItem">;
|
||||
|
||||
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.
|
||||
}
|
||||
}
|
||||
9
src/i18n/types.ts
Normal file
9
src/i18n/types.ts
Normal file
@@ -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<T> = {
|
||||
readonly [Key in keyof T]: T[Key] extends string ? string : LocaleMessageSchema<T[Key]>;
|
||||
};
|
||||
15
src/i18n/vant.ts
Normal file
15
src/i18n/vant.ts
Normal file
@@ -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<SupportedLocale, Record<string, unknown>>;
|
||||
|
||||
export function syncVantLocale(locale: SupportedLocale): void {
|
||||
Locale.use(locale, vantMessages[locale]);
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
44
yarn.lock
44
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"
|
||||
|
||||
Reference in New Issue
Block a user