feat: add i18n checker (ui) (#17283)

This commit is contained in:
Kindy Lin 2025-04-02 13:58:31 +08:00 committed by GitHub
parent 8c77f2dc03
commit 95c6bd1c8a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 224 additions and 1 deletions

View File

@ -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<Lang[]>([])
useEffect(() => {
setLangs(genLangs())
}, [])
return (
<div
style={{
height: 'calc(100% - 6em)',
overflowY: 'auto',
margin: '1em 1em 5em',
}}
>
<div style={{ minHeight: '75vh' }}>
<h2>Summary</h2>
<table
className={cn('mt-2 min-w-[340px] border-collapse border-0')}
>
<thead className="system-xs-medium-uppercase text-text-tertiary">
<tr>
<td className="w-5 min-w-5 whitespace-nowrap rounded-l-lg bg-background-section-burn pl-2 pr-1">
#
</td>
<td className="w-20 min-w-20 whitespace-nowrap bg-background-section-burn py-1.5 pl-3">
lang
</td>
<td className="w-20 min-w-20 whitespace-nowrap bg-background-section-burn py-1.5 pl-3">
count
</td>
<td className="w-20 min-w-20 whitespace-nowrap bg-background-section-burn py-1.5 pl-3">
missing
</td>
<td className="w-20 min-w-20 whitespace-nowrap bg-background-section-burn py-1.5 pl-3">
extra
</td>
</tr>
</thead>
<tbody className="system-sm-regular text-text-secondary">
{langs.map(({ locale, count, missing, extra }, idx) => <tr key={locale}>
<td className="">{idx}</td>
<td className="p-1.5">{locale}</td>
<td>{count}</td>
<td>{missing.length}</td>
<td>{extra.length}</td>
</tr>)}
</tbody>
</table>
</div>
<h2>Details</h2>
<table
className={cn('mt-2 w-full min-w-[340px] border-collapse border-0')}
>
<thead className="system-xs-medium-uppercase text-text-tertiary">
<tr>
<td className="w-5 min-w-5 whitespace-nowrap rounded-l-lg bg-background-section-burn pl-2 pr-1">
#
</td>
<td className="w-20 min-w-20 whitespace-nowrap bg-background-section-burn py-1.5 pl-3">
lang
</td>
<td className="w-full whitespace-nowrap bg-background-section-burn py-1.5 pl-3">
missing
</td>
<td className="w-full whitespace-nowrap bg-background-section-burn py-1.5 pl-3">
extra
</td>
</tr>
</thead>
<tbody>
{langs.map(({ locale, missing, extra }, idx) => {
return (<tr key={locale}>
<td className="py-2 align-top">{idx}</td>
<td className="py-2 align-top">{locale}</td>
<td className="py-2 align-top">
<ul>
{missing.map(key => (
<li key={key}>{key}</li>
))}
</ul>
</td>
<td className="py-2 align-top">
<ul>
{extra.map(key => (
<li key={key}>{key}</li>
))}
</ul>
</td>
</tr>
)
})}
</tbody>
</table>
</div>
)
}
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, any>): string[] {
const nestedKeys: string[] = []
const iterateKeys = (obj: Record<string, any>, 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<string>;
count: number;
missing: string[];
extra: string[];
}

View File

@ -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
}

49
web/i18n/DEV.md Normal file
View File

@ -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 `<I18nServer/>`
- 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 `<I18N/>`
- 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

View File

@ -47,8 +47,9 @@ const loadLangResources = (lang: string) => ({
},
})
type Resource = Record<string, ReturnType<typeof loadLangResources>>
// Automatically generate the resources object
const resources = LanguagesSupported.reduce((acc: any, lang: string) => {
export const resources = LanguagesSupported.reduce<Resource>((acc, lang) => {
acc[lang] = loadLangResources(lang)
return acc
}, {})