mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-07-03 20:59:22 +08:00
feat(i18n): add Vietnamese (vi-VN) language support (#14279)
### What this PR does Before this PR: Cherry Studio supported 11 languages but did not include Vietnamese. After this PR: Vietnamese (vi-VN / Tiếng Việt) is fully supported with 4016 translated keys across both renderer and main processes. Ant Design components correctly display Vietnamese locale. All 106 previously leaked non-Vietnamese strings have been re-translated. ### Why we need it and why it was done in this way Vietnamese users represent a growing segment of Cherry Studio's user base. Adding vi-VN follows the same pattern as all existing machine-translated locales (de-DE, es-ES, fr-FR, etc.): 1. Register `vi-VN` in the `LanguageVarious` TypeScript union type 2. Add locale entries to all `Record<LanguageVarious, ...>` maps (i18n, dayjs, EmojiPicker) 3. Add `vi-VN` to main process `locales.ts` for menu/dialog translations 4. Add `vi-VN` case to `AntdProvider.tsx` for Ant Design component localization 5. Add language selector option in GeneralSettings 6. Add `'vi-vn': 'Vietnamese'` to the auto-translate script's `languageMap` 7. Generate the initial `vi-vn.json` translation file and fix 106 wrong-language entries The following tradeoffs were made: - EmojiPicker falls back to English data/i18n for Vietnamese since `emoji-picker-element` doesn't ship Vietnamese locale data. This is consistent with how Romanian and Greek are handled. The following alternatives were considered: - Manual translation: rejected in favor of machine translation for consistency with other non-CJK locales, and because the CI pipeline auto-maintains translations going forward. ### Breaking changes None. ### Special notes for your reviewer - The `vi-vn.json` file (~6000 lines) is machine-translated. 106 entries that originally leaked from other locales (German, Spanish, French, Italian, Thai, Japanese, Korean, etc.) were detected and re-translated. - Compared to the previous PR (#14277), this version additionally updates `src/main/utils/locales.ts` (main process translations) and `src/renderer/src/context/AntdProvider.tsx` (Ant Design locale), which were missing before. - No changes to the CI workflow (`auto-i18n.yml`) were needed — it auto-discovers all JSON files in the `translate/` directory. - All checks pass: `pnpm lint`, `pnpm test` (4107 tests), `pnpm typecheck`, `pnpm i18n:check`. ### Checklist - [x] PR: The PR description is expressive enough and will help future contributors - [x] Code: Write code that humans can understand and Keep it simple - [x] Refactor: You have left the code cleaner than you found it (Boy Scout Rule) - [ ] Upgrade: Impact of this change on upgrade flows was considered and addressed if required - [x] Documentation: A user-guide update was considered and is present (link) or not required. Check this only when the PR introduces or changes a user-facing feature or behavior. - [x] Self-review: I have reviewed my own code before requesting review from others ### Release note ```release-note Added Vietnamese (Tiếng Việt) language support with complete UI translations and automatic translation maintenance via CI. ``` ### Screenshots | Agent Page | Assistant Page | Settings Page | |:---:|:---:|:---:| |  |  |  | --------- Signed-off-by: zhengke090@gmail.com <zhengke090@126.com> Co-authored-by: zhengke090@gmail.com <zhengke090@126.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: SuYao <sy20010504@gmail.com> Co-authored-by: George·Dong <98630204+GeorgeDong32@users.noreply.github.com>
This commit is contained in:
@@ -11,5 +11,6 @@ export const languageEnglishNameMap: Record<LanguageVarious, string> = {
|
||||
'ro-RO': 'Romanian',
|
||||
'ru-RU': 'Russian',
|
||||
'zh-CN': 'Chinese (Simplified)',
|
||||
'vi-VN': 'Vietnamese',
|
||||
'zh-TW': 'Chinese (Traditional)'
|
||||
}
|
||||
|
||||
@@ -153,7 +153,8 @@ const languageMap = {
|
||||
'fr-fr': 'French',
|
||||
'pt-pt': 'Portuguese',
|
||||
'de-de': 'German',
|
||||
'ro-ro': 'Romanian'
|
||||
'ro-ro': 'Romanian',
|
||||
'vi-vn': 'Vietnamese'
|
||||
}
|
||||
|
||||
const PROMPT = `
|
||||
|
||||
@@ -12,6 +12,7 @@ import JaJP from '../../renderer/src/i18n/translate/ja-jp.json'
|
||||
import ptPT from '../../renderer/src/i18n/translate/pt-pt.json'
|
||||
import roRO from '../../renderer/src/i18n/translate/ro-ro.json'
|
||||
import RuRu from '../../renderer/src/i18n/translate/ru-ru.json'
|
||||
import viVN from '../../renderer/src/i18n/translate/vi-vn.json'
|
||||
|
||||
const locales = Object.fromEntries(
|
||||
[
|
||||
@@ -25,7 +26,8 @@ const locales = Object.fromEntries(
|
||||
['es-ES', esES],
|
||||
['fr-FR', frFR],
|
||||
['pt-PT', ptPT],
|
||||
['ro-RO', roRO]
|
||||
['ro-RO', roRO],
|
||||
['vi-VN', viVN]
|
||||
].map(([locale, translation]) => [locale, { translation }])
|
||||
)
|
||||
|
||||
|
||||
@@ -46,7 +46,8 @@ const i18nMap: Record<LanguageVarious, typeof en> = {
|
||||
'ja-JP': ja,
|
||||
'pt-PT': pt_PT,
|
||||
'ro-RO': en, // No Romanian available, fallback to English
|
||||
'ru-RU': ru_RU
|
||||
'ru-RU': ru_RU,
|
||||
'vi-VN': en // No Vietnamese available, fallback to English
|
||||
}
|
||||
|
||||
// Mapping from app locale to emoji data URL
|
||||
@@ -62,7 +63,8 @@ const dataSourceMap: Record<LanguageVarious, string> = {
|
||||
'ja-JP': dataJA,
|
||||
'pt-PT': dataPT,
|
||||
'ro-RO': dataEN, // No Romanian CLDR available, fallback to English
|
||||
'ru-RU': dataRU
|
||||
'ru-RU': dataRU,
|
||||
'vi-VN': dataEN // No Vietnamese CLDR available, fallback to English
|
||||
}
|
||||
|
||||
// Mapping from app locale to emoji-picker-element locale string
|
||||
@@ -78,7 +80,8 @@ const localeMap: Record<LanguageVarious, string> = {
|
||||
'ja-JP': 'ja',
|
||||
'pt-PT': 'pt',
|
||||
'ro-RO': 'en',
|
||||
'ru-RU': 'ru'
|
||||
'ru-RU': 'ru',
|
||||
'vi-VN': 'en'
|
||||
}
|
||||
|
||||
const EmojiPicker: FC<Props> = ({ onEmojiClick }) => {
|
||||
|
||||
@@ -10,6 +10,7 @@ import jaJP from 'antd/locale/ja_JP'
|
||||
import ptPT from 'antd/locale/pt_PT'
|
||||
import roRO from 'antd/locale/ro_RO'
|
||||
import ruRU from 'antd/locale/ru_RU'
|
||||
import viVN from 'antd/locale/vi_VN'
|
||||
import zhCN from 'antd/locale/zh_CN'
|
||||
import zhTW from 'antd/locale/zh_TW'
|
||||
import type { FC, PropsWithChildren } from 'react'
|
||||
@@ -144,6 +145,8 @@ function getAntdLocale(language: LanguageVarious) {
|
||||
return ptPT
|
||||
case 'ro-RO':
|
||||
return roRO
|
||||
case 'vi-VN':
|
||||
return viVN
|
||||
default:
|
||||
return zhCN
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'dayjs/locale/ja'
|
||||
import 'dayjs/locale/pt'
|
||||
import 'dayjs/locale/ro'
|
||||
import 'dayjs/locale/ru'
|
||||
import 'dayjs/locale/vi'
|
||||
import 'dayjs/locale/zh-cn'
|
||||
import 'dayjs/locale/zh-tw'
|
||||
|
||||
@@ -28,6 +29,7 @@ import jaJP from './translate/ja-jp.json'
|
||||
import ptPT from './translate/pt-pt.json'
|
||||
import roRO from './translate/ro-ro.json'
|
||||
import ruRU from './translate/ru-ru.json'
|
||||
import viVN from './translate/vi-vn.json'
|
||||
|
||||
const logger = loggerService.withContext('I18N')
|
||||
|
||||
@@ -43,7 +45,8 @@ const resources = Object.fromEntries(
|
||||
['es-ES', esES],
|
||||
['fr-FR', frFR],
|
||||
['pt-PT', ptPT],
|
||||
['ro-RO', roRO]
|
||||
['ro-RO', roRO],
|
||||
['vi-VN', viVN]
|
||||
].map(([locale, translation]) => [locale, { translation }])
|
||||
)
|
||||
|
||||
@@ -67,7 +70,8 @@ const dayjsLocaleMap: Record<string, string> = {
|
||||
'es-ES': 'es',
|
||||
'fr-FR': 'fr',
|
||||
'pt-PT': 'pt',
|
||||
'ro-RO': 'ro'
|
||||
'ro-RO': 'ro',
|
||||
'vi-VN': 'vi'
|
||||
}
|
||||
|
||||
export const setDayjsLocale = (language: string) => {
|
||||
|
||||
6034
src/renderer/src/i18n/translate/vi-vn.json
Normal file
6034
src/renderer/src/i18n/translate/vi-vn.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -149,7 +149,8 @@ const GeneralSettings: FC = () => {
|
||||
{ value: 'es-ES', label: 'Español', flag: '🇪🇸' },
|
||||
{ value: 'fr-FR', label: 'Français', flag: '🇫🇷' },
|
||||
{ value: 'pt-PT', label: 'Português', flag: '🇵🇹' },
|
||||
{ value: 'ro-RO', label: 'Română', flag: '🇷🇴' }
|
||||
{ value: 'ro-RO', label: 'Română', flag: '🇷🇴' },
|
||||
{ value: 'vi-VN', label: 'Tiếng Việt', flag: '🇻🇳' }
|
||||
]
|
||||
|
||||
const notificationSettings = useSelector((state: RootState) => state.settings.notification)
|
||||
|
||||
@@ -550,6 +550,7 @@ export type LanguageVarious =
|
||||
| 'pt-PT'
|
||||
| 'ro-RO'
|
||||
| 'ru-RU'
|
||||
| 'vi-VN'
|
||||
|
||||
export type CodeStyleVarious = 'auto' | string
|
||||
|
||||
|
||||
Reference in New Issue
Block a user