This is an automated email from the ASF dual-hosted git repository. young pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/apisix-dashboard.git
The following commit(s) were added to refs/heads/master by this push: new 8679cc19f feat: i18n progress (#3094) 8679cc19f is described below commit 8679cc19f57e1b7683359924be100f15d5af4c18 Author: YYYoung <isk...@outlook.com> AuthorDate: Fri May 30 15:46:33 2025 +0800 feat: i18n progress (#3094) --- src/components/Header/LanguageMenu.tsx | 30 +++++++++- src/types/vite-env.d.ts | 5 ++ tsconfig.node.json | 2 +- vite-plugin-i18n-progress.ts | 100 +++++++++++++++++++++++++++++++++ vite.config.ts | 6 ++ 5 files changed, 140 insertions(+), 3 deletions(-) diff --git a/src/components/Header/LanguageMenu.tsx b/src/components/Header/LanguageMenu.tsx index 4347c9c07..107f05904 100644 --- a/src/components/Header/LanguageMenu.tsx +++ b/src/components/Header/LanguageMenu.tsx @@ -14,8 +14,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ActionIcon,Menu } from '@mantine/core'; +import { ActionIcon, Anchor, Menu } from '@mantine/core'; import { useTranslation } from 'react-i18next'; +import i18nProgress from 'virtual:i18n-progress'; import type { Resources } from '@/config/i18n'; import IconLanguage from '~icons/material-symbols/language-chinese-array'; @@ -25,6 +26,22 @@ const LangMap: Record<keyof Resources, string> = { zh: '中文', }; +const TranslationProgress = ({ lang }: { lang: string }) => { + const percent = i18nProgress[lang].percent; + if (typeof percent === 'number' && percent < 100) { + return ( + <span + style={{ + color: 'var(--mantine-color-gray-6)', + }} + > + ({percent}%) + </span> + ); + } + return null; +}; + export const LanguageMenu = () => { const { i18n, t } = useTranslation(); return ( @@ -43,10 +60,19 @@ export const LanguageMenu = () => { }} > {LangMap[lang as keyof Resources]} + <TranslationProgress lang={lang} /> </Menu.Item> ))} <Menu.Divider /> - <Menu.Label>{t('help-us-translate')}</Menu.Label> + <Menu.Label> + <Anchor + href="https://github.com/apache/apisix-dashboard/issues/1407" + target="_blank" + size="xs" + > + {t('help-us-translate')} + </Anchor> + </Menu.Label> </Menu.Dropdown> </Menu> ); diff --git a/src/types/vite-env.d.ts b/src/types/vite-env.d.ts index 70155fb44..462de486c 100644 --- a/src/types/vite-env.d.ts +++ b/src/types/vite-env.d.ts @@ -17,6 +17,11 @@ /// <reference types="vite/client" /> /// <reference types="unplugin-info/client" /> +declare module 'virtual:i18n-progress' { + const progress: import('./../../vite-plugin-i18n-progress').LangProgress<string>; + export default progress; +} + type FilterKeys<T, R extends string> = { [K in keyof T as K extends `${string}${R}` ? K : never]: T[K]; }; diff --git a/tsconfig.node.json b/tsconfig.node.json index dd48d780c..034749495 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -23,5 +23,5 @@ "node" ], }, - "include": ["vite.config.ts"] + "include": ["vite.config.ts", "vite-plugin-i18n-progress.ts"] } diff --git a/vite-plugin-i18n-progress.ts b/vite-plugin-i18n-progress.ts new file mode 100644 index 000000000..ffbdbbec5 --- /dev/null +++ b/vite-plugin-i18n-progress.ts @@ -0,0 +1,100 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { readdir, readFile } from 'fs/promises'; +import type { Plugin } from 'vite'; + +type Options<T extends string> = { + langs: T[]; + baseLang: T; + getTranslationDir: (lang: T) => Promise<string> | string; +}; + +export type LangProgress<T extends string> = { + [key in T]: { + count: number; + percent: number; + }; +}; + +const countKeys = (obj: any) => { + let count = 0; + for (const val of Object.values(obj)) { + if (typeof val === 'object' && val !== null) { + count += countKeys(val); + } else { + count++; + } + } + return count; +}; + +const getTranslationFiles = async <T extends string>( + params: Pick<Options<T>, 'getTranslationDir'> & { lang: T } +) => { + const { getTranslationDir, lang } = params; + const dir = await getTranslationDir(lang); + const files = await readdir(dir); + return files.filter((f) => f.endsWith('.json')).map((f) => `${dir}/${f}`); +}; + +const genI18nProgress = async <T extends string>(options: Options<T>) => { + const { langs, baseLang, getTranslationDir } = options; + const langCountsMap = new Map<T, number>(langs.map((lang) => [lang, 0])); + + await Promise.all( + langs.map(async (lang) => { + const files = await getTranslationFiles({ getTranslationDir, lang }); + const counts = await Promise.all( + files.map(async (file) => { + const content = await readFile(file, 'utf-8'); + return countKeys(JSON.parse(content)); + }) + ); + const count = counts.reduce((a, b) => a + b, 0); + langCountsMap.set(lang, count); + }) + ); + + const baseLangCount = langCountsMap.get(baseLang)!; + const res = {} as LangProgress<T>; + for (const [lang, count] of langCountsMap.entries()) { + res[lang] = { + count, + percent: Math.round((count / baseLangCount) * 100), + }; + } + return res; +}; + +const i18nProgress = <T extends string>(options: Options<T>): Plugin => { + const name = 'i18n-progress'; + const virtualModuleId = `virtual:${name}`; + const resolvedVirtualModuleId = '\0' + virtualModuleId; + return { + name: `vite-plugin-${name}`, + resolveId(id) { + if (id !== virtualModuleId) return; + return resolvedVirtualModuleId; + }, + async load(id) { + if (id !== resolvedVirtualModuleId) return; + const progress = await genI18nProgress(options); + return `export default ${JSON.stringify(progress)}`; + }, + }; +}; +export default i18nProgress; diff --git a/vite.config.ts b/vite.config.ts index 4368ad818..24467501e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -25,6 +25,7 @@ import { defineConfig } from 'vite'; import tsconfigPaths from 'vite-tsconfig-paths'; import { API_PREFIX, BASE_PATH } from './src/config/constant'; +import i18nProgress from './vite-plugin-i18n-progress'; const inDevContainer = process.env.REMOTE_CONTAINERS === 'true'; @@ -74,6 +75,11 @@ export default defineConfig({ autoCodeSplitting: true, semicolons: false, }), + i18nProgress({ + langs: ['en', 'zh'], + baseLang: 'en', + getTranslationDir: (lang) => `./src/locales/${lang}`, + }), react({ plugins: [observerPlugin() as never], }),