Skip to content

Commit d8a2c9c

Browse files
benjiamingalexjst
andauthored
chore(translation): Add Simplified Chinese (zh-CN support) (#19531)
Co-authored-by: Alex Y <alexjst@users.noreply.github.com>
1 parent 79d1925 commit d8a2c9c

79 files changed

Lines changed: 5380 additions & 6 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

datahub-web-react/src/alchemy-components/components/Editor/RemirrorLocaleProvider.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ async function loadRemirrorLocale(locale: string): Promise<void> {
2121
return;
2222
}
2323
const { default: messages } = await loader();
24-
const plurals = (remirrorPlurals as Record<string, ((n: number, ord?: boolean) => string) | undefined>)[locale];
24+
// Lingui keys plural rules by language only (`pt`, `zh`); region tags fall back to primary subtag.
25+
const pluralsByLocale = remirrorPlurals as Record<string, ((n: number, ord?: boolean) => string) | undefined>;
26+
const plurals = pluralsByLocale[locale] ?? pluralsByLocale[locale.split('-')[0]];
2527
if (plurals) {
2628
remirrorI18n.loadLocaleData(locale, { plurals });
2729
}
@@ -32,15 +34,23 @@ type Props = {
3234
children: React.ReactNode;
3335
};
3436

37+
// Prefer full BCP-47 tag (e.g. zh-CN, pt-BR) before primary subtag.
38+
export function resolveRemirrorLocale(language: string): string {
39+
if (REMIRROR_SUPPORTED_LOCALES.includes(language)) {
40+
return language;
41+
}
42+
const primarySubtag = language.split('-')[0];
43+
return REMIRROR_SUPPORTED_LOCALES.includes(primarySubtag) ? primarySubtag : 'en';
44+
}
45+
3546
/**
3647
* Shares DataHub's Remirror translations with any Remirror editor. Wrap a `<Remirror>` tree
3748
* with this so the editor's built-in labels follow the active app language, falling back to
3849
* English for languages we don't ship a bundle for.
3950
*/
4051
export default function RemirrorLocaleProvider({ children }: Props) {
4152
const { i18n: appI18n } = useTranslation();
42-
const appLanguage = (appI18n.resolvedLanguage || appI18n.language || 'en').split('-')[0];
43-
const locale = REMIRROR_SUPPORTED_LOCALES.includes(appLanguage) ? appLanguage : 'en';
53+
const locale = resolveRemirrorLocale(appI18n.resolvedLanguage || appI18n.language || 'en');
4454
// Only activate once the bundle is loaded, so labels never flash raw message ids. Until then
4555
// the provider stays on English (Remirror's built-in).
4656
const [activeLocale, setActiveLocale] = useState('en');
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import { resolveRemirrorLocale } from '@components/components/Editor/RemirrorLocaleProvider';
4+
5+
describe('resolveRemirrorLocale', () => {
6+
it('prefers an exact BCP-47 match when a bundle exists', () => {
7+
expect(resolveRemirrorLocale('zh-CN')).toBe('zh-CN');
8+
expect(resolveRemirrorLocale('pt-BR')).toBe('pt-BR');
9+
});
10+
11+
it('falls back to the primary language subtag when only that bundle exists', () => {
12+
expect(resolveRemirrorLocale('de-DE')).toBe('de');
13+
expect(resolveRemirrorLocale('ja-JP')).toBe('ja');
14+
});
15+
16+
it('falls back to English when no bundle matches', () => {
17+
expect(resolveRemirrorLocale('pt-PT')).toBe('en');
18+
expect(resolveRemirrorLocale('zh-Hant-TW')).toBe('en');
19+
expect(resolveRemirrorLocale('ko-KR')).toBe('en');
20+
});
21+
});

datahub-web-react/src/app/i18n/__tests__/utils.test.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ describe('detectBrowserLanguage', () => {
4646

4747
it('returns the first supported language in preference order', () => {
4848
// Use unsupported tags first so preference order is exercised past them.
49-
stubLanguages(['ko-KR', 'zh-CN', 'fr-FR', 'de']);
49+
stubLanguages(['ko-KR', 'th-TH', 'fr-FR', 'de']);
5050
expect(detectBrowserLanguage()).toBe('fr');
5151
});
5252

@@ -58,7 +58,29 @@ describe('detectBrowserLanguage', () => {
5858
});
5959

6060
it('returns undefined when no preferred language is supported', () => {
61-
stubLanguages(['ko-KR', 'zh-CN']);
61+
stubLanguages(['ko-KR', 'th-TH']);
62+
expect(detectBrowserLanguage()).toBeUndefined();
63+
});
64+
65+
it('maps Simplified Chinese tags to zh-CN', () => {
66+
stubLanguages(['zh-CN']);
67+
expect(detectBrowserLanguage()).toBe('zh-CN');
68+
stubLanguages(['zh-Hans']);
69+
expect(detectBrowserLanguage()).toBe('zh-CN');
70+
stubLanguages(['zh']);
71+
expect(detectBrowserLanguage()).toBe('zh-CN');
72+
stubLanguages(['zh-SG']);
73+
expect(detectBrowserLanguage()).toBe('zh-CN');
74+
});
75+
76+
it('does not map Traditional Chinese tags to zh-CN', () => {
77+
stubLanguages(['zh-TW']);
78+
expect(detectBrowserLanguage()).toBeUndefined();
79+
stubLanguages(['zh-Hant']);
80+
expect(detectBrowserLanguage()).toBeUndefined();
81+
stubLanguages(['zh-HK']);
82+
expect(detectBrowserLanguage()).toBeUndefined();
83+
stubLanguages(['zh-MO']);
6284
expect(detectBrowserLanguage()).toBeUndefined();
6385
});
6486

datahub-web-react/src/app/i18n/constants.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import jaJP from 'antd/lib/locale/ja_JP';
1010
import nbNO from 'antd/lib/locale/nb_NO';
1111
import ptBR from 'antd/lib/locale/pt_BR';
1212
import svSE from 'antd/lib/locale/sv_SE';
13+
import zhCN from 'antd/lib/locale/zh_CN';
1314

1415
import { LocaleConfig, SupportedLanguage } from '@app/i18n/types';
1516

@@ -90,6 +91,13 @@ export const JA_LOCALE_CONFIG: LocaleConfig = {
9091
label: '日本語 (Beta)',
9192
};
9293

94+
export const ZH_CN_LOCALE_CONFIG: LocaleConfig = {
95+
lang: 'zh-CN',
96+
antd: zhCN,
97+
dayjs: 'zh-cn',
98+
label: '简体中文',
99+
};
100+
93101
export const LOCALE_MAP: Record<SupportedLanguage, LocaleConfig> = {
94102
en: EN_LOCALE_CONFIG,
95103
de: DE_LOCALE_CONFIG,
@@ -102,6 +110,7 @@ export const LOCALE_MAP: Record<SupportedLanguage, LocaleConfig> = {
102110
hu: HU_LOCALE_CONFIG,
103111
fi: FI_LOCALE_CONFIG,
104112
ja: JA_LOCALE_CONFIG,
113+
'zh-CN': ZH_CN_LOCALE_CONFIG,
105114
};
106115

107116
export const LANGUAGE_OPTIONS: SelectOption[] = [
@@ -116,6 +125,7 @@ export const LANGUAGE_OPTIONS: SelectOption[] = [
116125
HU_LOCALE_CONFIG,
117126
FI_LOCALE_CONFIG,
118127
JA_LOCALE_CONFIG,
128+
ZH_CN_LOCALE_CONFIG,
119129
].map((localeConfig) => ({
120130
value: localeConfig.lang,
121131
label: localeConfig.label,

datahub-web-react/src/app/i18n/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Locale } from 'antd/lib/locale-provider';
22

3-
export type SupportedLanguage = 'en' | 'de' | 'es' | 'pt-BR' | 'fr' | 'it' | 'nb' | 'sv' | 'hu' | 'fi' | 'ja';
3+
export type SupportedLanguage = 'en' | 'de' | 'es' | 'pt-BR' | 'fr' | 'it' | 'nb' | 'sv' | 'hu' | 'fi' | 'ja' | 'zh-CN';
44

55
export type LocaleConfig = {
66
lang: SupportedLanguage;

datahub-web-react/src/app/i18n/utils.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ export function isSupportedLanguage(lang: string): lang is SupportedLanguage {
55
return lang in LOCALE_MAP;
66
}
77

8+
function lookupSupportedLanguage(code: string): SupportedLanguage | undefined {
9+
// Cast after the runtime membership check so companion-locale codes (e.g. zh-TW on the
10+
// zh-CN-only PR) type-check without being present in the SupportedLanguage union yet.
11+
return code in LOCALE_MAP ? (code as SupportedLanguage) : undefined;
12+
}
13+
814
/**
915
* Best-effort match of the browser's preferred languages (`navigator.languages`, most-preferred
1016
* first) to a supported UI locale. Tries an exact, case-insensitive match first, then folds region
@@ -20,6 +26,14 @@ export function detectBrowserLanguage(): SupportedLanguage | undefined {
2026
if (!tag) return undefined;
2127
const lower = tag.toLowerCase();
2228
const base = lower.split('-')[0];
29+
if (base === 'zh') {
30+
if (lower.includes('hant') || lower === 'zh-tw' || lower === 'zh-hk' || lower === 'zh-mo') {
31+
// Prefer zh-TW when registered (companion PR); never fold Traditional to zh-CN.
32+
return lookupSupportedLanguage('zh-TW');
33+
}
34+
// zh-Hans, zh-CN, zh-SG, bare zh
35+
return lookupSupportedLanguage('zh-CN');
36+
}
2337
// Exact (case-insensitive) match first, then fold region variants to their base language.
2438
return (
2539
supported.find((locale) => locale.toLowerCase() === lower) ??
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
{
2+
"avatar.defaultName": "用户名",
3+
"calendarChart.weekday.fri": "周五",
4+
"calendarChart.weekday.mon": "周一",
5+
"calendarChart.weekday.sat": "周六",
6+
"calendarChart.weekday.sun": "周日",
7+
"calendarChart.weekday.thu": "周四",
8+
"calendarChart.weekday.tue": "周二",
9+
"calendarChart.weekday.wed": "周三",
10+
"codeBlock.editorLabel": "代码编辑器",
11+
"codeBlock.format": "格式化",
12+
"codeBlock.formatInvalid": "无法格式化此代码,请检查代码是否有效。",
13+
"codeBlock.formatSuccess": "已格式化",
14+
"codeBlock.languageSelectLabel": "语言",
15+
"codeBlock.sqlSyntaxWarning": "此 SQL 可能存在语法问题。",
16+
"codeBlock.syntaxInvalid": "此代码存在语法问题。",
17+
"codeBlock.truncated": "此代码因超出最大长度而被截断。",
18+
"codeBlock.validate": "校验",
19+
"codeBlock.validateSuccess": "校验通过",
20+
"colorPicker.invalidHex.error": "请输入有效的十六进制颜色代码",
21+
"datePicker.placeholder": "选择日期",
22+
"editor.addImage.altLabel": "替代文本",
23+
"editor.addImage.embed": "嵌入图片",
24+
"editor.addImage.title": "添加图片",
25+
"editor.addImage.urlLabel": "图片 URL",
26+
"editor.command.editLink": "编辑链接",
27+
"editor.command.insertImage": "图片",
28+
"editor.command.insertLink": "链接",
29+
"editor.command.toggleBulletList": "项目符号列表",
30+
"editor.command.toggleLink": "移除链接",
31+
"editor.command.updateLink": "更新链接",
32+
"editor.heading.h1": "标题 1",
33+
"editor.heading.h2": "标题 2",
34+
"editor.heading.h3": "标题 3",
35+
"editor.heading.h4": "标题 4",
36+
"editor.heading.h5": "标题 5",
37+
"editor.heading.normal": "正文",
38+
"editor.link.textLabel": "文本",
39+
"editor.link.title": "添加链接",
40+
"editor.link.urlLabel": "链接 URL",
41+
"editor.mentions.notFound": "未找到实体",
42+
"editor.table.deleteColumn": "删除列",
43+
"editor.table.deleteRow": "删除行",
44+
"editor.table.deleteTable": "删除表格",
45+
"editor.table.insertColumnLeft": "在左侧插入列",
46+
"editor.table.insertColumnRight": "在右侧插入列",
47+
"editor.table.insertRowAbove": "在上方插入行",
48+
"editor.table.insertRowBelow": "在下方插入行",
49+
"editor.upload.chooseFile": "选择文件",
50+
"editor.upload.failedTitle": "上传失败",
51+
"editor.upload.fileTooLarge": "你的文件大小({{sizeGb}}GB)超出了 {{maxGb}}GB 的上限",
52+
"editor.upload.fileTypeNotSupported": "不支持的文件类型{{extensionSuffix}}",
53+
"editor.upload.notSupported": "当前场景暂不支持上传文件",
54+
"editor.upload.tabUrl": "URL",
55+
"editor.upload.uploadFile": "上传文件",
56+
"fileNode.uploading": "正在上传 {{fileName}}...",
57+
"fileUpload.dragDropPrompt": "拖入文件或<uploadButton>点击上传</uploadButton>",
58+
"fileUpload.maxSize": "最大大小:2GB",
59+
"graphCard.moreInfo": "更多信息",
60+
"graphCard.noStats": "当前尚未收集该数据资产的统计数据。",
61+
"incidentStage.fixed": "已修复",
62+
"incidentStage.inProgress": "处理中",
63+
"incidentStage.investigation": "调查中",
64+
"incidentStage.noAction": "无需处理",
65+
"incidentStage.triage": "待分类",
66+
"input.placeholder": "占位文本",
67+
"loadedImage.error": "未找到图片",
68+
"loadedVideo.error": "视频加载失败",
69+
"loadedVideo.unsupportedFallback": "你的浏览器不支持 HTML5 视频。请改用<anchor>视频链接</anchor>。",
70+
"noData": "暂无数据",
71+
"pagination.nextPage": "下一页",
72+
"pagination.previousPage": "上一页",
73+
"radio.label": "标签",
74+
"resizablePills.moreHiddenCount_other": "另有 {{count}} 项已隐藏",
75+
"search.placeholder": "搜索...",
76+
"select.placeholder": "请选择一个选项",
77+
"selectItems.addMoreSection": "添加更多",
78+
"selectItems.entitiesLabel": "实体",
79+
"selectItems.itemsLabel": "",
80+
"selectItems.noEntityFound": "未找到 {{entityName}}",
81+
"selectItems.removeAll": "全部移除",
82+
"selectItems.searchForEntity": "搜索 {{entityName}}...",
83+
"selectItems.selectedSection": "已选择",
84+
"switch.label": "标签",
85+
"table.loading": "正在加载数据...",
86+
"textArea.placeholder": "占位文本",
87+
"tooltipHeader.imageAlt": "提示框标题",
88+
"whiskerChart.metric.firstQuartile": "第一四分位数",
89+
"whiskerChart.metric.max": "最大值",
90+
"whiskerChart.metric.median": "中位数",
91+
"whiskerChart.metric.min": "最小值",
92+
"whiskerChart.metric.thirdQuartile": "第三四分位数"
93+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"allDomains": "所有数据域",
3+
"dataLandscapeSummary": "数据全景概览",
4+
"domainLandscapeSummary": "数据域全景概览",
5+
"failedToLoadCharts": "加载图表失败",
6+
"failedToLoadDomains": "加载数据域失败",
7+
"failedToLoadHighlights": "加载重点指标失败",
8+
"noDomainData": "该数据域暂无分析数据",
9+
"selectDomain": "选择数据域",
10+
"usageAnalytics": "使用分析"
11+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"confirmPasswordLabel": "确认密码",
3+
"confirmPasswordRequired": "请确认你的密码",
4+
"emailLabel": "邮箱",
5+
"emailPlaceholder": "name@company.com",
6+
"emailRequired": "请填写你的邮箱",
7+
"fullNameLabel": "全名",
8+
"fullNamePlaceholder": "名字 姓氏",
9+
"fullNameRequired": "请填写你的姓名",
10+
"login.failed": "登录失败!{{error}}",
11+
"login.loading": "正在登录...",
12+
"login.ssoButton": "使用 SSO 登录",
13+
"login.submitButton": "登录",
14+
"passwordHint": "长度必须为 8 个字符,区分大小写",
15+
"passwordLabel": "密码",
16+
"passwordRequired": "请填写你的密码",
17+
"passwordsDoNotMatch": "两次输入的密码不一致",
18+
"reset.failed": "登录失败!",
19+
"reset.loading": "正在重置凭据...",
20+
"reset.submitButton": "重置凭据",
21+
"signup.acceptInviteFailed": "接受邀请失败:{{error}}",
22+
"signup.acceptedInvite": "已接受邀请!",
23+
"signup.loginFailed": "登录失败!发生了意外错误。",
24+
"signup.subHeading": "开始之前,我们还有几个问题",
25+
"signup.submitButton": "开始使用",
26+
"usernameLabel": "用户名",
27+
"usernamePlaceholder": "请输入用户名",
28+
"usernameRequired": "请填写你的用户名",
29+
"welcomeToDataHub": "欢迎使用 DataHub"
30+
}

0 commit comments

Comments
 (0)