fix: Refactor i18n config and fix plugin search box styling issue (#22945)

This commit is contained in:
Wu Tianwei
2025-07-25 15:01:28 +08:00
committed by GitHub
parent 32df3b68c7
commit ad472d59e0
70 changed files with 96 additions and 85 deletions

49
web/i18n-config/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

175
web/i18n-config/README.md Normal file
View File

@@ -0,0 +1,175 @@
# Internationalization (i18n)
## Introduction
This directory contains the internationalization (i18n) files for this project.
## File Structure
```
├── [ 24] README.md
├── [ 0] README_CN.md
├── [ 704] en-US
│   ├── [2.4K] app-annotation.ts
│   ├── [5.2K] app-api.ts
│   ├── [ 16K] app-debug.ts
│   ├── [2.1K] app-log.ts
│   ├── [5.3K] app-overview.ts
│   ├── [1.9K] app.ts
│   ├── [4.1K] billing.ts
│   ├── [ 17K] common.ts
│   ├── [ 859] custom.ts
│   ├── [5.7K] dataset-creation.ts
│   ├── [ 10K] dataset-documents.ts
│   ├── [ 761] dataset-hit-testing.ts
│   ├── [1.7K] dataset-settings.ts
│   ├── [2.0K] dataset.ts
│   ├── [ 941] explore.ts
│   ├── [ 52] layout.ts
│   ├── [2.3K] login.ts
│   ├── [ 52] register.ts
│   ├── [2.5K] share.ts
│   └── [2.8K] tools.ts
├── [1.6K] i18next-config.ts
├── [ 634] index.ts
├── [4.4K] language.ts
```
We use English as the default language. The i18n files are organized by language and then by module. For example, the English translation for the `app` module is in `en-US/app.ts`.
If you want to add a new language or modify an existing translation, you can create a new file for the language or modify the existing file. The file name should be the language code (e.g., `zh-CN` for Chinese) and the file extension should be `.ts`.
For example, if you want to add french translation, you can create a new folder `fr-FR` and add the translation files in it.
By default we will use `LanguagesSupported` to determine which languages are supported. For example, in login page and settings page, we will use `LanguagesSupported` to determine which languages are supported and display them in the language selection dropdown.
## Example
1. Create a new folder for the new language.
```
cp -r en-US fr-FR
```
2. Modify the translation files in the new folder.
3. Add type to new language in the `language.ts` file.
```typescript
export type I18nText = {
'en-US': string
'zh-Hans': string
'pt-BR': string
'es-ES': string
'fr-FR': string
'de-DE': string
'ja-JP': string
'ko-KR': string
'ru-RU': string
'it-IT': string
'uk-UA': string
'YOUR_LANGUAGE_CODE': string
}
```
4. Add the new language to the `language.json` file.
```typescript
export const languages = [
{
value: 'en-US',
name: 'English(United States)',
example: 'Hello, Dify!',
supported: true,
},
{
value: 'zh-Hans',
name: '简体中文',
example: '你好Dify',
supported: true,
},
{
value: 'pt-BR',
name: 'Português(Brasil)',
example: 'Olá, Dify!',
supported: true,
},
{
value: 'es-ES',
name: 'Español(España)',
example: 'Saluton, Dify!',
supported: false,
},
{
value: 'fr-FR',
name: 'Français(France)',
example: 'Bonjour, Dify!',
supported: false,
},
{
value: 'de-DE',
name: 'Deutsch(Deutschland)',
example: 'Hallo, Dify!',
supported: false,
},
{
value: 'ja-JP',
name: '日本語 (日本)',
example: 'こんにちは、Dify!',
supported: false,
},
{
value: 'ko-KR',
name: '한국어 (대한민국)',
example: '안녕, Dify!',
supported: true,
},
{
value: 'ru-RU',
name: 'Русский(Россия)',
example: ' Привет, Dify!',
supported: false,
},
{
value: 'it-IT',
name: 'Italiano(Italia)',
example: 'Ciao, Dify!',
supported: false,
},
{
value: 'th-TH',
name: 'ไทย(ประเทศไทย)',
example: 'สวัสดี Dify!',
supported: false,
},
{
value: 'id-ID',
name: 'Bahasa Indonesia',
example: 'Saluto, Dify!',
supported: false,
},
{
value: 'uk-UA',
name: 'Українська(Україна)',
example: 'Привет, Dify!',
supported: true,
},
// Add your language here 👇
...
// Add your language here 👆
]
```
5. Don't forget to mark the supported field as `true` if the language is supported.
6. Sometime you might need to do some changes in the server side. Please change this file as well. 👇
https://github.com/langgenius/dify/blob/61e4bbabaf2758354db4073cbea09fdd21a5bec1/api/constants/languages.py#L5
## Clean Up
That's it! You have successfully added a new language to the project. If you want to remove a language, you can simply delete the folder and remove the language from the `language.ts` file.
We have a list of languages that we support in the `language.ts` file. But some of them are not supported yet. So, they are marked as `false`. If you want to support a language, you can follow the steps above and mark the supported field as `true`.

View File

@@ -0,0 +1,108 @@
const fs = require('node:fs')
const path = require('node:path')
const transpile = require('typescript').transpile
const magicast = require('magicast')
const { parseModule, generateCode, loadFile } = magicast
const bingTranslate = require('bing-translate-api')
const { translate } = bingTranslate
const data = require('./languages.json')
const targetLanguage = 'en-US'
// https://github.com/plainheart/bing-translate-api/blob/master/src/met/lang.json
const languageKeyMap = data.languages.reduce((map, language) => {
if (language.supported) {
if (language.value === 'zh-Hans' || language.value === 'zh-Hant')
map[language.value] = language.value
else
map[language.value] = language.value.split('-')[0]
}
return map
}, {})
async function translateMissingKeyDeeply(sourceObj, targetObject, toLanguage) {
await Promise.all(Object.keys(sourceObj).map(async (key) => {
if (targetObject[key] === undefined) {
if (typeof sourceObj[key] === 'object') {
targetObject[key] = {}
await translateMissingKeyDeeply(sourceObj[key], targetObject[key], toLanguage)
}
else {
try {
const source = sourceObj[key]
if (!source) {
targetObject[key] = ''
return
}
// not support translate with '(' or ')'
if (source.includes('(') || source.includes(')'))
return
const { translation } = await translate(sourceObj[key], null, languageKeyMap[toLanguage])
targetObject[key] = translation
}
catch {
console.error(`Error translating "${sourceObj[key]}" to ${toLanguage}. Key: ${key}`)
}
}
}
else if (typeof sourceObj[key] === 'object') {
targetObject[key] = targetObject[key] || {}
await translateMissingKeyDeeply(sourceObj[key], targetObject[key], toLanguage)
}
}))
}
async function autoGenTrans(fileName, toGenLanguage) {
const fullKeyFilePath = path.join(__dirname, targetLanguage, `${fileName}.ts`)
const toGenLanguageFilePath = path.join(__dirname, toGenLanguage, `${fileName}.ts`)
// eslint-disable-next-line sonarjs/code-eval
const fullKeyContent = eval(transpile(fs.readFileSync(fullKeyFilePath, 'utf8')))
// if toGenLanguageFilePath is not exist, create it
if (!fs.existsSync(toGenLanguageFilePath)) {
fs.writeFileSync(toGenLanguageFilePath, `const translation = {
}
export default translation
`)
}
// To keep object format and format it for magicast to work: const translation = { ... } => export default {...}
const readContent = await loadFile(toGenLanguageFilePath)
const { code: toGenContent } = generateCode(readContent)
const mod = await parseModule(`export default ${toGenContent.replace('export default translation', '').replace('const translation = ', '')}`)
const toGenOutPut = mod.exports.default
await translateMissingKeyDeeply(fullKeyContent, toGenOutPut, toGenLanguage)
const { code } = generateCode(mod)
const res = `const translation =${code.replace('export default', '')}
export default translation
`.replace(/,\n\n/g, ',\n').replace('};', '}')
fs.writeFileSync(toGenLanguageFilePath, res)
}
async function main() {
// const fileName = 'workflow'
// Promise.all(Object.keys(languageKeyMap).map(async (toLanguage) => {
// await autoGenTrans(fileName, toLanguage)
// }))
const files = fs
.readdirSync(path.join(__dirname, targetLanguage))
.map(file => file.replace(/\.ts/, ''))
.filter(f => f !== 'app-debug') // ast parse error in app-debug
await Promise.all(files.map(async (file) => {
await Promise.all(Object.keys(languageKeyMap).map(async (language) => {
try {
await autoGenTrans(file, language)
}
catch (e) {
console.error(`Error translating ${file} to ${language}`, e)
}
}))
}))
}
main()

View File

@@ -0,0 +1,84 @@
const fs = require('node:fs')
const path = require('node:path')
const transpile = require('typescript').transpile
const targetLanguage = 'en-US'
const data = require('./languages.json')
const languages = data.languages.filter(language => language.supported).map(language => language.value)
async function getKeysFromLanuage(language) {
return new Promise((resolve, reject) => {
const folderPath = path.join(__dirname, language)
let allKeys = []
fs.readdir(folderPath, (err, files) => {
if (err) {
console.error('Error reading folder:', err)
reject(err)
return
}
files.forEach((file) => {
const filePath = path.join(folderPath, file)
const fileName = file.replace(/\.[^/.]+$/, '') // Remove file extension
const camelCaseFileName = fileName.replace(/[-_](.)/g, (_, c) =>
c.toUpperCase(),
) // Convert to camel case
// console.log(camelCaseFileName)
const content = fs.readFileSync(filePath, 'utf8')
// eslint-disable-next-line sonarjs/code-eval
const translationObj = eval(transpile(content))
// console.log(translation)
if(!translationObj || typeof translationObj !== 'object') {
console.error(`Error parsing file: ${filePath}`)
reject(new Error(`Error parsing file: ${filePath}`))
return
}
const keys = Object.keys(translationObj)
const nestedKeys = []
const iterateKeys = (obj, prefix = '') => {
for (const key in obj) {
const nestedKey = prefix ? `${prefix}.${key}` : key
nestedKeys.push(nestedKey)
if (typeof obj[key] === 'object')
iterateKeys(obj[key], nestedKey)
}
}
iterateKeys(translationObj)
allKeys = [...keys, ...nestedKeys].map(
key => `${camelCaseFileName}.${key}`,
)
})
resolve(allKeys)
})
})
}
async function main() {
const compareKeysCount = async () => {
const targetKeys = await getKeysFromLanuage(targetLanguage)
const languagesKeys = await Promise.all(languages.map(language => getKeysFromLanuage(language)))
const keysCount = languagesKeys.map(keys => keys.length)
const targetKeysCount = targetKeys.length
const comparison = languages.reduce((result, language, index) => {
const languageKeysCount = keysCount[index]
const difference = targetKeysCount - languageKeysCount
result[language] = difference
return result
}, {})
console.log(comparison)
// Print missing keys
languages.forEach((language, index) => {
const missingKeys = targetKeys.filter(key => !languagesKeys[index].includes(key))
console.log(`Missing keys in ${language}:`, missingKeys)
})
}
compareKeysCount()
}
main()

View File

@@ -0,0 +1,97 @@
'use client'
import i18n from 'i18next'
import { camelCase } from 'lodash-es'
import { initReactI18next } from 'react-i18next'
const requireSilent = async (lang: string, namespace: string) => {
let res
try {
res = (await import(`../i18n/${lang}/${namespace}`)).default
}
catch {
res = (await import(`../i18n/en-US/${namespace}`)).default
}
return res
}
const NAMESPACES = [
'app-annotation',
'app-api',
'app-debug',
'app-log',
'app-overview',
'app',
'billing',
'common',
'custom',
'dataset-creation',
'dataset-documents',
'dataset-hit-testing',
'dataset-settings',
'dataset',
'education',
'explore',
'layout',
'login',
'plugin-tags',
'plugin',
'register',
'run-log',
'share',
'time',
'tools',
'workflow',
]
export const loadLangResources = async (lang: string) => {
const modules = await Promise.all(NAMESPACES.map(ns => requireSilent(lang, ns)))
const resources = modules.reduce((acc, mod, index) => {
acc[camelCase(NAMESPACES[index])] = mod
return acc
}, {} as Record<string, any>)
return resources
}
/**
* !Need to load en-US and zh-Hans resources for initial rendering, which are used in both marketplace and dify
* !Other languages will be loaded on demand
* !This is to avoid loading all languages at once which can be slow
*/
const getInitialTranslations = () => {
const en_USResources = NAMESPACES.reduce((acc, ns, index) => {
acc[camelCase(NAMESPACES[index])] = require(`../i18n/en-US/${ns}`).default
return acc
}, {} as Record<string, any>)
const zh_HansResources = NAMESPACES.reduce((acc, ns, index) => {
acc[camelCase(NAMESPACES[index])] = require(`../i18n/zh-Hans/${ns}`).default
return acc
}, {} as Record<string, any>)
return {
'en-US': {
translation: en_USResources,
},
'zh-Hans': {
translation: zh_HansResources,
},
}
}
if (!i18n.isInitialized) {
i18n.use(initReactI18next)
.init({
lng: undefined,
fallbackLng: 'en-US',
resources: getInitialTranslations(),
})
}
export const changeLanguage = async (lng?: string) => {
const resolvedLng = lng ?? 'en-US'
const resource = await loadLangResources(resolvedLng)
if (!i18n.hasResourceBundle(resolvedLng, 'translation'))
i18n.addResourceBundle(resolvedLng, 'translation', resource, true, true)
await i18n.changeLanguage(resolvedLng)
}
export default i18n

29
web/i18n-config/index.ts Normal file
View File

@@ -0,0 +1,29 @@
import Cookies from 'js-cookie'
import { changeLanguage } from '@/i18n-config/i18next-config'
import { LOCALE_COOKIE_NAME } from '@/config'
import { LanguagesSupported } from '@/i18n-config/language'
export const i18n = {
defaultLocale: 'en-US',
locales: LanguagesSupported,
} as const
export type Locale = typeof i18n['locales'][number]
export const setLocaleOnClient = async (locale: Locale, reloadPage = true) => {
Cookies.set(LOCALE_COOKIE_NAME, locale, { expires: 365 })
await changeLanguage(locale)
reloadPage && location.reload()
}
export const getLocaleOnClient = (): Locale => {
return Cookies.get(LOCALE_COOKIE_NAME) as Locale || i18n.defaultLocale
}
export const renderI18nObject = (obj: Record<string, string>, language: string) => {
if (!obj) return ''
if (obj?.[language]) return obj[language]
if (obj?.en_US) return obj.en_US
return Object.values(obj)[0]
}

114
web/i18n-config/language.ts Normal file
View File

@@ -0,0 +1,114 @@
import data from './languages.json'
export type Item = {
value: number | string
name: string
example: string
}
export type I18nText = {
'en-US': string
'zh-Hans': string
'pt-BR': string
'es-ES': string
'fr-FR': string
'de-DE': string
'ja-JP': string
'ko-KR': string
'ru-RU': string
'it-IT': string
'uk-UA': string
'vi-VN': string
'de_DE': string
'zh_Hant': string
'ro-RO': string
'pl-PL': string
'hi-IN': string
'fa-IR': string
'sl-SI': string
'th-TH': string
}
export const languages = data.languages
export const LanguagesSupported = languages.filter(item => item.supported).map(item => item.value)
export const getLanguage = (locale: string) => {
if (['zh-Hans', 'ja-JP'].includes(locale))
return locale.replace('-', '_')
return LanguagesSupported[0].replace('-', '_')
}
const DOC_LANGUAGE: Record<string, string> = {
'zh-Hans': 'zh-hans',
'ja-JP': 'ja-jp',
'en-US': 'en',
}
export const getDocLanguage = (locale: string) => {
return DOC_LANGUAGE[locale] || 'en'
}
const PRICING_PAGE_LANGUAGE: Record<string, string> = {
'ja-JP': 'jp',
}
export const getPricingPageLanguage = (locale: string) => {
return PRICING_PAGE_LANGUAGE[locale] || ''
}
export const NOTICE_I18N = {
title: {
en_US: 'Important Notice',
zh_Hans: '重要公告',
pt_BR: 'Aviso Importante',
es_ES: 'Aviso Importante',
fr_FR: 'Avis important',
de_DE: 'Wichtiger Hinweis',
ja_JP: '重要なお知らせ',
ko_KR: '중요 공지',
pl_PL: 'Ważne ogłoszenie',
uk_UA: 'Важливе повідомлення',
ru_RU: 'Важное Уведомление',
vi_VN: 'Thông báo quan trọng',
it_IT: 'Avviso Importante',
fa_IR: 'هشدار مهم',
sl_SI: 'Pomembno obvestilo',
th_TH: 'ประกาศสำคัญ',
},
desc: {
en_US:
'Our system will be unavailable from 19:00 to 24:00 UTC on August 28 for an upgrade. For questions, kindly contact our support team (support@dify.ai). We value your patience.',
zh_Hans:
'为了有效提升数据检索能力及稳定性Dify 将于 2023 年 8 月 29 日 03:00 至 08:00 期间进行服务升级,届时 Dify 云端版及应用将无法访问。感谢您的耐心与支持。',
pt_BR:
'Our system will be unavailable from 19:00 to 24:00 UTC on August 28 for an upgrade. For questions, kindly contact our support team (support@dify.ai). We value your patience.',
es_ES:
'Our system will be unavailable from 19:00 to 24:00 UTC on August 28 for an upgrade. For questions, kindly contact our support team (support@dify.ai). We value your patience.',
fr_FR:
'Our system will be unavailable from 19:00 to 24:00 UTC on August 28 for an upgrade. For questions, kindly contact our support team (support@dify.ai). We value your patience.',
de_DE:
'Our system will be unavailable from 19:00 to 24:00 UTC on August 28 for an upgrade. For questions, kindly contact our support team (support@dify.ai). We value your patience.',
ja_JP:
'Our system will be unavailable from 19:00 to 24:00 UTC on August 28 for an upgrade. For questions, kindly contact our support team (support@dify.ai). We value your patience.',
ko_KR:
'시스템이 업그레이드를 위해 UTC 시간대로 8 월 28 일 19:00 ~ 24:00 에 사용 불가될 예정입니다. 질문이 있으시면 지원 팀에 연락주세요 (support@dify.ai). 최선을 다해 답변해드리겠습니다.',
pl_PL:
'Nasz system będzie niedostępny od 19:00 do 24:00 UTC 28 sierpnia w celu aktualizacji. W przypadku pytań prosimy o kontakt z naszym zespołem wsparcia (support@dify.ai). Doceniamy Twoją cierpliwość.',
uk_UA:
'Наша система буде недоступна з 19:00 до 24:00 UTC 28 серпня для оновлення. Якщо у вас виникнуть запитання, будь ласка, зв’яжіться з нашою службою підтримки (support@dify.ai). Дякуємо за терпіння.',
ru_RU:
'Наша система будет недоступна с 19:00 до 24:00 UTC 28 августа для обновления. По вопросам, пожалуйста, обращайтесь в нашу службу поддержки (support@dify.ai). Спасибо за ваше терпение',
vi_VN:
'Hệ thống của chúng tôi sẽ ngừng hoạt động từ 19:00 đến 24:00 UTC vào ngày 28 tháng 8 để nâng cấp. Nếu có thắc mắc, vui lòng liên hệ với nhóm hỗ trợ của chúng tôi (support@dify.ai). Chúng tôi đánh giá cao sự kiên nhẫn của bạn.',
tr_TR:
'Sistemimiz, 28 Ağustos\'ta 19:00 ile 24:00 UTC saatleri arasında güncelleme nedeniyle kullanılamayacaktır. Sorularınız için lütfen destek ekibimizle iletişime geçin (support@dify.ai). Sabrınız için teşekkür ederiz.',
fa_IR:
'سیستم ما از ساعت 19:00 تا 24:00 UTC در تاریخ 28 اوت برای ارتقاء در دسترس نخواهد بود. برای سؤالات، لطفاً با تیم پشتیبانی ما (support@dify.ai) تماس بگیرید. ما برای صبر شما ارزش قائلیم.',
sl_SI:
'Naš sistem ne bo na voljo od 19:00 do 24:00 UTC 28. avgusta zaradi nadgradnje. Za vprašanja se obrnite na našo skupino za podporo (support@dify.ai). Cenimo vašo potrpežljivost.',
th_TH:
'ระบบของเราจะไม่สามารถใช้งานได้ตั้งแต่เวลา 19:00 ถึง 24:00 UTC ในวันที่ 28 สิงหาคม เพื่อทำการอัปเกรด หากมีคำถามใดๆ กรุณาติดต่อทีมสนับสนุนของเรา (support@dify.ai) เราขอขอบคุณในความอดทนของท่าน',
},
href: '#',
}

View File

@@ -0,0 +1,151 @@
{
"languages": [
{
"value": "en-US",
"name": "English (United States)",
"prompt_name": "English",
"example": "Hello, Dify!",
"supported": true
},
{
"value": "zh-Hans",
"name": "简体中文",
"prompt_name": "Chinese Simplified",
"example": "你好Dify",
"supported": true
},
{
"value": "zh-Hant",
"name": "繁體中文",
"prompt_name": "Chinese Traditional",
"example": "你好Dify",
"supported": true
},
{
"value": "pt-BR",
"name": "Português (Brasil)",
"prompt_name": "Portuguese",
"example": "Olá, Dify!",
"supported": true
},
{
"value": "es-ES",
"name": "Español (España)",
"prompt_name": "Spanish",
"example": "Saluton, Dify!",
"supported": true
},
{
"value": "fr-FR",
"name": "Français (France)",
"prompt_name": "French",
"example": "Bonjour, Dify!",
"supported": true
},
{
"value": "de-DE",
"name": "Deutsch (Deutschland)",
"prompt_name": "German",
"example": "Hallo, Dify!",
"supported": true
},
{
"value": "ja-JP",
"name": "日本語 (日本)",
"prompt_name": "Japanese",
"example": "こんにちは、Dify!",
"supported": true
},
{
"value": "ko-KR",
"name": "한국어 (대한민국)",
"prompt_name": "Korean",
"example": "안녕하세요, Dify!",
"supported": true
},
{
"value": "ru-RU",
"name": "Русский (Россия)",
"prompt_name": "Russian",
"example": " Привет, Dify!",
"supported": true
},
{
"value": "it-IT",
"name": "Italiano (Italia)",
"prompt_name": "Italian",
"example": "Ciao, Dify!",
"supported": true
},
{
"value": "th-TH",
"name": "ไทย (ประเทศไทย)",
"prompt_name": "Thai",
"example": "สวัสดี Dify!",
"supported": true
},
{
"value": "id-ID",
"name": "Bahasa Indonesia",
"prompt_name": "Indonesian",
"example": "Saluto, Dify!",
"supported": false
},
{
"value": "uk-UA",
"name": "Українська (Україна)",
"prompt_name": "Ukrainian",
"example": "Привет, Dify!",
"supported": true
},
{
"value": "vi-VN",
"name": "Tiếng Việt (Việt Nam)",
"prompt_name": "Vietnamese",
"example": "Xin chào, Dify!",
"supported": true
},
{
"value": "ro-RO",
"name": "Română (România)",
"prompt_name": "Romanian",
"example": "Salut, Dify!",
"supported": true
},
{
"value": "pl-PL",
"name": "Polski (Polish)",
"prompt_name": "Polish",
"example": "Cześć, Dify!",
"supported": true
},
{
"value": "hi-IN",
"name": "Hindi (India)",
"prompt_name": "Hindi",
"example": "नमस्ते, Dify!",
"supported": "true"
},
{
"value": "tr-TR",
"name": "Türkçe",
"prompt_name": "Türkçe",
"example": "Selam!",
"supported": "true"
},
{
"value": "fa-IR",
"name": "Farsi (Iran)",
"prompt_name": "Farsi",
"example": "سلام, دیفای!",
"supported": true
},
{
"value": "sl-SI",
"name": "Slovensko (Slovenija)",
"prompt_name": "Slovensko",
"example": "Zdravo, Dify!",
"supported": true
}
]
}

56
web/i18n-config/server.ts Normal file
View File

@@ -0,0 +1,56 @@
import { cookies, headers } from 'next/headers'
import Negotiator from 'negotiator'
import { match } from '@formatjs/intl-localematcher'
import { createInstance } from 'i18next'
import resourcesToBackend from 'i18next-resources-to-backend'
import { initReactI18next } from 'react-i18next/initReactI18next'
import { i18n } from '.'
import type { Locale } from '.'
// https://locize.com/blog/next-13-app-dir-i18n/
const initI18next = async (lng: Locale, ns: string) => {
const i18nInstance = createInstance()
await i18nInstance
.use(initReactI18next)
.use(resourcesToBackend((language: string, namespace: string) => import(`../i18n/${language}/${namespace}.ts`)))
.init({
lng: lng === 'zh-Hans' ? 'zh-Hans' : lng,
ns,
fallbackLng: 'en-US',
})
return i18nInstance
}
export async function useTranslation(lng: Locale, ns = '', options: Record<string, any> = {}) {
const i18nextInstance = await initI18next(lng, ns)
return {
t: i18nextInstance.getFixedT(lng, ns, options.keyPrefix),
i18n: i18nextInstance,
}
}
export const getLocaleOnServer = async (): Promise<Locale> => {
const locales: string[] = i18n.locales
let languages: string[] | undefined
// get locale from cookie
const localeCookie = (await cookies()).get('locale')
languages = localeCookie?.value ? [localeCookie.value] : []
if (!languages.length) {
// Negotiator expects plain object so we need to transform headers
const negotiatorHeaders: Record<string, string> = {};
(await headers()).forEach((value, key) => (negotiatorHeaders[key] = value))
// Use negotiator and intl-localematcher to get best locale
languages = new Negotiator({ headers: negotiatorHeaders }).languages()
}
// Validate languages
if (!Array.isArray(languages) || languages.length === 0 || !languages.every(lang => typeof lang === 'string' && /^[\w-]+$/.test(lang)))
languages = [i18n.defaultLocale]
// match locale
const matchedLocale = match(languages, locales, i18n.defaultLocale) as Locale
return matchedLocale
}