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 |
|:---:|:---:|:---:|
|
![Agent](https://pub-a9416c5573a34388b8d9465d8bef4257.r2.dev/pr-assets/14279/vi-agent.png)
|
![Assistant](https://pub-a9416c5573a34388b8d9465d8bef4257.r2.dev/pr-assets/14279/vi-assistant.png)
|
![Settings](https://pub-a9416c5573a34388b8d9465d8bef4257.r2.dev/pr-assets/14279/vi-settings.png)
|

---------

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:
Kennyzheng
2026-04-16 23:40:39 +08:00
committed by GitHub
parent d1652da1ff
commit 6b5cb0b76b
9 changed files with 6058 additions and 8 deletions

View File

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

View File

@@ -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 = `

View File

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

View File

@@ -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 }) => {

View File

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

View File

@@ -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) => {

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -550,6 +550,7 @@ export type LanguageVarious =
| 'pt-PT'
| 'ro-RO'
| 'ru-RU'
| 'vi-VN'
export type CodeStyleVarious = 'auto' | string