diff --git a/.gitignore b/.gitignore index 3687ca98..1507928f 100644 --- a/.gitignore +++ b/.gitignore @@ -87,3 +87,4 @@ profile.cov # i18n 生成物(由 scripts/merge-i18n-fragments.mjs 从 fragments 生成) frontend/messages/zh-CN.json frontend/messages/en.json +frontend/messages/ru.json diff --git a/docs/changelog/index.md b/docs/changelog/index.md index 18da8d3c..7db5512c 100644 --- a/docs/changelog/index.md +++ b/docs/changelog/index.md @@ -17,6 +17,10 @@ sidebar: false ## [Unreleased] +- 新增完整俄语界面资源及服务端展示文案,支持俄语浏览器语言识别、原生语言名称、日期与数字格式、日历及数量复数形式;补齐图表标题、表单字段和无障碍按钮文案的国际化,内置错误页及离线页跟随界面语言,保留用户自定义内容。 +- 选择非默认语言时,在静态页面首次绘制前等待对应语言资源就绪,避免刷新或完整跳转时短暂显示英文。 +- 修复本地化审查发现的通知收件人误判,保留历史系统目标并正确解析同名普通用户;恢复推送模板变量与富文本标签,补齐公开错误和限流响应翻译,修正俄语技术示例、数量及术语,并在首次渲染、系统指标和日历无障碍标签中应用当前语言。界面脚本加载失败时提供独立的同语言重新加载入口,避免无限空白页。 + ### 🛠 修复 - 将界面默认语言调整为英文,保留已有语言偏好和中文语言资源。 diff --git a/docs/design/log-language.md b/docs/design/log-language.md index 48aa140a..8925f4b8 100644 --- a/docs/design/log-language.md +++ b/docs/design/log-language.md @@ -4,19 +4,22 @@ Server、Agent、后台任务、安装/卸载脚本以及浏览器 PoW 调试日志的固定文案使用英文。语言切换不会改变控制台日志或 OTel 异常信息,便于跨语言检索和排障。任务参数、文件名、用户配置和第三方错误保留原文,可能包含中文。 -前端继续使用 `next-intl`,以 `en` 为默认语言,保留 `zh-CN` 语言资源。当前阶段先整理英文基准文案并移除运行时中文硬编码,中文译文的进一步审校和统一验证安排在后续阶段。文案维护在 `frontend/messages/fragments`,通过 `node scripts/merge-i18n-fragments.mjs` 生成主消息包。技术示例与 HTML 使用原始消息读取,避免把代码中的花括号当作 ICU 参数。 +前端使用 `next-intl`,支持 `en`、`zh-CN` 和 `ru`,默认语言仍为 `en`。语言偏好按 `NEXT_LOCALE` Cookie、浏览器语言、默认语言的顺序解析,俄语区域变体(如 `ru-RU`)归一为 `ru`。文案维护在 `frontend/messages/fragments`,通过 `node scripts/merge-i18n-fragments.mjs` 生成主消息包,禁止手动修改生成的 `en.json`、`zh-CN.json` 和 `ru.json`。技术示例与 HTML 使用原始消息读取,避免把代码中的花括号当作 ICU 参数。 -语言选择器始终从各语言自己的资源读取名称:`English` 与 `中文`,不随当前界面语言翻译语言名称。 +语言选择器始终从各语言自己的资源读取名称:`English`、`中文`、`Русский`,不随当前界面语言翻译语言名称。日期、数字和日历跟随界面语言;语言切换不改变既有时区。俄语数量文案使用 ICU 的 `one`、`few`、`many` 和 `other` 分支。 + +静态导出仍预渲染英文。`` 中的独立启动脚本在正文绘制前读取语言偏好;若与预渲染语言不同,则暂时隐藏正文,Provider 提交对应语言资源后再显示,避免刷新时闪现英文。脚本不依赖应用 bundle,损坏的 Cookie 会回退到浏览器语言。脚本加载出错或等待超过 10 秒时,显示同语言的独立恢复提示和重新加载按钮;应用恢复后隐藏提示并清理等待监听,避免无限空白页。格式化使用的语言在 Provider 更新上下文前同步,已有数据的首次本地化渲染也使用正确的日期和数字格式。 不在 React 渲染上下文中的浏览器代码(如网络异常和纯表单工具)通过 `frontend/i18n/runtime.ts` 调用同一套 `next-intl` 文案。消息在使用时解析语言,不在模块导入时固定语言。API 客户端发送当前界面语言的 `Accept-Language`。 ## 服务端展示边界 -`pkg/i18n/locales` 保留内置服务端文案的英文和中文版本,英文同时是运行时源码中的基准文案。展示接口依次采用 `NEXT_LOCALE` Cookie、`Accept-Language`,没有支持的语言时回退英文。 +`pkg/i18n/locales` 保留内置服务端文案的英文、中文和俄语版本,英文同时是运行时源码中的基准文案。展示接口依次采用 `NEXT_LOCALE` Cookie、`Accept-Language`,没有支持的语言时回退英文。 - 错误中间件先记录原始英文异常及 OTel 状态,再翻译 HTTP 响应中的 `error_msg`。登录流程使用的 `smtp_invalid:` 和 `need_email_code:` 控制前缀保持不变。 - 任务类型、参数说明、执行记录和内置通知元数据使用请求内的副本进行翻译,避免修改共享注册表或持久化记录。 - 默认定时任务名称可翻译;管理员自定义名称、通知接收目标和已保存的消息模板保留原文。 +- 通知接收目标只识别既有技术标识 `system`、`0` 和历史别名 `系统`;显示译文 `System`、`Система` 不会扩大保留目标集合,也不会把同名普通用户改投给管理员。 - 新任务执行详情保存英文系统文案。历史中文文本通过已知的完整文案或格式模板在读取时展示为英文,不执行数据库回写。 格式模板只识别已登记的系统消息,并逐字保留捕获到的参数;时间戳也保留。未知消息原样返回。此兼容机制只用于系统拥有的展示字段,不能用来翻译任意用户数据,也不能通过比较翻译后的文本决定业务状态。任务重试和存储配置校验使用错误类型进行分类。 @@ -27,7 +30,9 @@ Server、Agent、后台任务、安装/卸载脚本以及浏览器 PoW 调试日 历史 SQL 迁移、测试样例、中文项目文档以及用户已经保存的数据不属于待替换的运行时固定文案。离线页面和通知模板一旦由管理员选择、编辑并保存,就作为用户数据处理,不随界面语言切换而回写。 -系统配置描述和内置邮件模板的名称、说明可能来自历史 seed。管理界面仅在配置键或内置模板键已登记、且文本与已知中英文默认值完全一致时,使用 `builtin-metadata` 语言分片展示;未知键、自定义描述与非内置模板保持原文。编辑表单保留原始状态,单纯保存其他字段不会把展示翻译写回数据库;邮件主题和正文始终按已保存内容显示。 +系统配置描述和内置邮件模板的名称、说明可能来自历史 seed。管理界面仅在配置键或内置模板键已登记、且文本与已知语言默认值完全一致时,使用 `builtin-metadata` 语言分片展示;未知键、自定义描述与非内置模板保持原文。编辑表单保留原始状态,单纯保存其他字段不会把展示翻译写回数据库;邮件主题和正文始终按已保存内容显示。内置源站错误页和离线页在选择模板或空值预览时使用当前语言,已保存的自定义 HTML 保持不变。 + +系统状态 API 的诊断时长和容量字符串保留英文及技术单位,界面在展示边界转换为当前语言;原始字符串继续用于既有内存计算。数据库计数采用当前 `next-intl` 上下文格式化。日历使用 DayPicker 扩展语言包,同时翻译日期与导航、下拉框、选中日期等无障碍名称。 ## 校验与扩展 @@ -35,6 +40,6 @@ Server、Agent、后台任务、安装/卸载脚本以及浏览器 PoW 调试日 清单包含标点、表情、注释、文档、语言资源与历史迁移,不表示每个命中都需要替换。依赖、构建输出和私有环境文件不进入清单;二进制文件单独列出,不执行截图 OCR 或解包。生成的 Swagger 和中文文档站配置保留中文,既有上传分类键保留兼容性。字符集也不等同于语言,例如 Han 字符可能属于中文或日文。 -`pnpm check:i18n` 校验中英文键一致性,并检查 Go、TypeScript/TSX 和安装脚本中的中文运行时文本;注释、语言资源、测试样例和上述六个持久化键有明确边界。该检查同时接入 `make code-check` 和现有前端 CI。 +`pnpm check:i18n` 校验三个语言包的键、标签和 `$` 变量一致性,并检查 Go、TypeScript/TSX 和安装脚本中的汉字、日文假名、韩文及西里尔字母运行时文本;注释、语言资源、测试样例、六个上传持久化键及通知历史目标别名有明确边界。该检查同时接入 `make code-check` 和现有前端 CI。俄语资源测试还验证命名参数、真实 rich-text 渲染、ICU 格式、复数边界、可复制的限速语法及 HTML 模板占位符,服务端测试验证格式动词、模板变量和枚举值。技术标识(如 `10r/s`、`30m`、`true`、`and`)不随语言翻译。 新增语言时,补齐前端分片、语言配置与 Provider 消息映射,以及服务端同名语言目录项,并验证格式占位符一致。新增系统消息仍以英文作为源文案,在相应语言资源中补齐译文。新增数据协议应使用稳定标识与参数,不再引入以中文显示文案充当持久化键的设计。 diff --git a/frontend/app/(main)/access-logs/components/ip-analysis-panel.tsx b/frontend/app/(main)/access-logs/components/ip-analysis-panel.tsx index 1ac6fd9d..cd0d3661 100644 --- a/frontend/app/(main)/access-logs/components/ip-analysis-panel.tsx +++ b/frontend/app/(main)/access-logs/components/ip-analysis-panel.tsx @@ -368,6 +368,8 @@ export function IpAnalysisPanel({ */ initialHours?: number; }) { + const uiText = useTranslations('runtime'); + const t = useTranslations('accessLogs'); const trendChartConfig = useTrendChartConfig(); const [ipGroupOpen, setIpGroupOpen] = useState(false); @@ -576,32 +578,32 @@ export function IpAnalysisPanel({ {analysis ? (
diff --git a/frontend/app/(main)/access-logs/components/overview-tab.tsx b/frontend/app/(main)/access-logs/components/overview-tab.tsx index 47ba8a0e..5a419eb7 100644 --- a/frontend/app/(main)/access-logs/components/overview-tab.tsx +++ b/frontend/app/(main)/access-logs/components/overview-tab.tsx @@ -318,6 +318,8 @@ function OverviewContent({ data: AccessLogOverview; hours: number; }) { + const uiText = useTranslations('runtime'); + const t = useTranslations('accessLogs'); const requestLabels = data.trends.requests.map((point) => formatOverviewTrendLabel(point.bucket_started_at, hours), @@ -331,7 +333,7 @@ function OverviewContent({ <>
- Requests over time + {uiText('ui_requestsOverTime')} {t('overview.requestsHint')} @@ -389,13 +391,13 @@ function OverviewContent({
@@ -422,17 +424,17 @@ function OverviewContent({
diff --git a/frontend/app/(main)/admin/database/components/cache-manager.tsx b/frontend/app/(main)/admin/database/components/cache-manager.tsx index ccbd776e..be76f5a6 100644 --- a/frontend/app/(main)/admin/database/components/cache-manager.tsx +++ b/frontend/app/(main)/admin/database/components/cache-manager.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { useCallback, useEffect, useState } from 'react'; -import { useTranslations } from 'next-intl'; +import { useFormatter, useTranslations } from 'next-intl'; import { toast } from 'sonner'; import { HardDrive, RefreshCw, Trash2 } from 'lucide-react'; @@ -28,25 +28,7 @@ import { } from '@/components/ui/dialog'; import type { CacheStatus } from '@/lib/services/admin'; import services from '@/lib/services'; - -/** - * 格式化字节大小 - */ -const formatBytes = (bytes: number) => { - if (bytes === 0) return '0 Bytes'; - const k = 1024; - const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; -}; - -/** - * 格式化数字,每3位加逗号 - */ -const formatNumber = (num: number | string) => { - if (num === undefined || num === null) return '0'; - return num.toString().replace(/\B(?=(\d{3})+(?!\B))/g, ','); -}; +import { formatBytes } from '@/lib/utils/metrics'; interface CacheManagerProps { refreshTrigger: number; @@ -54,6 +36,7 @@ interface CacheManagerProps { export function CacheManager({ refreshTrigger }: CacheManagerProps) { const t = useTranslations('admin.database.cache'); + const format = useFormatter(); const [cacheStatus, setCacheStatus] = useState(null); const [loadingCache, setLoadingCache] = useState(true); const [savingConfig, setSavingConfig] = useState(false); @@ -208,7 +191,7 @@ export function CacheManager({ refreshTrigger }: CacheManagerProps) { {t('cacheKeysCount')}

- {formatNumber(cacheStatus.keys_count)}{' '} + {format.number(cacheStatus.keys_count)}{' '} {t('files')} diff --git a/frontend/app/(main)/admin/database/components/sql-console.tsx b/frontend/app/(main)/admin/database/components/sql-console.tsx index 3ddd5150..4653b264 100644 --- a/frontend/app/(main)/admin/database/components/sql-console.tsx +++ b/frontend/app/(main)/admin/database/components/sql-console.tsx @@ -142,7 +142,9 @@ export function SQLConsole({ dbType, onClose }: SQLConsoleProps) {

- {dbType === 'postgres' ? 'PostgreSQL Connected' : 'SQLite Connected'} + {t('connected', { + database: dbType === 'postgres' ? 'PostgreSQL' : 'SQLite', + })}
diff --git a/frontend/app/(main)/admin/database/components/table-browser.tsx b/frontend/app/(main)/admin/database/components/table-browser.tsx index 795b6d59..bfa08ef4 100644 --- a/frontend/app/(main)/admin/database/components/table-browser.tsx +++ b/frontend/app/(main)/admin/database/components/table-browser.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { useCallback, useEffect, useState } from 'react'; -import { useTranslations } from 'next-intl'; +import { useFormatter, useTranslations } from 'next-intl'; import { toast } from 'sonner'; import { Layers, RefreshCw } from 'lucide-react'; @@ -33,14 +33,6 @@ import { Button } from '@/components/ui/button'; import type { TableDataResponse } from '@/lib/services/db-manage'; import { DbManageService } from '@/lib/services/db-manage'; -/** - * 格式化数字,每3位加逗号 - */ -const formatNumber = (num: number | string) => { - if (num === undefined || num === null) return '0'; - return num.toString().replace(/\B(?=(\d{3})+(?!\B))/g, ','); -}; - /** * 格式化表格单元格内容 */ @@ -69,6 +61,7 @@ export function TableBrowser({ refreshTrigger, }: TableBrowserProps) { const t = useTranslations('admin.database.tableBrowser'); + const format = useFormatter(); const [selectedTable, setSelectedTable] = useState(''); const [tableData, setTableData] = useState(null); const [page, setPage] = useState(1); @@ -222,7 +215,7 @@ export function TableBrowser({
{t('totalRows')}:{' '} - {formatNumber(tableData.total)} + {format.number(tableData.total)} {' '} {t('records')}
diff --git a/frontend/app/(main)/admin/database/page-client.tsx b/frontend/app/(main)/admin/database/page-client.tsx index fc9ae3eb..80e6d228 100644 --- a/frontend/app/(main)/admin/database/page-client.tsx +++ b/frontend/app/(main)/admin/database/page-client.tsx @@ -3,7 +3,7 @@ import dynamic from 'next/dynamic'; import * as React from 'react'; import { useCallback, useEffect, useState } from 'react'; -import { useTranslations } from 'next-intl'; +import { useFormatter, useTranslations } from 'next-intl'; import { toast } from 'sonner'; import { Activity, @@ -49,16 +49,9 @@ const SQLConsole = dynamic( { ssr: false }, ); -/** - * 格式化数字,每3位加逗号 - */ -const formatNumber = (num: number | string) => { - if (num === undefined || num === null) return '0'; - return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); -}; - export function DatabasePageClient() { const t = useTranslations('admin.database'); + const format = useFormatter(); // 核心状态 const [overview, setOverview] = useState(null); const [tables, setTables] = useState([]); @@ -275,7 +268,7 @@ export function DatabasePageClient() {
- {formatNumber(overview.table_count)} + {format.number(overview.table_count)}
@@ -290,7 +283,7 @@ export function DatabasePageClient() {
- {formatNumber(overview.connections)} + {format.number(overview.connections)}
diff --git a/frontend/app/(main)/admin/files/components/file-list.tsx b/frontend/app/(main)/admin/files/components/file-list.tsx index b2188627..6cb3e5bf 100644 --- a/frontend/app/(main)/admin/files/components/file-list.tsx +++ b/frontend/app/(main)/admin/files/components/file-list.tsx @@ -83,6 +83,8 @@ function formatDate(dateStr: string) { } export function FileList() { + const uiText = useTranslations('runtime'); + const t = useTranslations('admin.files'); const queryClient = useQueryClient(); const [keyword, setKeyword] = React.useState(''); @@ -565,7 +567,7 @@ export function FileList() {
- Hash (SHA-256) + {uiText('ui_fileHash')} {detailTarget.hash} diff --git a/frontend/app/(main)/admin/files/components/file-stats.tsx b/frontend/app/(main)/admin/files/components/file-stats.tsx index 219461ee..ac29c235 100644 --- a/frontend/app/(main)/admin/files/components/file-stats.tsx +++ b/frontend/app/(main)/admin/files/components/file-stats.tsx @@ -223,7 +223,7 @@ export function FileStats() {
{stats.total_count.toLocaleString()}{' '} - {t('stats.countUnit')} + {t.raw('stats.countUnit')}

@@ -260,7 +260,7 @@ export function FileStats() {

+{trendSummary.count.toLocaleString()}{' '} - {t('stats.countUnit')} + {t.raw('stats.countUnit')}

@@ -384,7 +384,7 @@ export function FileStats() { const formattedValue = trendMetric === 'size' ? formatFileSize(Number(value)) - : `${value} ${t('stats.countUnit')}`; + : `${value} ${t.raw('stats.countUnit')}`; return ( <>

@@ -494,7 +494,7 @@ export function FileStats() {
- {value} {t('stats.countUnit')} + {value} {t.raw('stats.countUnit')} ); diff --git a/frontend/app/(main)/admin/files/components/storage-config-tab.tsx b/frontend/app/(main)/admin/files/components/storage-config-tab.tsx index 11ae498f..95ad0c74 100644 --- a/frontend/app/(main)/admin/files/components/storage-config-tab.tsx +++ b/frontend/app/(main)/admin/files/components/storage-config-tab.tsx @@ -501,13 +501,15 @@ function ObjectFields({ value: ObjectStorageConfig; onChange: (patch: Partial) => void; }) { + const uiText = useTranslations('runtime'); + const t = useTranslations('admin.files'); return ( <> {driver === 'r2' && (
onChange({ account_id })} /> @@ -516,7 +518,7 @@ function ObjectFields({ {driver !== 's3' && (
onChange({ endpoint })} @@ -524,22 +526,22 @@ function ObjectFields({
)} onChange({ region })} /> onChange({ bucket })} /> onChange({ access_key_id })} /> onChange({ secret_access_key })} @@ -564,7 +566,7 @@ function ObjectFields({ >
- Path Style + {uiText('ui_pathStyle')} {t('storage.pathStyleDesc')} diff --git a/frontend/app/(main)/admin/logs/components/access-logs.tsx b/frontend/app/(main)/admin/logs/components/access-logs.tsx index 0e1340ea..c8179f94 100644 --- a/frontend/app/(main)/admin/logs/components/access-logs.tsx +++ b/frontend/app/(main)/admin/logs/components/access-logs.tsx @@ -1,3 +1,5 @@ +'use client'; + /* Copyright 2026 Arctel.net @@ -14,8 +16,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use client'; - import { useMemo, useState } from 'react'; import { isLogStorageDisabledMessage } from './log-storage-error'; import { useQuery } from '@tanstack/react-query'; @@ -99,6 +99,8 @@ function isClickhouseDisabledError(error: Error) { } export function AccessLogs() { + const uiText = useTranslations('runtime'); + const t = useTranslations('admin.logs.accessLogs'); const [page, setPage] = useState(1); @@ -520,7 +522,7 @@ export function AccessLogs() {
diff --git a/frontend/app/(main)/admin/settings/components/openflare-ops.tsx b/frontend/app/(main)/admin/settings/components/openflare-ops.tsx index 0f092349..24aaec95 100644 --- a/frontend/app/(main)/admin/settings/components/openflare-ops.tsx +++ b/frontend/app/(main)/admin/settings/components/openflare-ops.tsx @@ -64,6 +64,8 @@ async function copyText(value: string) { } export function OpenFlareOpsSettings() { + const uiText = useTranslations('runtime'); + const t = useTranslations('openflareOps'); const queryClient = useQueryClient(); const [fields, setFields] = useState( @@ -346,7 +348,7 @@ export function OpenFlareOpsSettings() { value={geoIPMutation.data.name || '—'} />
@@ -427,7 +429,7 @@ export function OpenFlareOpsSettings() { updateField('server_address', value)} placeholder='https://yourdomain.com' diff --git a/frontend/app/(main)/admin/settings/components/system-status.tsx b/frontend/app/(main)/admin/settings/components/system-status.tsx index 5303b62a..b4c86672 100644 --- a/frontend/app/(main)/admin/settings/components/system-status.tsx +++ b/frontend/app/(main)/admin/settings/components/system-status.tsx @@ -1,7 +1,9 @@ 'use client'; import * as React from 'react'; -import { useTranslations } from 'next-intl'; +import { useLocale, useTranslations } from 'next-intl'; +import { normalizeLocale } from '@/i18n/config'; +import { formatSystemMetric } from '@/lib/utils/system-metrics'; import { useCallback, useEffect, useRef, useState } from 'react'; import { toast } from 'sonner'; import { @@ -34,19 +36,12 @@ import { Skeleton } from '@/components/ui/skeleton'; import type { SystemStatus } from '@/lib/services/admin'; import services from '@/lib/services'; -/** - * 格式化数字,每3位加逗号 - */ -const formatNumber = (num: number | string) => { - if (num === undefined || num === null) return '0'; - return num.toString().replace(/\B(?=(\d{3})+(?!\B))/g, ','); -}; - /** * 运行时系统状态展示与管理组件 */ export function SystemStatusManager() { const t = useTranslations('openflareOps.status'); + const locale = normalizeLocale(useLocale()); const [status, setStatus] = useState(null); const [loading, setLoading] = useState(true); const [wavelet, setWavelet] = useState(false); @@ -164,7 +159,7 @@ export function SystemStatusManager() { - {typeof value === 'number' ? formatNumber(value) : value} + {formatSystemMetric(value, locale)}
); diff --git a/frontend/app/(main)/admin/users/components/user-detail-sheet.tsx b/frontend/app/(main)/admin/users/components/user-detail-sheet.tsx index 083f8e1f..74566c60 100644 --- a/frontend/app/(main)/admin/users/components/user-detail-sheet.tsx +++ b/frontend/app/(main)/admin/users/components/user-detail-sheet.tsx @@ -34,6 +34,8 @@ export function UserDetailSheet({ detailLoading, onStatusToggle, }: UserDetailSheetProps) { + const uiText = useTranslations('runtime'); + const t = useTranslations('admin.users'); const displayValue = (value?: string) => value && value.trim() ? value : '-'; @@ -72,7 +74,7 @@ export function UserDetailSheet({ {selectedUser.is_admin && ( - Admin + {uiText('ui_administrator')} )}
diff --git a/frontend/app/(main)/apply-logs/components/log-detail-sheet.tsx b/frontend/app/(main)/apply-logs/components/log-detail-sheet.tsx index b48d7e94..28f6884b 100644 --- a/frontend/app/(main)/apply-logs/components/log-detail-sheet.tsx +++ b/frontend/app/(main)/apply-logs/components/log-detail-sheet.tsx @@ -61,6 +61,8 @@ export function LogDetailSheet({ open, onOpenChange, }: LogDetailSheetProps) { + const uiText = useTranslations('runtime'); + const t = useTranslations('applyLogs'); return ( @@ -75,7 +77,7 @@ export function LogDetailSheet({
- Node: {log.node_id} + {uiText('ui_nodeLabel')} {log.node_id} {t('versionBadge', { version: log.version })} diff --git a/frontend/app/(main)/cloudflare/components/sync-tasks-panel.tsx b/frontend/app/(main)/cloudflare/components/sync-tasks-panel.tsx index 8ff0be52..79393883 100644 --- a/frontend/app/(main)/cloudflare/components/sync-tasks-panel.tsx +++ b/frontend/app/(main)/cloudflare/components/sync-tasks-panel.tsx @@ -130,6 +130,8 @@ function statusVariant(status: TaskExecutionStatus) { } export function SyncTasksPanel() { + const uiText = useTranslations('runtime'); + const t = useTranslations('cloudflare.tasks'); const queryClient = useQueryClient(); const [page, setPage] = useState(1); @@ -469,7 +471,9 @@ export function SyncTasksPanel() { ) : null}
-
Payload
+
+ {uiText('ui_payload')} +
                     {selected.payload || '{}'}
                   
diff --git a/frontend/app/(main)/cloudflare/groups/[id]/page-client.tsx b/frontend/app/(main)/cloudflare/groups/[id]/page-client.tsx index 5093ed1c..4ed4d4ae 100644 --- a/frontend/app/(main)/cloudflare/groups/[id]/page-client.tsx +++ b/frontend/app/(main)/cloudflare/groups/[id]/page-client.tsx @@ -67,6 +67,8 @@ function getGroupIdFromPathname(pathname: string | null): number { } export function CloudflareGroupDetailPageClient() { + const uiText = useTranslations('runtime'); + const t = useTranslations('cloudflare'); const tCommon = useTranslations('common'); const [mounted, setMounted] = useState(false); @@ -449,7 +451,7 @@ export function CloudflareGroupDetailPageClient() { setSelectedMemberIDs(new Set()); } }} - aria-label='Select all' + aria-label={uiText('ui_selectAll')} /> {t('columns.domain')} diff --git a/frontend/app/(main)/nodes/components/edge-node-detail.tsx b/frontend/app/(main)/nodes/components/edge-node-detail.tsx index e18964a6..95e3e274 100644 --- a/frontend/app/(main)/nodes/components/edge-node-detail.tsx +++ b/frontend/app/(main)/nodes/components/edge-node-detail.tsx @@ -64,6 +64,7 @@ const nodesQueryKey = ['openflare', 'nodes']; export function EdgeNodeDetail({ node }: { node: NodeItem }) { const t = useTranslations('nodes'); + const rt = useTranslations('runtime'); const tc = useTranslations('common'); const router = useRouter(); const queryClient = useQueryClient(); @@ -320,7 +321,7 @@ export function EdgeNodeDetail({ node }: { node: NodeItem }) { <> = { all: t('filter.all'), - edge: 'Edge', - relay: 'Relay', - tunnel: 'Tunnel', + edge: rt('node_type_edge'), + relay: rt('node_type_relay'), + tunnel: rt('node_type_tunnel'), }; return ( diff --git a/frontend/app/(main)/nodes/components/node-utils.ts b/frontend/app/(main)/nodes/components/node-utils.ts index 57e4091e..a4c9a0cd 100644 --- a/frontend/app/(main)/nodes/components/node-utils.ts +++ b/frontend/app/(main)/nodes/components/node-utils.ts @@ -1,4 +1,5 @@ import { getFormattingLocale } from '@/i18n/format'; +import { runtimeText } from '@/i18n/runtime'; import type { ApplyResult, NodeItem, @@ -112,9 +113,9 @@ export function getRelayStatusLabel( } export function getNodeTypeLabel(nodeType: NodeItem['node_type']) { - if (nodeType === 'tunnel_relay') return 'Relay'; - if (nodeType === 'tunnel_client') return 'Tunnel'; - return 'Edge'; + if (nodeType === 'tunnel_relay') return runtimeText('node_type_relay'); + if (nodeType === 'tunnel_client') return runtimeText('node_type_tunnel'); + return runtimeText('node_type_edge'); } export function getErrorMessage(error: unknown, fallback: string) { diff --git a/frontend/app/(main)/nodes/components/relay-node-detail.tsx b/frontend/app/(main)/nodes/components/relay-node-detail.tsx index 7b842cec..d54baddd 100644 --- a/frontend/app/(main)/nodes/components/relay-node-detail.tsx +++ b/frontend/app/(main)/nodes/components/relay-node-detail.tsx @@ -489,7 +489,7 @@ export function RelayNodeDetail({ node }: { node: NodeItem }) { <> - Production + {uiText('ui_production')} {t('history.productionHint')}