mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-08-16 02:25:54 +08:00
feat: add i18n checker (ui) (#17283)
This commit is contained in:
parent
8c77f2dc03
commit
95c6bd1c8a
164
web/app/dev-only/i18n-checker/page.tsx
Normal file
164
web/app/dev-only/i18n-checker/page.tsx
Normal 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[];
|
||||||
|
}
|
9
web/app/dev-only/layout.tsx
Normal file
9
web/app/dev-only/layout.tsx
Normal 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
49
web/i18n/DEV.md
Normal 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
|
@ -47,8 +47,9 @@ const loadLangResources = (lang: string) => ({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
type Resource = Record<string, ReturnType<typeof loadLangResources>>
|
||||||
// Automatically generate the resources object
|
// 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)
|
acc[lang] = loadLangResources(lang)
|
||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
|
Loading…
x
Reference in New Issue
Block a user