diff --git a/web/app/dev-only/i18n-checker/page.tsx b/web/app/dev-only/i18n-checker/page.tsx new file mode 100644 index 0000000000..5ed0c86b82 --- /dev/null +++ b/web/app/dev-only/i18n-checker/page.tsx @@ -0,0 +1,164 @@ +'use client' +import { resources } from '@/i18n/i18next-config' +import { useEffect, useState } from 'react' +import cn from '@/utils/classnames' + +export default function I18nTest() { + const [langs, setLangs] = useState([]) + + useEffect(() => { + setLangs(genLangs()) + }, []) + + return ( +
+ +
+

Summary

+ + + + + + + + + + + + + {langs.map(({ locale, count, missing, extra }, idx) => + + + + + + )} + +
+ # + + lang + + count + + missing + + extra +
{idx}{locale}{count}{missing.length}{extra.length}
+
+ +

Details

+ + + + + + + + + + + + + {langs.map(({ locale, missing, extra }, idx) => { + return ( + + + + + + ) + })} + +
+ # + + lang + + missing + + extra +
{idx}{locale} +
    + {missing.map(key => ( +
  • {key}
  • + ))} +
+
+
    + {extra.map(key => ( +
  • {key}
  • + ))} +
+
+ +
+ ) +} + +function genLangs() { + const langs_: Lang[] = [] + let en!: Lang + + for (const [key, value] of Object.entries(resources)) { + const keys = getNestedKeys(value.translation) + const lang: Lang = { + locale: key, + keys: new Set(keys), + count: keys.length, + missing: [], + extra: [], + } + + langs_.push(lang) + if (key === 'en-US') en = lang + } + + for (const lang of langs_) { + const missing: string[] = [] + const extra: string[] = [] + + for (const key of lang.keys) + if (!en.keys.has(key)) extra.push(key) + + for (const key of en.keys) + if (!lang.keys.has(key)) missing.push(key) + + lang.missing = missing + lang.extra = extra + } + return langs_ +} + +function getNestedKeys(translation: Record): string[] { + const nestedKeys: string[] = [] + const iterateKeys = (obj: Record, prefix = '') => { + for (const key in obj) { + const nestedKey = prefix ? `${prefix}.${key}` : key + // nestedKeys.push(nestedKey); + if (typeof obj[key] === 'object') iterateKeys(obj[key], nestedKey) + else if (typeof obj[key] === 'string') nestedKeys.push(nestedKey) + } + } + iterateKeys(translation) + return nestedKeys +} + +type Lang = { + locale: string; + keys: Set; + count: number; + missing: string[]; + extra: string[]; +} diff --git a/web/app/dev-only/layout.tsx b/web/app/dev-only/layout.tsx new file mode 100644 index 0000000000..d8bcc5e679 --- /dev/null +++ b/web/app/dev-only/layout.tsx @@ -0,0 +1,9 @@ +import type React from 'react' +import { notFound } from 'next/navigation' + +export default async function Layout({ children }: React.PropsWithChildren) { + if (process.env.NODE_ENV !== 'development') + notFound() + + return children +} diff --git a/web/i18n/DEV.md b/web/i18n/DEV.md new file mode 100644 index 0000000000..08b478fed4 --- /dev/null +++ b/web/i18n/DEV.md @@ -0,0 +1,49 @@ + +## library + +* i18next +* react-i18next + +## hooks + +* useTranslation +* useGetLanguage +* useI18N +* useRenderI18nObject + +## impl + +* App Boot + - app/layout.tsx load i18n and init context + - use `` + - read locale with `getLocaleOnServer` (in node.js) + - locale from cookie, or browser request header + - only used in client app init and 2 server code(plugin desc, datasets) + - use `` + - init i18n context + - `setLocaleOnClient` + - `changeLanguage` (defined in i18n/i18next-config, also init i18n resources (side effects)) + * is `i18next.changeLanguage` + * all languages text is merge & load in FrontEnd as .js (see i18n/i18next-config) +* i18n context + - `locale` - current locale code (ex `eu-US`, `zh-Hans`) + - `i18n` - useless + - `setLocaleOnClient` - used by App Boot and user change language + +### load i18n resources + +- client: i18n/i18next-config.ts + * ns = camalCase(filename) + * ex: `app/components/datasets/create/embedding-process/index.tsx` + * `t('datasetSettings.form.retrievalSetting.title')` +- server: i18n/server.ts + * ns = filename + * ex: `app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/settings/page.tsx` + * `translate(locale, 'dataset-settings')` + +## TODO + +* [ ] ts docs for useGetLanguage +* [ ] ts docs for useI18N +* [ ] client docs for i18n +* [ ] server docs for i18n diff --git a/web/i18n/i18next-config.ts b/web/i18n/i18next-config.ts index eea15ac8be..8c5583bf9a 100644 --- a/web/i18n/i18next-config.ts +++ b/web/i18n/i18next-config.ts @@ -47,8 +47,9 @@ const loadLangResources = (lang: string) => ({ }, }) +type Resource = Record> // Automatically generate the resources object -const resources = LanguagesSupported.reduce((acc: any, lang: string) => { +export const resources = LanguagesSupported.reduce((acc, lang) => { acc[lang] = loadLangResources(lang) return acc }, {})