双语添加
This commit is contained in:
parent
eef33f87c2
commit
794cf32033
261
README.md
Normal file
261
README.md
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
# Frontend
|
||||||
|
|
||||||
|
`Frontend/` 是 AppPlatform 的前端工作区,采用 `pnpm workspace + Turbo` 的 monorepo 组织方式。当前目录同时承载主业务应用、能力演示、文档站、Mock 服务以及一组共享包和工程脚本。
|
||||||
|
|
||||||
|
项目整体基于 Vue 3 技术栈,并延续 `Vue Vben Admin v5` 的分层方式做业务化扩展。
|
||||||
|
|
||||||
|
## 1. 技术栈概览
|
||||||
|
|
||||||
|
- Vue 3
|
||||||
|
- Vite
|
||||||
|
- TypeScript
|
||||||
|
- Pinia
|
||||||
|
- Vue Router
|
||||||
|
- Turbo
|
||||||
|
- pnpm workspace
|
||||||
|
- VitePress
|
||||||
|
- Playwright
|
||||||
|
- Nitro Mock
|
||||||
|
- Ant Design Vue / antdv-next
|
||||||
|
|
||||||
|
## 2. 工作区结构
|
||||||
|
|
||||||
|
```text
|
||||||
|
Frontend/
|
||||||
|
├── apps/ 主业务应用(数据平台)
|
||||||
|
├── playground/ 示例应用、能力演示、E2E 验证入口
|
||||||
|
├── docs/ VitePress 文档站
|
||||||
|
├── backend-mock/ Nitro Mock 服务
|
||||||
|
├── packages/ 业务与框架共享包
|
||||||
|
├── internal/ 内部工程配置与构建基础设施
|
||||||
|
├── scripts/ CLI 与部署脚本
|
||||||
|
├── turbo.json Turbo 任务编排
|
||||||
|
├── pnpm-workspace.yaml Workspace 包范围定义
|
||||||
|
└── package.json 根脚本与统一依赖入口
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 关键目录说明
|
||||||
|
|
||||||
|
### 3.1 apps
|
||||||
|
|
||||||
|
主业务前端应用,包名为 `@vben/web-antdv-next`。
|
||||||
|
|
||||||
|
- 应用标题默认配置为“数据平台”
|
||||||
|
- 开发端口来自 `apps/.env.development`,默认是 `5999`
|
||||||
|
- 开发态通过 Vite 代理将 `/console` 转发到 `http://127.0.0.1:2700`
|
||||||
|
- 入口在 `apps/src/main.ts`
|
||||||
|
- 启动装配逻辑在 `apps/src/bootstrap.ts`
|
||||||
|
|
||||||
|
`apps/src` 主要分层如下:
|
||||||
|
|
||||||
|
- `api/core`:较新的核心接口封装
|
||||||
|
- `api/legacy`:历史业务接口封装
|
||||||
|
- `router`:路由与权限守卫
|
||||||
|
- `store`:状态管理
|
||||||
|
- `layouts`:布局入口
|
||||||
|
- `views/*`:按业务域组织的页面模块
|
||||||
|
- `components`:应用级组件
|
||||||
|
- `adapter`:表单、组件等适配层
|
||||||
|
- `locales`:国际化资源
|
||||||
|
|
||||||
|
### 3.2 playground
|
||||||
|
|
||||||
|
演示与验证用应用,包名为 `@vben/playground`。
|
||||||
|
|
||||||
|
- 默认开发端口为 `5555`
|
||||||
|
- 开发态接口前缀为 `/api`
|
||||||
|
- 代理目标为 `http://localhost:5320/api`
|
||||||
|
- 默认开启 `VITE_NITRO_MOCK=true`
|
||||||
|
- 内置 Playwright E2E,配置位于 `playground/playwright.config.ts`
|
||||||
|
|
||||||
|
适合用于:
|
||||||
|
|
||||||
|
- 验证基础能力包是否可用
|
||||||
|
- 编写和运行 E2E 用例
|
||||||
|
- 调试通用组件、布局、示例页面
|
||||||
|
|
||||||
|
### 3.3 docs
|
||||||
|
|
||||||
|
文档站应用,包名为 `@vben/docs`,基于 VitePress。
|
||||||
|
|
||||||
|
- 配置入口在 `docs/.vitepress/config/index.mts`
|
||||||
|
- 同时提供中文与英文站点
|
||||||
|
- 首页内容位于 `docs/src/index.md` 和 `docs/src/en/index.md`
|
||||||
|
|
||||||
|
### 3.4 backend-mock
|
||||||
|
|
||||||
|
Mock 服务包名为 `@vben/backend-mock`,基于 Nitro。
|
||||||
|
|
||||||
|
- 默认端口来自 `backend-mock/.env`,当前为 `5320`
|
||||||
|
- 提供前端开发期所需的接口、鉴权、上传等模拟能力
|
||||||
|
- 按现有说明,通常由开发流程集成启用,不一定需要单独常驻手动启动
|
||||||
|
|
||||||
|
### 3.5 packages
|
||||||
|
|
||||||
|
共享包分为几层:
|
||||||
|
|
||||||
|
- `packages/@core/base/*`
|
||||||
|
- 底层基础能力,如 `design`、`icons`、`shared`、`typings`
|
||||||
|
- 约束:`base` 层“请勿引入 workspace 依赖”
|
||||||
|
- `packages/@core/ui-kit/*`
|
||||||
|
- 通用 UI 原子/组合能力,如 `form-ui`、`layout-ui`、`menu-ui`、`popup-ui`、`tabs-ui`、`shadcn-ui`
|
||||||
|
- `packages/@core/composables`
|
||||||
|
- 通用组合式能力
|
||||||
|
- `packages/@core/preferences`
|
||||||
|
- 偏好设置相关底层能力
|
||||||
|
- `packages/effects/*`
|
||||||
|
- 偏应用层的组合能力,如 `access`、`common-ui`、`hooks`、`layouts`、`plugins`、`request`
|
||||||
|
- `packages/constants|icons|locales|preferences|stores|styles|types|utils`
|
||||||
|
- 面向应用的共享常量、样式、类型、工具与状态能力
|
||||||
|
|
||||||
|
### 3.6 internal
|
||||||
|
|
||||||
|
内部工程基础设施,不直接承载业务页面:
|
||||||
|
|
||||||
|
- `internal/lint-configs/*`:ESLint / Stylelint 共享配置
|
||||||
|
- `internal/tsconfig`:TS 配置基线
|
||||||
|
- `internal/vite-config`:Vite 配置封装
|
||||||
|
- `internal/tailwind-config`:Tailwind 配置
|
||||||
|
- `internal/node-utils`:Node 侧工具能力
|
||||||
|
|
||||||
|
### 3.7 scripts
|
||||||
|
|
||||||
|
工程脚本目录:
|
||||||
|
|
||||||
|
- `scripts/turbo-run`
|
||||||
|
- 交互式运行 monorepo 中具备相同脚本的包
|
||||||
|
- `scripts/vsh`
|
||||||
|
- 工程巡检工具,包含依赖检查、循环依赖扫描、发布检查、lint 封装等能力
|
||||||
|
- `scripts/deploy`
|
||||||
|
- Docker / Nginx 部署相关脚本
|
||||||
|
|
||||||
|
## 4. 安装与启动
|
||||||
|
|
||||||
|
### 4.1 环境要求
|
||||||
|
|
||||||
|
- Node.js: `^20.19.0 || ^22.18.0 || ^24.0.0`
|
||||||
|
- pnpm: `>=10.0.0`
|
||||||
|
|
||||||
|
推荐先启用 Corepack:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
corepack enable
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
项目根目录限制使用 `pnpm`,`npm/yarn` 不作为默认包管理器。
|
||||||
|
|
||||||
|
### 4.2 常用开发命令
|
||||||
|
|
||||||
|
在 `Frontend/` 目录执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 交互式选择带 dev 脚本的包启动
|
||||||
|
pnpm dev
|
||||||
|
|
||||||
|
# 启动主业务应用
|
||||||
|
pnpm dev:antdv-next
|
||||||
|
|
||||||
|
# 启动 playground
|
||||||
|
pnpm dev:play
|
||||||
|
|
||||||
|
# 启动文档站
|
||||||
|
pnpm dev:docs
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 构建命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 构建整个 workspace
|
||||||
|
pnpm build
|
||||||
|
|
||||||
|
# 构建主应用
|
||||||
|
pnpm build:antdv-next
|
||||||
|
|
||||||
|
# 构建 playground
|
||||||
|
pnpm build:play
|
||||||
|
|
||||||
|
# 构建文档站
|
||||||
|
pnpm build:docs
|
||||||
|
|
||||||
|
# 本地构建 Docker 镜像
|
||||||
|
pnpm build:docker
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 质量与测试命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# lint / format
|
||||||
|
pnpm lint
|
||||||
|
pnpm format
|
||||||
|
|
||||||
|
# 类型检查、依赖检查、循环依赖检查、拼写检查
|
||||||
|
pnpm check
|
||||||
|
|
||||||
|
# 单元测试
|
||||||
|
pnpm test:unit
|
||||||
|
|
||||||
|
# E2E 测试
|
||||||
|
pnpm test:e2e
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 环境变量与运行时约定
|
||||||
|
|
||||||
|
### 5.1 apps
|
||||||
|
|
||||||
|
`apps/.env` 与 `apps/.env.*` 中常见变量:
|
||||||
|
|
||||||
|
- `VITE_APP_TITLE`:应用标题
|
||||||
|
- `VITE_APP_NAMESPACE`:本地缓存、store 等命名空间前缀
|
||||||
|
- `VITE_APP_STORE_SECURE_KEY`:持久化加密密钥
|
||||||
|
- `VITE_PORT`:开发端口
|
||||||
|
- `VITE_GLOB_API_URL`:接口基地址
|
||||||
|
- `VITE_NITRO_MOCK`:是否启用 Nitro Mock
|
||||||
|
|
||||||
|
当前开发环境关键值:
|
||||||
|
|
||||||
|
- 端口:`5999`
|
||||||
|
- 接口:`http://127.0.0.1:2700/console`
|
||||||
|
- Mock:`false`
|
||||||
|
|
||||||
|
### 5.2 playground
|
||||||
|
|
||||||
|
当前开发环境关键值:
|
||||||
|
|
||||||
|
- 端口:`5555`
|
||||||
|
- 接口:`/api`
|
||||||
|
- Mock:`true`
|
||||||
|
|
||||||
|
### 5.3 backend-mock
|
||||||
|
|
||||||
|
- `PORT=5320`
|
||||||
|
- `ACCESS_TOKEN_SECRET`
|
||||||
|
- `REFRESH_TOKEN_SECRET`
|
||||||
|
|
||||||
|
## 6. 全局工程约定
|
||||||
|
|
||||||
|
- 根命令统一从 `Frontend/package.json` 发起,避免在子包内各自维护重复脚本
|
||||||
|
- `pnpm dev` 与 `pnpm preview` 通过 `turbo-run` 做交互式过滤,不是“启动所有应用”
|
||||||
|
- Turbo 任务编排定义在 `turbo.json`
|
||||||
|
- workspace 范围定义在 `pnpm-workspace.yaml`
|
||||||
|
- `.npmrc` 已配置镜像源与 peer 依赖策略,默认使用 `https://registry.npmmirror.com`
|
||||||
|
- `@vben-core/base` 层保持最底层,不应反向依赖其它 workspace 包
|
||||||
|
- 主应用通过偏好设置系统按 `namespace + version + env` 隔离本地缓存与持久化数据
|
||||||
|
|
||||||
|
## 7. 建议的阅读顺序
|
||||||
|
|
||||||
|
第一次接手该目录,建议按下面顺序阅读:
|
||||||
|
|
||||||
|
1. `package.json`
|
||||||
|
2. `pnpm-workspace.yaml`
|
||||||
|
3. `turbo.json`
|
||||||
|
4. `apps/package.json`
|
||||||
|
5. `apps/src/main.ts`
|
||||||
|
6. `apps/src/bootstrap.ts`
|
||||||
|
7. `playground/package.json`
|
||||||
|
8. `backend-mock/README.md`
|
||||||
|
|
||||||
|
## 8. 补充说明
|
||||||
|
|
||||||
|
- 现有 `README.zh-CN.md` 仅保留了最基础的安装与运行说明
|
||||||
|
- 本文件用于补充 `Frontend/` 的全局结构、运行方式与分层约定,便于后续开发、交接与排障
|
||||||
@ -15,6 +15,7 @@ import SysOriginSelect from '#/components/sys-origin-select.vue';
|
|||||||
import { initComponentAdapter } from './adapter/component';
|
import { initComponentAdapter } from './adapter/component';
|
||||||
import { initSetupVbenForm } from './adapter/form';
|
import { initSetupVbenForm } from './adapter/form';
|
||||||
import App from './app.vue';
|
import App from './app.vue';
|
||||||
|
import { setupRuntimeLocaleSync } from './locales/runtime';
|
||||||
import { router } from './router';
|
import { router } from './router';
|
||||||
import './styles/table-filters.css';
|
import './styles/table-filters.css';
|
||||||
|
|
||||||
@ -68,12 +69,13 @@ async function bootstrap(namespace: string) {
|
|||||||
if (preferences.app.dynamicTitle) {
|
if (preferences.app.dynamicTitle) {
|
||||||
const routeTitle = router.currentRoute.value.meta?.title;
|
const routeTitle = router.currentRoute.value.meta?.title;
|
||||||
const pageTitle =
|
const pageTitle =
|
||||||
(routeTitle ? `${$t(routeTitle)} - ` : '') + preferences.app.name;
|
(routeTitle ? `${$t(routeTitle)} - ` : '') + $t(preferences.app.name);
|
||||||
useTitle(pageTitle);
|
useTitle(pageTitle);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.mount('#app');
|
app.mount('#app');
|
||||||
|
setupRuntimeLocaleSync();
|
||||||
}
|
}
|
||||||
|
|
||||||
export { bootstrap };
|
export { bootstrap };
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { preferences } from '@vben/preferences';
|
|||||||
|
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
const appName = computed(() => preferences.app.name);
|
const appName = computed(() => $t(preferences.app.name));
|
||||||
const logo = computed(() => preferences.logo.source);
|
const logo = computed(() => preferences.logo.source);
|
||||||
const logoDark = computed(() => preferences.logo.sourceDark);
|
const logoDark = computed(() => preferences.logo.sourceDark);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import dayjs from 'dayjs';
|
|||||||
const antdLocale = ref<Locale>(antdDefaultLocale);
|
const antdLocale = ref<Locale>(antdDefaultLocale);
|
||||||
|
|
||||||
const modules = import.meta.glob('./langs/**/*.json');
|
const modules = import.meta.glob('./langs/**/*.json');
|
||||||
|
const runtimeModules = import.meta.glob('./langs/*/runtime.json');
|
||||||
|
|
||||||
const localesMap = loadLocalesMapFromDir(
|
const localesMap = loadLocalesMapFromDir(
|
||||||
/\.\/langs\/([^/]+)\/(.*)\.json$/,
|
/\.\/langs\/([^/]+)\/(.*)\.json$/,
|
||||||
@ -31,11 +32,26 @@ const localesMap = loadLocalesMapFromDir(
|
|||||||
* @param lang
|
* @param lang
|
||||||
*/
|
*/
|
||||||
async function loadMessages(lang: SupportedLanguagesType) {
|
async function loadMessages(lang: SupportedLanguagesType) {
|
||||||
const [appLocaleMessages] = await Promise.all([
|
const [appLocaleMessages, runtimeMessages] = await Promise.all([
|
||||||
localesMap[lang]?.(),
|
localesMap[lang]?.(),
|
||||||
|
loadRuntimeMessages(lang),
|
||||||
loadThirdPartyMessage(lang),
|
loadThirdPartyMessage(lang),
|
||||||
]);
|
]);
|
||||||
return appLocaleMessages?.default;
|
const appMessages = appLocaleMessages?.default ?? {};
|
||||||
|
const { runtime: _runtime, ...rest } = appMessages;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...(runtimeMessages ?? {}),
|
||||||
|
...rest,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadRuntimeMessages(lang: SupportedLanguagesType) {
|
||||||
|
const runtimeLoader = runtimeModules[`./langs/${lang}/runtime.json`];
|
||||||
|
const runtimeMessages = runtimeLoader
|
||||||
|
? ((await runtimeLoader()) as { default?: Record<string, string> })
|
||||||
|
: undefined;
|
||||||
|
return runtimeMessages?.default;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
2918
apps/src/locales/langs/en-US/runtime.json
Normal file
2918
apps/src/locales/langs/en-US/runtime.json
Normal file
File diff suppressed because it is too large
Load Diff
2915
apps/src/locales/langs/zh-CN/runtime.json
Normal file
2915
apps/src/locales/langs/zh-CN/runtime.json
Normal file
File diff suppressed because it is too large
Load Diff
267
apps/src/locales/runtime.ts
Normal file
267
apps/src/locales/runtime.ts
Normal file
@ -0,0 +1,267 @@
|
|||||||
|
import { nextTick, watch } from 'vue';
|
||||||
|
|
||||||
|
import { i18n } from '@vben/locales';
|
||||||
|
|
||||||
|
const CHINESE_RE = /[\u3400-\u9fff]/;
|
||||||
|
const SKIP_ATTRIBUTE_SELECTOR =
|
||||||
|
'[data-no-runtime-locale], .ace_editor, .monaco-editor, noscript, script, style';
|
||||||
|
const SKIP_TEXT_SELECTOR = `${SKIP_ATTRIBUTE_SELECTOR}, [contenteditable="true"], code, pre`;
|
||||||
|
const TRANSLATABLE_ATTRIBUTES = [
|
||||||
|
'alt',
|
||||||
|
'aria-label',
|
||||||
|
'placeholder',
|
||||||
|
'title',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const elementOriginals = new WeakMap<Element, Map<string, string>>();
|
||||||
|
const textOriginals = new WeakMap<Text, string>();
|
||||||
|
|
||||||
|
let initialized = false;
|
||||||
|
let observer: MutationObserver | undefined;
|
||||||
|
let runtimeMatcherCache:
|
||||||
|
| {
|
||||||
|
dict: Record<string, string>;
|
||||||
|
locale: string;
|
||||||
|
pattern?: RegExp;
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
function escapeRegExp(text: string) {
|
||||||
|
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRuntimeDictionary(locale: string) {
|
||||||
|
const messages = i18n.global.getLocaleMessage(locale) as Record<string, unknown>;
|
||||||
|
const dictionary: Record<string, string> = {};
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(messages)) {
|
||||||
|
if (typeof value === 'string' && CHINESE_RE.test(key)) {
|
||||||
|
dictionary[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dictionary;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRuntimeMatcher(locale: string) {
|
||||||
|
if (runtimeMatcherCache?.locale === locale) {
|
||||||
|
return runtimeMatcherCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dict = locale === 'en-US' ? getRuntimeDictionary(locale) : {};
|
||||||
|
const keys = Object.keys(dict).sort((left, right) => right.length - left.length);
|
||||||
|
|
||||||
|
runtimeMatcherCache = {
|
||||||
|
dict,
|
||||||
|
locale,
|
||||||
|
pattern:
|
||||||
|
keys.length > 0
|
||||||
|
? new RegExp(keys.map((key) => escapeRegExp(key)).join('|'), 'g')
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
return runtimeMatcherCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getElementOriginalMap(element: Element) {
|
||||||
|
let originalMap = elementOriginals.get(element);
|
||||||
|
|
||||||
|
if (!originalMap) {
|
||||||
|
originalMap = new Map<string, string>();
|
||||||
|
elementOriginals.set(element, originalMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
return originalMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldSkipElement(element: Element) {
|
||||||
|
return element.matches(SKIP_ATTRIBUTE_SELECTOR) || !!element.closest(SKIP_ATTRIBUTE_SELECTOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldSkipTextNode(node: Text) {
|
||||||
|
return !!node.parentElement?.closest(SKIP_TEXT_SELECTOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentLocale() {
|
||||||
|
return String(i18n.global.locale.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function translateText(text: string) {
|
||||||
|
if (!text || !CHINESE_RE.test(text)) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
const locale = getCurrentLocale();
|
||||||
|
if (locale !== 'en-US') {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { dict, pattern } = getRuntimeMatcher(locale);
|
||||||
|
if (!pattern) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
return text.replace(pattern, (matched) => dict[matched] ?? matched);
|
||||||
|
}
|
||||||
|
|
||||||
|
function translateElementAttributes(element: Element) {
|
||||||
|
if (shouldSkipElement(element)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const locale = getCurrentLocale();
|
||||||
|
const originals = getElementOriginalMap(element);
|
||||||
|
|
||||||
|
for (const attribute of TRANSLATABLE_ATTRIBUTES) {
|
||||||
|
const rawValue = element.getAttribute(attribute);
|
||||||
|
if (rawValue == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (locale === 'en-US') {
|
||||||
|
const originalValue = CHINESE_RE.test(rawValue)
|
||||||
|
? rawValue
|
||||||
|
: (originals.get(attribute) ?? rawValue);
|
||||||
|
if (!CHINESE_RE.test(originalValue)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const translatedValue = translateText(originalValue);
|
||||||
|
if (translatedValue !== originalValue) {
|
||||||
|
originals.set(attribute, originalValue);
|
||||||
|
if (rawValue !== translatedValue) {
|
||||||
|
element.setAttribute(attribute, translatedValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalValue = originals.get(attribute);
|
||||||
|
if (originalValue !== undefined && rawValue !== originalValue) {
|
||||||
|
element.setAttribute(attribute, originalValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function translateTextNode(node: Text) {
|
||||||
|
if (shouldSkipTextNode(node)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawValue = node.nodeValue ?? '';
|
||||||
|
const locale = getCurrentLocale();
|
||||||
|
|
||||||
|
if (locale === 'en-US') {
|
||||||
|
const originalValue = CHINESE_RE.test(rawValue)
|
||||||
|
? rawValue
|
||||||
|
: (textOriginals.get(node) ?? rawValue);
|
||||||
|
if (!CHINESE_RE.test(originalValue)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const translatedValue = translateText(originalValue);
|
||||||
|
if (translatedValue !== originalValue) {
|
||||||
|
textOriginals.set(node, originalValue);
|
||||||
|
if (rawValue !== translatedValue) {
|
||||||
|
node.nodeValue = translatedValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalValue = textOriginals.get(node);
|
||||||
|
if (originalValue !== undefined && rawValue !== originalValue) {
|
||||||
|
node.nodeValue = originalValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function processNode(root: Node) {
|
||||||
|
const stack: Node[] = [root];
|
||||||
|
|
||||||
|
while (stack.length > 0) {
|
||||||
|
const node = stack.pop();
|
||||||
|
if (!node) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.nodeType === Node.TEXT_NODE) {
|
||||||
|
translateTextNode(node as Text);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.nodeType !== Node.ELEMENT_NODE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const element = node as Element;
|
||||||
|
translateElementAttributes(element);
|
||||||
|
|
||||||
|
for (let index = element.childNodes.length - 1; index >= 0; index -= 1) {
|
||||||
|
const childNode = element.childNodes[index];
|
||||||
|
if (childNode) {
|
||||||
|
stack.push(childNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshRuntimeLocale() {
|
||||||
|
if (typeof document === 'undefined' || !document.body) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
processNode(document.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
function startObserver() {
|
||||||
|
if (
|
||||||
|
observer ||
|
||||||
|
typeof document === 'undefined' ||
|
||||||
|
typeof MutationObserver === 'undefined' ||
|
||||||
|
!document.body
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
observer = new MutationObserver((mutations) => {
|
||||||
|
for (const mutation of mutations) {
|
||||||
|
if (mutation.type === 'attributes' || mutation.type === 'characterData') {
|
||||||
|
processNode(mutation.target);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
mutation.addedNodes.forEach((node) => processNode(node));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(document.body, {
|
||||||
|
attributeFilter: [...TRANSLATABLE_ATTRIBUTES],
|
||||||
|
attributes: true,
|
||||||
|
characterData: true,
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupRuntimeLocaleSync() {
|
||||||
|
if (initialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
initialized = true;
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => getCurrentLocale(),
|
||||||
|
async () => {
|
||||||
|
runtimeMatcherCache = undefined;
|
||||||
|
await nextTick();
|
||||||
|
refreshRuntimeLocale();
|
||||||
|
startObserver();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
flush: 'post',
|
||||||
|
immediate: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { setupRuntimeLocaleSync, translateText as translateLocaleText };
|
||||||
@ -4,11 +4,13 @@ import { useRoute } from 'vue-router';
|
|||||||
|
|
||||||
import { Page } from '@vben/common-ui';
|
import { Page } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
defineOptions({ name: 'LegacyExternalIframe' });
|
defineOptions({ name: 'LegacyExternalIframe' });
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
const title = computed(() => String(route.meta.title || '外部页面'));
|
const title = computed(() => String($t(String(route.meta.title || '外部页面'))));
|
||||||
const iframeSrc = computed(() => String(route.meta.iframeSrc || ''));
|
const iframeSrc = computed(() => String(route.meta.iframeSrc || ''));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -240,16 +240,16 @@ const getForceMount = computed(() => {
|
|||||||
/>
|
/>
|
||||||
<SheetTitle v-if="title" class="text-left">
|
<SheetTitle v-if="title" class="text-left">
|
||||||
<slot name="title">
|
<slot name="title">
|
||||||
{{ title }}
|
{{ $t(title) }}
|
||||||
|
|
||||||
<VbenHelpTooltip v-if="titleTooltip" trigger-class="pb-1">
|
<VbenHelpTooltip v-if="titleTooltip" trigger-class="pb-1">
|
||||||
{{ titleTooltip }}
|
{{ $t(titleTooltip) }}
|
||||||
</VbenHelpTooltip>
|
</VbenHelpTooltip>
|
||||||
</slot>
|
</slot>
|
||||||
</SheetTitle>
|
</SheetTitle>
|
||||||
<SheetDescription v-if="description" class="mt-1 text-xs">
|
<SheetDescription v-if="description" class="mt-1 text-xs">
|
||||||
<slot name="description">
|
<slot name="description">
|
||||||
{{ description }}
|
{{ $t(description) }}
|
||||||
</slot>
|
</slot>
|
||||||
</SheetDescription>
|
</SheetDescription>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -287,18 +287,18 @@ function handleClosed() {
|
|||||||
>
|
>
|
||||||
<DialogTitle v-if="title" class="text-left">
|
<DialogTitle v-if="title" class="text-left">
|
||||||
<slot name="title">
|
<slot name="title">
|
||||||
{{ title }}
|
{{ $t(title) }}
|
||||||
|
|
||||||
<slot v-if="titleTooltip" name="titleTooltip">
|
<slot v-if="titleTooltip" name="titleTooltip">
|
||||||
<VbenHelpTooltip trigger-class="pb-1">
|
<VbenHelpTooltip trigger-class="pb-1">
|
||||||
{{ titleTooltip }}
|
{{ $t(titleTooltip) }}
|
||||||
</VbenHelpTooltip>
|
</VbenHelpTooltip>
|
||||||
</slot>
|
</slot>
|
||||||
</slot>
|
</slot>
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription v-if="description">
|
<DialogDescription v-if="description">
|
||||||
<slot name="description">
|
<slot name="description">
|
||||||
{{ description }}
|
{{ $t(description) }}
|
||||||
</slot>
|
</slot>
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
<VisuallyHidden v-if="!title || !description">
|
<VisuallyHidden v-if="!title || !description">
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import type { PageProps } from './types';
|
|||||||
|
|
||||||
import { computed, nextTick, onMounted, ref, useTemplateRef } from 'vue';
|
import { computed, nextTick, onMounted, ref, useTemplateRef } from 'vue';
|
||||||
|
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
import { CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT } from '@vben-core/shared/constants';
|
import { CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT } from '@vben-core/shared/constants';
|
||||||
import { cn } from '@vben-core/shared/utils';
|
import { cn } from '@vben-core/shared/utils';
|
||||||
|
|
||||||
@ -70,13 +71,13 @@ onMounted(() => {
|
|||||||
<div class="flex-auto">
|
<div class="flex-auto">
|
||||||
<slot name="title">
|
<slot name="title">
|
||||||
<div v-if="title" class="mb-2 flex text-lg font-semibold">
|
<div v-if="title" class="mb-2 flex text-lg font-semibold">
|
||||||
{{ title }}
|
{{ $t(title) }}
|
||||||
</div>
|
</div>
|
||||||
</slot>
|
</slot>
|
||||||
|
|
||||||
<slot name="description">
|
<slot name="description">
|
||||||
<p v-if="description" class="text-muted-foreground">
|
<p v-if="description" class="text-muted-foreground">
|
||||||
{{ description }}
|
{{ $t(description) }}
|
||||||
</p>
|
</p>
|
||||||
</slot>
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -36,7 +36,7 @@ const IconOffline = defineAsyncComponent(
|
|||||||
|
|
||||||
const titleText = computed(() => {
|
const titleText = computed(() => {
|
||||||
if (props.title) {
|
if (props.title) {
|
||||||
return props.title;
|
return $t(props.title);
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (props.status) {
|
switch (props.status) {
|
||||||
@ -63,7 +63,7 @@ const titleText = computed(() => {
|
|||||||
|
|
||||||
const descText = computed(() => {
|
const descText = computed(() => {
|
||||||
if (props.description) {
|
if (props.description) {
|
||||||
return props.description;
|
return $t(props.description);
|
||||||
}
|
}
|
||||||
switch (props.status) {
|
switch (props.status) {
|
||||||
case '403': {
|
case '403': {
|
||||||
|
|||||||
@ -70,6 +70,10 @@ const headerTheme = computed(() => {
|
|||||||
return dark ? 'dark' : 'light';
|
return dark ? 'dark' : 'light';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const appName = computed(() => {
|
||||||
|
return $t(preferences.app.name);
|
||||||
|
});
|
||||||
|
|
||||||
const logoClass = computed(() => {
|
const logoClass = computed(() => {
|
||||||
const { collapsedShowTitle } = preferences.sidebar;
|
const { collapsedShowTitle } = preferences.sidebar;
|
||||||
const classes: string[] = [];
|
const classes: string[] = [];
|
||||||
@ -281,7 +285,7 @@ const headerSlots = computed(() => {
|
|||||||
:collapsed="logoCollapsed"
|
:collapsed="logoCollapsed"
|
||||||
:src="preferences.logo.source"
|
:src="preferences.logo.source"
|
||||||
:src-dark="preferences.logo.sourceDark"
|
:src-dark="preferences.logo.sourceDark"
|
||||||
:text="preferences.app.name"
|
:text="appName"
|
||||||
:theme="showHeaderNav ? headerTheme : theme"
|
:theme="showHeaderNav ? headerTheme : theme"
|
||||||
@click="clickLogo"
|
@click="clickLogo"
|
||||||
>
|
>
|
||||||
@ -372,7 +376,7 @@ const headerSlots = computed(() => {
|
|||||||
<VbenLogo
|
<VbenLogo
|
||||||
v-if="preferences.logo.enable"
|
v-if="preferences.logo.enable"
|
||||||
:fit="preferences.logo.fit"
|
:fit="preferences.logo.fit"
|
||||||
:text="preferences.app.name"
|
:text="appName"
|
||||||
:theme="sidebarThemeSub"
|
:theme="sidebarThemeSub"
|
||||||
>
|
>
|
||||||
<template v-if="$slots['logo-text']" #text>
|
<template v-if="$slots['logo-text']" #text>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user