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],
     }),

Reply via email to