Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
060a969
feat(i18n): detect browser language and add fallbackLng
710leo Jul 7, 2026
50648f6
feat(i18n): sync moment locale with language switch
710leo Jul 7, 2026
0990ffe
feat(i18n): translate login page and SSO callback messages
710leo Jul 7, 2026
5d0dc5d
chore(i18n): add locale key checker and fill all missing en_US keys
710leo Jul 7, 2026
110fdc6
fix(i18n): resolve t('中文') calls that had no English translation
710leo Jul 7, 2026
364d2ca
fix(i18n): localize AiChatNG form-select summary text
710leo Jul 7, 2026
a278760
fix(i18n): localize datasource auth form and channel payload defaults
710leo Jul 7, 2026
42fe8a7
feat(i18n): localize product doc links with the _en/_hk suffix
710leo Jul 7, 2026
5cb65ad
fix(i18n): translate scattered hardcoded strings in core pages
710leo Jul 7, 2026
f49b5ab
fix(i18n): localize trace labels and switch enum values to identifiers
710leo Jul 7, 2026
e770ed6
fix(i18n): translate the dashboard migration tool
710leo Jul 7, 2026
8c5bdd1
fix(i18n): map zh_HK/ja_JP to their antd locales
710leo Jul 7, 2026
b06d589
Merge remote-tracking branch 'origin/main' into optimize-i18n
710leo Jul 7, 2026
872de8f
feat(i18n): add language fallback chain and complete ja/ru/hk locales
710leo Jul 7, 2026
8b6b392
fix(i18n): backfill Chinese-key entries into zh_CN and complete local…
710leo Jul 7, 2026
471aa71
chore(i18n): remove orphan locale keys not referenced by any code
710leo Jul 7, 2026
23aac78
refactor(i18n): replace hardcoded strings with translation keys
710leo Jul 7, 2026
32fbc85
Merge remote-tracking branch 'origin/main' into optimize-i18n
710leo Jul 8, 2026
30e8133
fix(i18n): resolve raw-key rendering on core pages
710leo Jul 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions scripts/check_locale_keys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/**
* 脚本执行注意事项(同 generate_all_locales.ts):
* npx ts-node scripts/check_locale_keys.ts
*
* 以 zh_CN 为基准,检查每个 locale 目录:
* 1. 其他语言文件相对 zh_CN 缺失的 key
* 2. en_US 中 value 仍含中文(疑似未翻译)的 key
*
* 语言文件(zh_CN.ts / en_US.ts 等)约定为无外部依赖的纯对象模块,
* 这里直接 transpile 后求值,require 一律返回空对象兜底。
*/

import fs from 'fs';
import path from 'path';
import * as glob from 'glob';
import ts from 'typescript';
import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const LANGS = ['en_US', 'zh_HK', 'ja_JP', 'ru_RU'];
const CJK_RE = /[一-龥]/;

function loadLocale(file: string): any {
const source = fs.readFileSync(file, 'utf8');
const js = ts.transpileModule(source, {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2019 },
}).outputText;
const module = { exports: {} as any };
new Function('module', 'exports', 'require', js)(module, module.exports, () => ({}));
return module.exports.default ?? module.exports;
}

function flatten(obj: any, prefix = '', result: { [key: string]: any } = {}) {
for (const key of Object.keys(obj || {})) {
const value = obj[key];
const fullKey = prefix ? `${prefix}.${key}` : key;
if (value && typeof value === 'object' && !Array.isArray(value)) {
flatten(value, fullKey, result);
} else {
result[fullKey] = value;
}
}
return result;
}

const zhFiles = glob
.sync(path.resolve(__dirname, '../src/**/{locale,locales}/zh_CN.ts'))
.filter((file) => !file.includes('/plus/'))
.sort();

let totalMissing = 0;
let totalUntranslated = 0;

