(null);
@@ -247,7 +260,10 @@ export default function KnowledgePage() {
try {
const response = await knowledgeDocsApi.list();
- setDocs(normalizeDocs(response));
+ setDocs(normalizeDocs(response, {
+ fallbackDocumentName: (count) => t('knowledge.fallback.documentName', { count }),
+ fallbackFileType: t('knowledge.fallback.fileType'),
+ }));
} catch (caughtError) {
setDocs([]);
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
@@ -267,7 +283,7 @@ export default function KnowledgePage() {
setError(null);
try {
- const base64 = await readFileAsBase64(file);
+ const base64 = await readFileAsBase64(file, t('knowledge.errors.readFile'));
await knowledgeDocsApi.upload({
fileName: file.name,
base64,
@@ -327,17 +343,16 @@ export default function KnowledgePage() {
}
onClose={closeDeleteDialog}
onConfirm={() => {
@@ -347,12 +362,12 @@ export default function KnowledgePage() {
{
void handleDelete(doc);
}}
- deleteLabel={t('knowledge.table.delete', undefined, 'Delete')}
+ deleteLabel={t('knowledge.table.delete')}
deletingLabel={t('knowledge.status.deleting')}
fileNameLabel={t('knowledge.table.name')}
sizeLabel={t('knowledge.table.size')}
@@ -404,7 +419,7 @@ export default function KnowledgePage() {
typeLabel={t('knowledge.table.type')}
actionLabel={t('knowledge.table.actions')}
formatBytes={formatBytes}
- formatDateTime={formatDateTime}
+ formatDateTime={(value) => formatDateTime(value, locale)}
trashIcon={}
/>
)}
diff --git a/src/pages/Login/auth.ts b/src/pages/Login/auth.ts
index cca6b78..9676ccb 100644
--- a/src/pages/Login/auth.ts
+++ b/src/pages/Login/auth.ts
@@ -1,4 +1,5 @@
import { generateUUID } from '../../utils/generateUUID';
+import { t } from '../../i18n';
import { persistAuthTokens } from '../../router/auth-session';
export type LoginFormValues = {
@@ -35,11 +36,11 @@ function buildUrl(path: string): string {
function normalizeLoginResponse(payload: LoginResponse): LoginResponse {
if (typeof payload.code === 'number' && payload.code !== 200) {
- throw new Error(payload.msg || '登录失败,请稍后重试');
+ throw new Error(payload.msg || t('login.submitFailed'));
}
if (!payload.access_token) {
- throw new Error(String(payload.msg || '登录失败,未返回 access_token'));
+ throw new Error(String(payload.msg || t('login.errors.missingAccessToken')));
}
return payload;
@@ -76,7 +77,7 @@ export async function loginWithPassword(form: LoginFormValues): Promise ({}))) as LoginResponse;
if (!response.ok) {
- throw new Error(String(payload.msg || `登录失败 (${response.status})`));
+ throw new Error(String(payload.msg || t('login.errors.submitStatus', { status: response.status })));
}
return normalizeLoginResponse(payload);
@@ -87,7 +88,7 @@ export function persistLoginTokens(payload: LoginResponse): void {
const refreshToken = String(payload.refresh_token ?? '');
if (!accessToken) {
- throw new Error('登录失败,未返回 access_token');
+ throw new Error(t('login.errors.missingAccessToken'));
}
persistAuthTokens(accessToken, refreshToken);
diff --git a/src/pages/Models/copy.ts b/src/pages/Models/copy.ts
index 7ca9eba..316ab34 100644
--- a/src/pages/Models/copy.ts
+++ b/src/pages/Models/copy.ts
@@ -1,48 +1,12 @@
-import { useLocale } from '../../i18n';
-import type { LanguageCode } from '../../types/runtime';
-import { MODELS_MESSAGES, type MessageTree } from './messages';
+import { useI18n } from '../../i18n';
type Primitive = string | number;
type InterpolationParams = Record;
export type ModelsTranslate = (path: string, params?: InterpolationParams, fallback?: string) => string;
-function normalizeModelsPath(path: string): string {
- return path.startsWith('models.') ? path.slice('models.'.length) : path;
-}
-
-function lookupMessage(source: MessageTree, path: string): unknown {
- return path.split('.').reduce((current, segment) => {
- if (!current || typeof current !== 'object') return undefined;
- return (current as Record)[segment];
- }, source);
-}
-
-function interpolate(template: string, params?: InterpolationParams): string {
- if (!params) return template;
-
- return template.replace(/\{(\w+)\}/g, (_match, token) => {
- const value = params[token];
- return typeof value === 'undefined' ? `{${token}}` : String(value);
- });
-}
-
-function createModelsTranslate(locale: LanguageCode): ModelsTranslate {
- return (path, params, fallback) => {
- const normalizedPath = normalizeModelsPath(path);
- const translated = lookupMessage(MODELS_MESSAGES[locale] ?? MODELS_MESSAGES.en, normalizedPath)
- ?? lookupMessage(MODELS_MESSAGES.en, normalizedPath);
-
- if (typeof translated === 'string' || typeof translated === 'number') {
- return interpolate(String(translated), params);
- }
-
- return fallback ?? path;
- };
-}
-
export function useModelsCopy(): ModelsTranslate {
- const locale = useLocale();
+ const { t } = useI18n();
- return createModelsTranslate(locale);
+ return (path, params, fallback) => t(path, params, fallback);
}
diff --git a/src/pages/Models/messages.ts b/src/pages/Models/messages.ts
deleted file mode 100644
index 9d00b60..0000000
--- a/src/pages/Models/messages.ts
+++ /dev/null
@@ -1,314 +0,0 @@
-import type { LanguageCode } from '../../types/runtime';
-
-type Primitive = string | number;
-type MessageTree = {
- [key: string]: Primitive | MessageTree;
-};
-
-const EN_MODELS_MESSAGES: MessageTree = {
- page: {
- title: 'Models Configuration',
- subtitle: 'Configure your AI providers and view token usage.',
- },
- common: {
- closeDialog: 'Close dialog',
- unknownModel: 'Unknown Model',
- unknown: 'Unknown',
- empty: 'Empty',
- loading: 'Loading...',
- ai: 'AI',
- },
- providers: {
- title: 'AI Providers',
- subtitle: 'Manage your AI models and API keys.',
- add: 'Add Provider',
- loading: 'Loading provider settings...',
- defaultBadge: 'Default',
- configured: 'Configured',
- notConfigured: 'Not Configured',
- setDefault: 'Set Default',
- edit: 'Edit',
- delete: 'Delete',
- empty: 'No providers configured yet. Click "Add Provider" to start.',
- confirmDelete: 'Are you sure you want to delete this provider?',
- notices: {
- loadError: 'Failed to load provider settings.',
- defaultSuccess: 'Default provider set successfully.',
- defaultError: 'Failed to set the default provider.',
- deleteSuccess: 'Provider deleted successfully.',
- deleteError: 'Failed to delete this provider.',
- saveSuccess: 'Provider saved successfully.',
- saveError: 'Failed to save this provider.',
- },
- picker: {
- title: 'Add AI Provider',
- subtitle: 'Configure a new provider for model access and API routing.',
- },
- editor: {
- addTitle: 'Add AI Provider',
- editTitle: 'Edit Provider',
- subtitle: 'Configure the provider account details stored on this device.',
- switchProvider: 'Switch provider',
- viewDocs: 'View docs',
- displayName: 'Display Name',
- displayNamePlaceholder: 'Provider name',
- apiKey: 'API Key',
- apiKeyPlaceholder: 'Enter API Key',
- apiKeyHint: 'Your API key is stored locally on this machine.',
- baseUrl: 'Base URL (Optional)',
- baseUrlPlaceholder: 'https://api.example.com/v1',
- defaultModel: 'Default Model (Optional)',
- defaultModelPlaceholder: 'gpt-5.4',
- saving: 'Saving...',
- save: 'Save',
- add: 'Add Provider',
- },
- },
- usage: {
- title: 'Token Usage History',
- loading: 'Loading usage history...',
- loadError: 'Failed to load usage history.',
- empty: 'No usage history available.',
- emptyWindow: 'No usage history in the selected time window.',
- groupByModel: 'By Model',
- groupByTime: 'By Time',
- window7d: 'Last 7 Days',
- window30d: 'Last 30 Days',
- windowAll: 'All Time',
- showingRecords: 'Showing {count} records',
- chart: {
- total: 'Total',
- input: 'Input',
- output: 'Output',
- cache: 'Cache',
- },
- row: {
- noUsageInfo: 'No usage info',
- errorParsingUsage: 'Error parsing usage',
- input: 'Input: {count}',
- output: 'Output: {count}',
- cacheRead: 'Cache Read: {count}',
- cacheWrite: 'Cache Write: {count}',
- noUsageReported: 'No usage reported',
- parseError: 'Parse error',
- viewContent: 'View Content',
- error: 'Error',
- },
- pagination: {
- pageOf: 'Page {page} of {total}',
- prev: 'Prev',
- next: 'Next',
- },
- requestContent: {
- title: 'Request Content',
- },
- },
-};
-
-const ZH_MODELS_MESSAGES: MessageTree = {
- page: {
- title: '模型配置',
- subtitle: '统一配置 AI 服务商,并查看模型 Token 使用情况。',
- },
- common: {
- closeDialog: '关闭弹窗',
- unknownModel: '未知模型',
- unknown: '未知',
- empty: '暂无数据',
- loading: '加载中...',
- ai: 'AI',
- },
- providers: {
- title: 'AI 服务商',
- subtitle: '管理你的 AI 模型与 API Key。',
- add: '添加服务商',
- loading: '正在加载服务商配置...',
- defaultBadge: '默认',
- configured: '已配置',
- notConfigured: '未配置',
- setDefault: '设为默认',
- edit: '编辑',
- delete: '删除',
- empty: '暂未配置服务商。点击“添加服务商”开始。',
- confirmDelete: '确定删除这个服务商吗?',
- notices: {
- loadError: '加载服务商配置失败。',
- defaultSuccess: '默认服务商设置成功。',
- defaultError: '设置默认服务商失败。',
- deleteSuccess: '服务商删除成功。',
- deleteError: '删除服务商失败。',
- saveSuccess: '服务商保存成功。',
- saveError: '保存服务商失败。',
- },
- picker: {
- title: '添加 AI 服务商',
- subtitle: '新增一个服务商,用于模型访问和 API 路由。',
- },
- editor: {
- addTitle: '添加 AI 服务商',
- editTitle: '编辑服务商',
- subtitle: '配置保存在当前设备上的服务商账号信息。',
- switchProvider: '切换服务商',
- viewDocs: '查看文档',
- displayName: '显示名称',
- displayNamePlaceholder: '服务商名称',
- apiKey: 'API Key',
- apiKeyPlaceholder: '输入 API Key',
- apiKeyHint: '你的 API Key 只会保存在当前设备本地。',
- baseUrl: 'Base URL(可选)',
- baseUrlPlaceholder: 'https://api.example.com/v1',
- defaultModel: '默认模型(可选)',
- defaultModelPlaceholder: 'gpt-5.4',
- saving: '保存中...',
- save: '保存',
- add: '添加服务商',
- },
- },
- usage: {
- title: 'Token 使用历史',
- loading: '正在加载使用历史...',
- loadError: '加载使用历史失败。',
- empty: '暂无使用历史。',
- emptyWindow: '所选时间范围内暂无使用历史。',
- groupByModel: '按模型',
- groupByTime: '按时间',
- window7d: '最近 7 天',
- window30d: '最近 30 天',
- windowAll: '全部时间',
- showingRecords: '共显示 {count} 条记录',
- chart: {
- total: '总计',
- input: '输入',
- output: '输出',
- cache: '缓存',
- },
- row: {
- noUsageInfo: '无用量信息',
- errorParsingUsage: '用量解析错误',
- input: '输入:{count}',
- output: '输出:{count}',
- cacheRead: '缓存读取:{count}',
- cacheWrite: '缓存写入:{count}',
- noUsageReported: '未上报用量',
- parseError: '解析失败',
- viewContent: '查看内容',
- error: '错误',
- },
- pagination: {
- pageOf: '第 {page} / {total} 页',
- prev: '上一页',
- next: '下一页',
- },
- requestContent: {
- title: '请求内容',
- },
- },
-};
-
-const JA_MODELS_MESSAGES: MessageTree = {
- page: {
- title: 'モデル設定',
- subtitle: 'AI プロバイダーを設定し、モデルの Token 使用状況を確認できます。',
- },
- common: {
- closeDialog: 'ダイアログを閉じる',
- unknownModel: '不明なモデル',
- unknown: '不明',
- empty: 'データなし',
- loading: '読み込み中...',
- ai: 'AI',
- },
- providers: {
- title: 'AI プロバイダー',
- subtitle: 'AI モデルと API キーを管理します。',
- add: 'プロバイダーを追加',
- loading: 'プロバイダー設定を読み込み中...',
- defaultBadge: 'デフォルト',
- configured: '設定済み',
- notConfigured: '未設定',
- setDefault: 'デフォルトに設定',
- edit: '編集',
- delete: '削除',
- empty: 'プロバイダーはまだ設定されていません。「プロバイダーを追加」をクリックして開始してください。',
- confirmDelete: 'このプロバイダーを削除してもよろしいですか?',
- notices: {
- loadError: 'プロバイダー設定の読み込みに失敗しました。',
- defaultSuccess: 'デフォルトのプロバイダーを設定しました。',
- defaultError: 'デフォルトのプロバイダー設定に失敗しました。',
- deleteSuccess: 'プロバイダーを削除しました。',
- deleteError: 'プロバイダーの削除に失敗しました。',
- saveSuccess: 'プロバイダーを保存しました。',
- saveError: 'プロバイダーの保存に失敗しました。',
- },
- picker: {
- title: 'AI プロバイダーを追加',
- subtitle: 'モデルアクセスと API ルーティング用の新しいプロバイダーを設定します。',
- },
- editor: {
- addTitle: 'AI プロバイダーを追加',
- editTitle: 'プロバイダーを編集',
- subtitle: 'このデバイスに保存されるプロバイダーアカウント情報を設定します。',
- switchProvider: 'プロバイダーを切り替え',
- viewDocs: 'ドキュメントを見る',
- displayName: '表示名',
- displayNamePlaceholder: 'プロバイダー名',
- apiKey: 'API キー',
- apiKeyPlaceholder: 'API キーを入力',
- apiKeyHint: 'API キーはこの端末にのみ保存されます。',
- baseUrl: 'Base URL(任意)',
- baseUrlPlaceholder: 'https://api.example.com/v1',
- defaultModel: 'デフォルトモデル(任意)',
- defaultModelPlaceholder: 'gpt-5.4',
- saving: '保存中...',
- save: '保存',
- add: 'プロバイダーを追加',
- },
- },
- usage: {
- title: 'Token 使用履歴',
- loading: '使用履歴を読み込み中...',
- loadError: '使用履歴の読み込みに失敗しました。',
- empty: '使用履歴はありません。',
- emptyWindow: '選択した期間には使用履歴がありません。',
- groupByModel: 'モデル別',
- groupByTime: '時間別',
- window7d: '過去 7 日',
- window30d: '過去 30 日',
- windowAll: '全期間',
- showingRecords: '{count} 件を表示',
- chart: {
- total: '合計',
- input: '入力',
- output: '出力',
- cache: 'キャッシュ',
- },
- row: {
- noUsageInfo: '使用量情報なし',
- errorParsingUsage: '使用量の解析エラー',
- input: '入力: {count}',
- output: '出力: {count}',
- cacheRead: 'キャッシュ読込: {count}',
- cacheWrite: 'キャッシュ書込: {count}',
- noUsageReported: '使用量が報告されていません',
- parseError: '解析エラー',
- viewContent: '内容を見る',
- error: 'エラー',
- },
- pagination: {
- pageOf: '{page} / {total} ページ',
- prev: '前へ',
- next: '次へ',
- },
- requestContent: {
- title: 'リクエスト内容',
- },
- },
-};
-
-export const MODELS_MESSAGES: Record = {
- en: EN_MODELS_MESSAGES,
- zh: ZH_MODELS_MESSAGES,
- ja: JA_MODELS_MESSAGES,
-};
-
-export type { MessageTree };
diff --git a/src/pages/Scripts/components/ScriptCard.tsx b/src/pages/Scripts/components/ScriptCard.tsx
index 8b483dc..5de4ef2 100644
--- a/src/pages/Scripts/components/ScriptCard.tsx
+++ b/src/pages/Scripts/components/ScriptCard.tsx
@@ -1,4 +1,5 @@
import type { AutomationScript, ScriptExecutionResult } from '../../../lib/script-types';
+import { useI18n, type LanguageCode } from '../../../i18n';
import {
CheckIcon,
DeleteIcon,
@@ -10,6 +11,8 @@ import {
} from './icons';
import { Switch } from './ScriptDialogs';
+type Translate = ReturnType['t'];
+
type ScriptCardProps = {
script: AutomationScript;
executionLog?: ScriptExecutionResult;
@@ -25,27 +28,28 @@ type ScriptCardProps = {
onTest: () => Promise;
};
-const CHANNEL_LABELS: Record = {
- fliggy: '飞猪',
- meituan: '美团',
- douyin: '抖音',
- common: '通用',
+const CHANNEL_LABEL_KEYS: Record = {
+ fliggy: 'scripts.card.channels.fliggy',
+ meituan: 'scripts.card.channels.meituan',
+ douyin: 'scripts.card.channels.douyin',
+ common: 'scripts.card.channels.common',
};
-function formatRelativeTime(timestamp: string) {
+function formatRelativeTime(timestamp: string, locale: LanguageCode, t: Translate) {
const date = new Date(timestamp);
const diffMs = Date.now() - date.getTime();
const diffSec = Math.round(diffMs / 1000);
const diffMin = Math.round(diffSec / 60);
const diffHour = Math.round(diffMin / 60);
const diffDay = Math.round(diffHour / 24);
+ const relativeTime = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
- if (diffSec < 10) return '刚刚';
- if (diffMin < 1) return `${diffSec} 秒前`;
- if (diffHour < 1) return `${diffMin} 分钟前`;
- if (diffDay < 1) return `${diffHour} 小时前`;
- if (diffDay < 30) return `${diffDay} 天前`;
- return new Intl.DateTimeFormat('zh-CN', { month: 'short', day: 'numeric' }).format(date);
+ if (diffSec < 10) return t('scripts.card.relative.justNow');
+ if (diffMin < 1) return relativeTime.format(-diffSec, 'second');
+ if (diffHour < 1) return relativeTime.format(-diffMin, 'minute');
+ if (diffDay < 1) return relativeTime.format(-diffHour, 'hour');
+ if (diffDay < 30) return relativeTime.format(-diffDay, 'day');
+ return new Intl.DateTimeFormat(locale, { month: 'short', day: 'numeric' }).format(date);
}
function getExecutionPreview(executionLog?: ScriptExecutionResult) {
@@ -62,8 +66,9 @@ export default function ScriptCard({
onDelete,
onTest,
}: ScriptCardProps) {
+ const { t, locale } = useI18n();
const channelLabel = script.channel && script.channel !== 'common'
- ? (CHANNEL_LABELS[script.channel] || script.channel)
+ ? (CHANNEL_LABEL_KEYS[script.channel] ? t(CHANNEL_LABEL_KEYS[script.channel]) : script.channel)
: '';
return (
@@ -82,7 +87,7 @@ export default function ScriptCard({
'h-2 w-2 shrink-0 rounded-full',
script.enabled ? 'bg-green-500' : 'bg-[#99A0AE] dark:bg-gray-500',
].join(' ')}
- title={script.enabled ? '已启用' : '已停用'}
+ title={t(script.enabled ? 'scripts.card.status.enabled' : 'scripts.card.status.disabled')}
/>
@@ -106,22 +111,22 @@ export default function ScriptCard({
- {script.description || '暂未填写脚本描述。'}
+ {script.description || t('scripts.card.noDescription')}
{script.lastRun ? (
- 最近执行:{formatRelativeTime(script.lastRun.time)}
+ {t('scripts.card.lastRun', { time: formatRelativeTime(script.lastRun.time, locale, t) })}
{script.lastRun.success ? (
) : (
- 失败
+ {t('scripts.card.failed')}
)}
) : (
- 还没有执行记录
+ {t('scripts.card.neverRun')}
)}
{script.id}
@@ -149,7 +154,7 @@ export default function ScriptCard({
disabled={busyState?.testing}
>
- {busyState?.testing ? '测试中...' : '测试'}
+ {busyState?.testing ? t('scripts.card.testing') : t('scripts.card.test')}