refactor(i18n): 重构国际化模块结构
All checks were successful
CI/CD Pipeline / playwright (push) Successful in 4m14s
CI/CD Pipeline / build-and-deploy (push) Successful in 4m39s

This commit is contained in:
严浩
2025-10-29 13:09:17 +08:00
parent 94d09d0bdd
commit 7dd7ce73bc
10 changed files with 44 additions and 45 deletions

View File

@@ -0,0 +1,28 @@
/* https://github.com/intlify/bundle-tools/tree/main/packages/unplugin-vue-i18n#static-bundle-importing
* All i18n resources specified in the plugin `include` option can be loaded
* at once using the import syntax
*/
import messages from '@intlify/unplugin-vue-i18n/messages';
import { createI18n } from 'vue-i18n';
const locale = useLocalStorage<string>('app-locale', navigator.language);
// https://vue-i18n.intlify.dev/guide/essentials/started.html#registering-the-i18n-plugin
export const i18nInstance = createI18n({
legacy: false, // you must set `false`, to use Composition API
locale: locale.value,
fallbackRoot: false,
// flatJson: true,
missing: (locale, key /* , instance, type */) => {
consola.warn(`缺少国际化内容: locale='${locale}', key='${key}'`);
return `[${key}]`;
},
missingWarn: !true,
fallbackWarn: !true,
messages,
});
watchEffect(() => {
locale.value = i18nInstance.global.locale.value;
});

View File

@@ -0,0 +1,66 @@
# route-messages
此目录存放专门用于**路由名称**的国际化i18n消息。这些消息通过一套自定义的编译时安全机制为应用的导航菜单提供标题。
## 解决什么问题?
`unplugin-vue-router``definePage()` 宏在编译时执行,无法访问 Vue `<script setup>` 作用域中的运行时变量(如 `t()` 函数)。这使得在路由元信息(`meta`)中直接定义多语言标题变得不可能。
## 解决方案:自定义的编译时安全机制
我们采用一种**约定优于配置**的策略,并利用 TypeScript 进行编译时检查,以确保所有菜单标题都已定义。
**工作流程:**
1. **`RouteNamedMap` 的生成**`unplugin-vue-router` 会扫描你的页面组件,并自动生成一个名为 `RouteNamedMap` 的 TypeScript 类型,该类型包含了项目中所有具名路由的 `name`
2. **自定义全局类型**:我们定义了一个全局类型 `PageTitleLocalizations`
```typescript
declare global {
type PageTitleLocalizations = Record<keyof RouteNamedMap, string>;
}
```
这个自定义类型创建了一个**契约**:任何满足此类型的对象,都**必须**为 `RouteNamedMap` 中的每一个路由名称提供一个字符串类型的键值对。
3. **编译时检查**:此目录下的每个语言环境文件(如 `en-US.ts`)都必须使用 `satisfies PageTitleLocalizations` 来进行类型断言。
```typescript
// ./en-US.ts
export default { ... } satisfies PageTitleLocalizations;
```
这个操作会触发 TypeScript 在**编译时**进行检查。如果你新增了一个具名路由但忘记在此处添加翻译,**TypeScript 编译将会失败**,并明确提示你缺少的键。
4. **菜单生成**:在运行时,`@/composables/useMetaLayoutsMenuOptions.tsx` 会获取当前路由的 `name`,并使用它作为键(`t(routeName)`)来查找并显示菜单标题。由于有编译时检查,我们可以确信翻译始终存在。
### 带来的好处
- **杜绝遗漏**:从根本上解决了菜单项标题缺失或显示为原始键的问题。
- **关注点分离**:路由定义只关心路由结构,显示文本则集中在此处管理。
### 开发者实践指南
1. **理解路由命名规则**
`unplugin-vue-router` 会根据页面组件的**文件路径**自动生成 `PascalCase` 格式的路由 `name`。
- **示例**
- 文件路径:`src/pages/demos/api-demo.page.vue`
- 自动生成的路由 `name``DemosApiDemo`
2. **添加对应的翻译**
使用上一步中自动生成的 `name` 作为键,在此目录的每个语言文件中添加翻译。
```ts
// ./zh-CN.ts
export default {
// ... 其他翻译
DemosApiDemo: 'API 演示',
} satisfies PageTitleLocalizations;
// ./en-US.ts
export default {
// ... 其他翻译
DemosApiDemo: 'API Demo',
} satisfies PageTitleLocalizations;
```

View File

@@ -0,0 +1,12 @@
export default {
Root: 'Index',
$Path: '$Path',
Demos: 'Demos',
DemosApiDemo: 'API Demo',
DemosCounterDemo: 'Counter Demo',
DemosI18nDemo: 'i18n Demo',
DemosPrimevueDemo: 'PrimeVue Demo',
DemosWebsocketDemo: 'WebSocket Demo',
Home: 'Home',
Login: 'Login',
} satisfies PageTitleLocalizations;

View File

@@ -0,0 +1,17 @@
import type { I18nOptions } from 'vue-i18n';
const modules = import.meta.glob(['./*.ts', '!./route-messages-auto-imports'], {
eager: true,
import: 'default',
});
type MessageType = Record<string, string>;
export const i18nRouteMessages: I18nOptions['messages'] = Object.entries(modules).reduce(
(messages, [path, mod]) => {
const locale = path.replace(/(\.\/|\.ts)/g, '');
messages[locale] = mod as MessageType;
return messages;
},
{} as Record<string, MessageType>,
);

View File

@@ -0,0 +1,12 @@
export default {
Root: '根 (Gēn)',
$Path: '$Path',
Demos: '示例演示',
DemosApiDemo: 'API 调用示例',
DemosCounterDemo: '点击计数器',
DemosI18nDemo: '国际化示例',
DemosPrimevueDemo: 'PrimeVue 组件示例',
DemosWebsocketDemo: 'WebSocket 示例',
Home: '首页',
Login: '登录',
} satisfies PageTitleLocalizations;