for (const zhFile of zhFiles) {
const dir = path.dirname(zhFile);
const relDir = path.relative(path.resolve(__dirname, '..'), dir);
const zhKeys = flatten(loadLocale(zhFile));

const problems: string[] = [];
for (const lang of LANGS) {
const langFile = path.join(dir, `${lang}.ts`);
if (!fs.existsSync(langFile)) {
problems.push(` [${lang}] 文件缺失`);
continue;
}
const langKeys = flatten(loadLocale(langFile));
const missing = Object.keys(zhKeys).filter((key) => !(key in langKeys));
if (missing.length) {
if (lang === 'en_US') totalMissing += missing.length;
problems.push(` [${lang}] 缺失 ${missing.length} 个 key: ${missing.join(', ')}`);
}
if (lang === 'en_US') {
const untranslated = Object.keys(langKeys).filter((key) => typeof langKeys[key] === 'string' && CJK_RE.test(langKeys[key]));
if (untranslated.length) {
totalUntranslated += untranslated.length;
problems.push(` [${lang}] value 含中文 ${untranslated.length} 个 key: ${untranslated.join(', ')}`);
}
}
}

if (problems.length) {
console.log(relDir);
problems.forEach((problem) => console.log(problem));
}
}

console.log(`\n共 ${zhFiles.length} 个 locale 目录;en_US 缺失 ${totalMissing} 个 key,value 含中文 ${totalUntranslated} 个 key`);
export {};
6 changes: 2 additions & 4 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,6 @@ import React, { useEffect, useState, createContext, useRef } from 'react';
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
// Modal 会被注入的代码所使用,请不要删除
import { ConfigProvider, Modal, Spin } from 'antd';
import zhCN from 'antd/lib/locale/zh_CN';
import enUS from 'antd/lib/locale/en_US';
import ruRU from 'antd/lib/locale/ru_RU';
import 'antd/dist/antd.less';
import { useTranslation } from 'react-i18next';
import _ from 'lodash';
Expand All @@ -34,6 +31,7 @@ import { getCleanBusinessGroupIds, getDefaultBusiness, getVaildBusinessGroup } f
import { IRawTimeRange } from '@/components/TimeRangePicker';
import { getN9eConfig } from '@/pages/siteSettings/services';
import { getDarkMode, updateDarkMode } from '@/utils/darkMode';
import { getAntdLocale } from '@/utils/antdLocale';
import { AiChatProvider, AiChatContainer } from '@/components/AiChatNG';
import HocRenderer from './components/HocRenderer';
import HeaderMenu from './components/SideMenu';
Expand Down Expand Up @@ -341,7 +339,7 @@ function App() {
<AiChatProvider>
<div className='App'>
<CommonStateContext.Provider value={commonState}>
<ConfigProvider locale={i18n.language == 'en_US' ? enUS : i18n.language == 'ru_RU' ? ruRU : zhCN}>
<ConfigProvider locale={getAntdLocale(i18n.language)}>
<Router
getUserConfirmation={(message, callback) => {
if (message === 'CUSTOM') return;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import { Button, Select } from 'antd';
import { CheckCircleOutlined, ProfileOutlined } from '@ant-design/icons';
import i18next from 'i18next';
import { useTranslation } from 'react-i18next';
import { NAME_SPACE } from '../constants';
import ContentCard from './ContentCard';
Expand Down Expand Up @@ -51,10 +52,14 @@ function getDefaultCandidateIds(candidates?: IFormSelectCandidate[]) {

function buildContentText(params: { busiGroupName?: string; datasourceName?: string; teamNames?: string[] }) {
const { busiGroupName, datasourceName, teamNames } = params;
const isZh = i18next.language?.startsWith('zh');
const colon = isZh ? ':' : ': ';
const separator = isZh ? '、' : ', ';
const label = (key: string) => i18next.t(`${NAME_SPACE}:form_select.${key}`);
const parts: string[] = [];
if (busiGroupName) parts.push(`业务组:${busiGroupName}`);
if (datasourceName) parts.push(`数据源:${datasourceName}`);
if (teamNames?.length) parts.push(`团队:${teamNames.join('、')}`);
if (busiGroupName) parts.push(`${label('busi_group')}${colon}${busiGroupName}`);
if (datasourceName) parts.push(`${label('datasource')}${colon}${datasourceName}`);
if (teamNames?.length) parts.push(`${label('team')}${colon}${teamNames.join(separator)}`);
return parts.join(' ');
}

Expand Down
2 changes: 1 addition & 1 deletion src/components/Code/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export default function Code(props: Props) {
<div className='code-area'>
<span className='code-text'>{children}</span>
<span className='copy-btn' onClick={handleCopy}>
{copied ? t('复制成功') : t('复制')}
{copied ? t('common:copied') : t('common:btn.copy2')}
</span>
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion src/components/DeviceSelect/HostSelect/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export default function index(props: Props) {
return (
<div key={field.key}>
<Space align='baseline'>
{idx > 0 && <div className='alert-rule-host-condition-tips'></div>}
{idx > 0 && <div className='alert-rule-host-condition-tips'>{t('host.and')}</div>}
<Form.Item {...field} name={[field.name, 'key']} rules={[{ required: true, message: 'Missing key' }]}>
<Select
style={{ minWidth: idx > 0 ? 100 : 142 }}
Expand Down
1 change: 1 addition & 0 deletions src/components/DeviceSelect/locale/en_US.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const en_US = {
host: {
and: 'AND',
title: 'Host filter',
key: {
all_hosts: 'All hosts',
Expand Down
1 change: 1 addition & 0 deletions src/components/DeviceSelect/locale/ja_JP.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const ja_JP = {
host: {
and: 'かつ',
title: 'ホストフィルタ',
key: {
all_hosts: 'すべてのホスト',
Expand Down
1 change: 1 addition & 0 deletions src/components/DeviceSelect/locale/ru_RU.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const ru_RU = {
host: {
and: 'И',
title: 'Фильтрация хостов',
key: {
all_hosts: 'Все хосты',
Expand Down
1 change: 1 addition & 0 deletions src/components/DeviceSelect/locale/zh_CN.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const zh_CN = {
host: {
and: '且',
title: '机器筛选',
key: {
all_hosts: '全部机器',
Expand Down
1 change: 1 addition & 0 deletions src/components/DeviceSelect/locale/zh_HK.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const zh_HK = {
host: {
and: '且',
title: '機器篩選',
key: {
all_hosts: '全部機器',
Expand Down
11 changes: 2 additions & 9 deletions src/components/DocumentDrawer/Document.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import MDEditor from '@uiw/react-md-editor';

import { CommonStateContext } from '@/App';
import { IS_ENT } from '@/utils/constant';
import { DOC_URL_LANG_SUFFIX } from '@/utils/docUrl';

import './style.less';

Expand All @@ -14,14 +15,6 @@ interface Props {
type?: 'md' | 'iframe';
}

const filenameMap = {
zh_CN: '',
zh_HK: '_hk',
en_US: '_en',
ja_JP: '_en',
ru_RU: '_en',
};

export default function index(props: Props) {
const { i18n } = useTranslation();
const { darkMode } = useContext(CommonStateContext);
Expand Down Expand Up @@ -76,7 +69,7 @@ export default function index(props: Props) {
{type === 'iframe' && (
<Spin spinning={loading} wrapperClassName='n9e-document-drawer-iframe-loading'>
<iframe
src={`${realDocumentPath}${filenameMap[i18n.language]}/?onlyContent&theme=${darkMode ? 'dark' : 'light'}`}
src={`${realDocumentPath}${DOC_URL_LANG_SUFFIX[i18n.language] || ''}/?onlyContent&theme=${darkMode ? 'dark' : 'light'}`}
style={{ width: '100%', height: '100%', border: '0 none', visibility: loading ? 'hidden' : 'visible' }}
onLoad={() => {
setLoading(false);
Expand Down
13 changes: 3 additions & 10 deletions src/components/DocumentDrawer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import MDEditor from '@uiw/react-md-editor';
import { useTranslation } from 'react-i18next';

import { IS_ENT } from '@/utils/constant';
import { DOC_URL_LANG_SUFFIX } from '@/utils/docUrl';

import ModalHOC, { ModalWrapProps } from '../ModalHOC';
import Document from './Document';
Expand All @@ -24,14 +25,6 @@ interface Props {
onClose?: (destroy: () => void) => void;
}

const filenameMap = {
zh_CN: '',
zh_HK: '_hk',
en_US: '_en',
ja_JP: '_en',
ru_RU: '_en',
};

function index(props: Props & ModalWrapProps) {
const { t } = useTranslation();
const { visible, destroy, title, width = '60%', documentPath, onClose, type = 'md', zIndex, anchor } = props;
Expand Down Expand Up @@ -77,7 +70,7 @@ function index(props: Props & ModalWrapProps) {
<Space>
{title}
{type === 'iframe' && (
<a target='_blank' href={`${realDocumentPath}${filenameMap[language]}/${anchor || ''}`} className='text-[12px]'>
<a target='_blank' href={`${realDocumentPath}${DOC_URL_LANG_SUFFIX[language] || ''}/${anchor || ''}`} className='text-[12px]'>
<Space size={4}>
{t('common:more_document_link')}
<ExportOutlined />
Expand Down Expand Up @@ -112,7 +105,7 @@ function index(props: Props & ModalWrapProps) {
{type === 'iframe' && (
<Spin spinning={loading} wrapperClassName='n9e-document-drawer-iframe-loading'>
<iframe
src={`${realDocumentPath}${filenameMap[language]}/?onlyContent&theme=${darkMode ? 'dark' : 'light'}${anchor || ''}`}
src={`${realDocumentPath}${DOC_URL_LANG_SUFFIX[language] || ''}/?onlyContent&theme=${darkMode ? 'dark' : 'light'}${anchor || ''}`}
style={{ width: '100%', height: '100%', border: '0 none', visibility: loading ? 'hidden' : 'visible' }}
onLoad={() => {
setLoading(false);
Expand Down
4 changes: 2 additions & 2 deletions src/components/InputEnlarge/LinkBuilder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export default function LinkBuilder({
mappingParamsArr?: ILogMappingParams[];
}) {
const [form] = Form.useForm();
const { t } = useTranslation();
const { t } = useTranslation('inputEnlarge');

const handleClose = () => {
onClose();
Expand Down Expand Up @@ -69,7 +69,7 @@ export default function LinkBuilder({
};

return (
<Modal width={600} bodyStyle={{ padding: 12 }} title={t('跳转链接生成器')} visible={visible} okText={t('生成')} onCancel={handleClose} onOk={handleOk}>
<Modal width={600} bodyStyle={{ padding: 12 }} title={t('linkBuilderTip')} visible={visible} okText={t('生成')} onCancel={handleClose} onOk={handleOk}>
<Form layout='vertical' form={form}>
<Form.Item label={t('下钻到')} name={['target_type']} initialValue={Type.Custom}>
<Select style={{ width: '100%' }}>
Expand Down
4 changes: 4 additions & 0 deletions src/components/InputEnlarge/locale/en_US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,9 @@ const en_US = {
预览结果: 'Preview result',
请输入地址: 'Please input address',
请输入: 'Please input',
生成: 'Generate',
下钻到: 'Drill down to',
自定义链接: 'Custom link',
日志检索: 'Log explorer',
};
export default en_US;
4 changes: 4 additions & 0 deletions src/components/InputEnlarge/locale/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import en_US from './en_US';
import zh_HK from './zh_HK';
import zh_CN from './zh_CN';
import ja_JP from './ja_JP';
import ru_RU from './ru_RU';

const resources = {
inputEnlarge: {
en_US,
zh_HK,
zh_CN,
ja_JP,
ru_RU,
},
};

Expand Down
26 changes: 26 additions & 0 deletions src/components/InputEnlarge/locale/ja_JP.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const ja_JP = {
customTip: `ログのラベルはすべて変数として参照できます。例:$key1、$key2、$a.b
<1 />組み込み変数:
<1 /><2 />開始・終了時刻:$__from と $__to、デフォルトの単位はミリ秒
<1 /><2 />フォーマットが必要な場合は $__time_format__ を使用できます。形式は unix、YYYY-MM-DD HH:mm:ss など
<1 /><2 />本システムのアドレス:$local_url(プロトコルとドメインを含む)`,
linkBuilderTip: 'ジャンプリンクジェネレーター',
时间范围: '時間範囲',
继承当前查询时间: '現在のクエリ時間を引き継ぐ',
业务组: 'ビジネスグループ',
请选择: '選択してください',
请选择业务组: 'ビジネスグループを選択してください',
仪表盘: 'ダッシュボード',
请选择仪表盘: 'ダッシュボードを選択してください',
设置仪表盘变量: 'ダッシュボード変数を設定',
可选变量: '利用可能な変数',
预览结果: 'プレビュー結果',
请输入地址: 'アドレスを入力してください',
请输入: '入力してください',
生成: '生成',
下钻到: 'ドリルダウン先',
自定义链接: 'カスタムリンク',
日志检索: 'ログ検索',
};

export default ja_JP;
26 changes: 26 additions & 0 deletions src/components/InputEnlarge/locale/ru_RU.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const ru_RU = {
customTip: `Все метки логов можно использовать как переменные, например $key1, $key2, $a.b
<1 />Встроенные переменные:
<1 /><2 />Время начала и окончания: $__from и $__to, единица по умолчанию — миллисекунды
<1 /><2 />Для форматирования можно использовать $__time_format__, форматы включают unix, YYYY-MM-DD HH:mm:ss и др.
<1 /><2 />Адрес системы: $local_url, включая протокол и домен`,
linkBuilderTip: 'Генератор ссылок перехода',
时间范围: 'Диапазон времени',
继承当前查询时间: 'Наследовать текущее время запроса',
业务组: 'Бизнес-группа',
请选择: 'Выберите',
请选择业务组: 'Выберите бизнес-группу',
仪表盘: 'Панель мониторинга',
请选择仪表盘: 'Выберите панель мониторинга',
设置仪表盘变量: 'Настроить переменные панели',
可选变量: 'Доступные переменные',
预览结果: 'Предпросмотр результата',
请输入地址: 'Введите адрес',
请输入: 'Введите',
生成: 'Сгенерировать',
下钻到: 'Перейти к',
自定义链接: 'Пользовательская ссылка',
日志检索: 'Поиск логов',
};

export default ru_RU;
16 changes: 16 additions & 0 deletions src/components/InputEnlarge/locale/zh_CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,22 @@ const zh_CN = {
<1 /><2 />如需格式化可带入$__time_format__,格式包括 unix YYYY-MM-DD HH:mm:ss等
<1 /><2 />本系统地址:$local_url,包含了协议和域名`,
linkBuilderTip: '跳转链接生成器',
时间范围: '时间范围',
继承当前查询时间: '继承当前查询时间',
业务组: '业务组',
请选择: '请选择',
请选择业务组: '请选择业务组',
仪表盘: '仪表盘',
请选择仪表盘: '请选择仪表盘',
设置仪表盘变量: '设置仪表盘变量',
可选变量: '可选变量',
预览结果: '预览结果',
请输入地址: '请输入地址',
请输入: '请输入',
生成: '生成',
下钻到: '下钻到',
自定义链接: '自定义链接',
日志检索: '日志检索',
};

export default zh_CN;
Loading