Files
pangolin/web/usercenter/lib/i18n.ts
T
wangjia 5b89de656e
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (pull_request) Successful in 25s
ci-pangolin / Codegen Drift — token 生成物未漂移 (pull_request) Successful in 7s
ci-pangolin / Cleartext Scan — Android 禁明文 (pull_request) Successful in 26s
ci-pangolin / DS-flow — 原型/跨端同源/代码色单源闸 (pull_request) Successful in 11s
ci-pangolin / Go — build + test (pull_request) Successful in 15s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (pull_request) Successful in 26s
ci-pangolin / E2E Smoke — L4 进程级端到端 (pull_request) Successful in 10s
ci-pangolin / Go — integration (mysql/redis testcontainers) (pull_request) Successful in 4m36s
ci-pangolin / Golden — 视觉回归 (全量:components/auth/desktop/tablet) (pull_request) Successful in 20s
ci-pangolin / Lint — shellcheck (pull_request) Failing after 10m3s
Deploy Site / deploy-site (push) Failing after 13m20s
ci-pangolin / Flutter — analyze + test (pull_request) Failing after 14m42s
ci-pangolin / OpenAPI Sync Check (pull_request) Failing after 14m52s
feat(web): 5 项 UI 修复 — 统一浅色/登录回跳/logo locale/用户名下拉/Docs 真页
用户中心:
- 统一浅色:theme.tsx 无偏好时不再跟随系统 prefers-color-scheme:dark(登录页变
  黑真因),恒浅色;保留手动切换。
- 登录回跳:UserCenter 加 safeRedirect 白名单,登录成功按 ?redirect= 回来源页
  (官网带 /,登录后回主页),无则进 overview。
- logo locale:新增 i18n brandName(zh 穿山甲/其余 Pangolin),Login/UserCenter
  /Subscription 三处引用。
- 存 pg_uc_email(getMe 时)/ clearSession 删,供官网读用户名。

官网(全走 CSS class 合 CSP,无内联 style):
- logo locale:i18n nav.brand(zh 穿山甲/其余 Pangolin),Header+Footer。
- 登录态头部显示用户名(读同源 pg_uc_email)+ 下拉菜单(进入用户中心/切换用户/
  退出登录,复用 .langmenu 风格);未登录 Log in 带 ?redirect=/ 回跳。
- Docs 四卡片补真内容页(en+zh:quickstart/faq/protocol/privacy + Doc.astro
  布局),卡片改回 <a href>;Protocol 改正 sing-box+REALITY(去 WireGuard)。

验证:同源闸绿 · 两端 build 过 · redline 0 · Header 零内联 style · 无 WireGuard。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 09:52:04 +08:00

152 lines
28 KiB
TypeScript

// i18n.ts — 穿山甲 Web 用户中心 · 多语字串(单显,绝不并排)
// 直接承袭 design/ui_kits/usercenter/ucparts.jsx 的 UCSTRINGS,并补充设置/设备/TOTP 键。
export type Lang = 'zh' | 'en' | 'ja' | 'ko' | 'ru' | 'es';
type Entry = { zh: string; en: string; ja: string; ko: string; ru: string; es: string };
export const STRINGS: Record<string, Entry> = {
navOverview: { zh: '概览', en: 'Overview', ja: '概要', ko: '개요', ru: 'Обзор', es: 'Resumen' },
navSub: { zh: '订阅', en: 'Subscription', ja: 'サブスクリプション', ko: '구독', ru: 'Подписка', es: 'Suscripción' },
navRedeem: { zh: '兑换 & 购买', en: 'Redeem & buy', ja: '引き換え & 購入', ko: '코드 등록 & 구매', ru: 'Активация и покупка', es: 'Canjear y comprar' },
navInvite: { zh: '邀请返利', en: 'Referral', ja: '紹介', ko: '추천', ru: 'Рефералы', es: 'Referidos' },
navSettings: { zh: '设置', en: 'Settings', ja: '設定', ko: '설정', ru: 'Настройки', es: 'Ajustes' },
signOut: { zh: '退出', en: 'Sign out', ja: 'ログアウト', ko: '로그아웃', ru: 'Выйти', es: 'Cerrar sesión' },
backHome: { zh: '返回主页', en: 'Home', ja: 'ホーム', ko: '홈', ru: 'На главную', es: 'Inicio' },
brandName: { zh: '穿山甲', en: 'Pangolin', ja: 'Pangolin', ko: 'Pangolin', ru: 'Pangolin', es: 'Pangolin' },
/* login */
loginTitle: { zh: '登录用户中心', en: 'Log in to your account', ja: 'アカウントにログイン', ko: '계정에 로그인', ru: 'Вход в аккаунт', es: 'Inicia sesión en tu cuenta' },
loginSub: { zh: '管理订阅、兑换激活码、查看用量', en: 'Manage subscription, redeem codes, track usage', ja: 'サブスクの管理、コードの引き換え、使用状況の確認', ko: '구독 관리, 코드 등록, 사용량 확인', ru: 'Управление подпиской, активация кодов, статистика использования', es: 'Gestiona tu suscripción, canjea códigos y consulta el uso' },
emailLabel: { zh: '邮箱', en: 'Email', ja: 'メールアドレス', ko: '이메일', ru: 'Эл. почта', es: 'Correo electrónico' },
emailPh: { zh: '你的邮箱地址', en: 'your@email.com', ja: 'your@email.com', ko: 'your@email.com', ru: 'your@email.com', es: 'your@email.com' },
pwLabel: { zh: '密码', en: 'Password', ja: 'パスワード', ko: '비밀번호', ru: 'Пароль', es: 'Contraseña' },
pwPh: { zh: '输入密码', en: 'Enter password', ja: 'パスワードを入力', ko: '비밀번호 입력', ru: 'Введите пароль', es: 'Introduce la contraseña' },
doLogin: { zh: '登录', en: 'Log in', ja: 'ログイン', ko: '로그인', ru: 'Войти', es: 'Iniciar sesión' },
forgotPw: { zh: '忘记密码?', en: 'Forgot password?', ja: 'パスワードをお忘れですか?', ko: '비밀번호를 잊으셨나요?', ru: 'Забыли пароль?', es: '¿Olvidaste tu contraseña?' },
noAccount: { zh: '没有账户?在 App 内注册', en: 'No account? Sign up in the app', ja: 'アカウントがない場合はアプリで登録', ko: '계정이 없나요? 앱에서 가입하세요', ru: 'Нет аккаунта? Зарегистрируйтесь в приложении', es: '¿No tienes cuenta? Regístrate en la app' },
loginFailed: { zh: '邮箱或密码错误', en: 'Wrong email or password', ja: 'メールアドレスまたはパスワードが違います', ko: '이메일 또는 비밀번호가 올바르지 않습니다', ru: 'Неверная почта или пароль', es: 'Correo o contraseña incorrectos' },
loginLocked: { zh: '失败次数过多,账户已临时锁定', en: 'Too many attempts — account temporarily locked', ja: '試行回数が多すぎます — アカウントを一時的にロックしました', ko: '시도 횟수가 너무 많습니다 — 계정이 일시적으로 잠겼습니다', ru: 'Слишком много попыток — аккаунт временно заблокирован', es: 'Demasiados intentos: la cuenta está bloqueada temporalmente' },
lockedCountdown: { zh: '请于 {s} 秒后重试', en: 'Try again in {s}s', ja: '{s} 秒後に再試行してください', ko: '{s}초 후에 다시 시도하세요', ru: 'Повторите через {s} с', es: 'Vuelve a intentarlo en {s}s' },
/* sso(App→网页免登录落地页) */
ssoTitle: { zh: '正在登录…', en: 'Signing in…', ja: 'ログイン中…', ko: '로그인 중…', ru: 'Вход…', es: 'Iniciando sesión…' },
ssoMsg: { zh: '正在验证来自 App 的登录票据', en: 'Verifying the sign-in ticket from the app', ja: 'アプリからのログインチケットを確認しています', ko: '앱에서 받은 로그인 티켓을 확인하는 중', ru: 'Проверяем токен входа из приложения', es: 'Verificando el ticket de inicio de sesión de la app' },
ssoFailTitle: { zh: '登录跳转失败', en: 'Sign-in redirect failed', ja: 'ログインのリダイレクトに失敗しました', ko: '로그인 리디렉션에 실패했습니다', ru: 'Не удалось выполнить переход для входа', es: 'Error en la redirección de inicio de sesión' },
ssoFailHint: { zh: '即将跳转到登录页,请手动登录', en: 'Redirecting to the login page, please sign in manually', ja: 'ログインページに移動します。手動でログインしてください', ko: '로그인 페이지로 이동합니다. 직접 로그인해 주세요', ru: 'Перенаправляем на страницу входа, войдите вручную', es: 'Redirigiendo a la página de inicio de sesión, inicia sesión manualmente' },
/* overview */
greeting: { zh: '欢迎回来', en: 'Welcome back', ja: 'おかえりなさい', ko: '다시 오신 것을 환영합니다', ru: 'С возвращением', es: 'Bienvenido de nuevo' },
curPlan: { zh: '当前套餐', en: 'Current plan', ja: '現在のプラン', ko: '현재 요금제', ru: 'Текущий тариф', es: 'Plan actual' },
freePlan: { zh: '免费版', en: 'Free', ja: '無料版', ko: '무료', ru: 'Бесплатный', es: 'Gratis' },
proMember: { zh: 'PRO 会员', en: 'PRO member', ja: 'PRO会員', ko: 'PRO 회원', ru: 'PRO-участник', es: 'Miembro PRO' },
expires: { zh: '有效期至', en: 'Expires', ja: '有効期限', ko: '만료일', ru: 'Действует до', es: 'Caduca' },
renew: { zh: '续费 / 升级', en: 'Renew / Upgrade', ja: '更新 / アップグレード', ko: '갱신 / 업그레이드', ru: 'Продлить / Улучшить', es: 'Renovar / Mejorar' },
quotaToday: { zh: '今日剩余时长', en: 'Time left today', ja: '本日の残り時間', ko: '오늘 남은 시간', ru: 'Осталось времени сегодня', es: 'Tiempo restante hoy' },
dataToday: { zh: '今日已用流量', en: 'Data used today', ja: '本日の使用データ量', ko: '오늘 사용한 데이터', ru: 'Трафик за сегодня', es: 'Datos usados hoy' },
devices: { zh: '在线设备', en: 'Devices online', ja: 'オンラインのデバイス', ko: '온라인 기기', ru: 'Устройств онлайн', es: 'Dispositivos en línea' },
quotaFree: { zh: '免费版 · 每日 10 分钟', en: 'Free · 10 min/day', ja: '無料版 · 1日10分', ko: '무료 · 하루 10분', ru: 'Бесплатно · 10 мин/день', es: 'Gratis · 10 min/día' },
usageTitle: { zh: '近 7 日流量 (GB)', en: 'Last 7 days (GB)', ja: '過去7日間 (GB)', ko: '최근 7일 (GB)', ru: 'За 7 дней (ГБ)', es: 'Últimos 7 días (GB)' },
quickSub: { zh: '快速操作', en: 'Quick actions', ja: 'クイック操作', ko: '빠른 작업', ru: 'Быстрые действия', es: 'Acciones rápidas' },
qaSub: { zh: '获取订阅链接', en: 'Get subscription', ja: 'サブスクリプションを取得', ko: '구독 가져오기', ru: 'Получить подписку', es: 'Obtener suscripción' },
qaRedeem: { zh: '兑换激活码', en: 'Redeem a code', ja: 'コードを引き換える', ko: '코드 등록', ru: 'Активировать код', es: 'Canjear un código' },
qaApp: { zh: '下载 App', en: 'Download apps', ja: 'アプリをダウンロード', ko: '앱 다운로드', ru: 'Скачать приложения', es: 'Descargar apps' },
/* subscription */
subTitle: { zh: '我的订阅', en: 'My subscription', ja: 'マイサブスクリプション', ko: '내 구독', ru: 'Моя подписка', es: 'Mi suscripción' },
subDesc: { zh: '订阅链接是你的专属凭证,泄露后他人可使用你的额度。请勿分享。', en: 'Your subscription link is a private credential. Never share it.', ja: 'サブスクリプションリンクはあなた専用の認証情報です。他人と共有しないでください。', ko: '구독 링크는 개인 인증 정보입니다. 절대 공유하지 마세요.', ru: 'Ссылка на подписку — ваш личный ключ доступа. Никому не передавайте её.', es: 'Tu enlace de suscripción es una credencial privada. Nunca lo compartas.' },
subCopy: { zh: '复制链接', en: 'Copy link', ja: 'リンクをコピー', ko: '링크 복사', ru: 'Копировать ссылку', es: 'Copiar enlace' },
subCopied: { zh: '已复制', en: 'Copied', ja: 'コピーしました', ko: '복사됨', ru: 'Скопировано', es: 'Copiado' },
subReset: { zh: '重置链接', en: 'Reset link', ja: 'リンクをリセット', ko: '링크 재설정', ru: 'Сбросить ссылку', es: 'Restablecer enlace' },
subResetSub: { zh: '旧链接立即失效,所有设备需重新导入', en: 'Old link stops working; re-import on all devices', ja: '古いリンクは即座に無効になり、すべてのデバイスで再インポートが必要です', ko: '기존 링크는 즉시 만료되며 모든 기기에서 다시 가져와야 합니다', ru: 'Старая ссылка перестанет работать; переимпортируйте на всех устройствах', es: 'El enlace anterior deja de funcionar; vuelve a importarlo en todos los dispositivos' },
subResetOk: { zh: '已重置,请重新导入', en: 'Reset — re-import on your devices', ja: 'リセットしました。デバイスで再インポートしてください', ko: '재설정됨 — 기기에서 다시 가져오세요', ru: 'Сброшено — переимпортируйте на устройствах', es: 'Restablecido: vuelve a importarlo en tus dispositivos' },
scanTitle: { zh: '扫码导入', en: 'Scan to import', ja: 'スキャンしてインポート', ko: '스캔하여 가져오기', ru: 'Сканировать для импорта', es: 'Escanear para importar' },
scanSub: { zh: '用穿山甲 App 或三方客户端扫码', en: 'Scan with the Pangolin app or a 3rd-party client', ja: 'Pangolinアプリまたはサードパーティクライアントでスキャン', ko: 'Pangolin 앱 또는 서드파티 클라이언트로 스캔하세요', ru: 'Сканируйте в приложении Pangolin или стороннем клиенте', es: 'Escanea con la app Pangolin o un cliente de terceros' },
importTitle: { zh: '一键导入三方客户端', en: 'One-tap import', ja: 'ワンタップインポート', ko: '원탭 가져오기', ru: 'Импорт в одно касание', es: 'Importar con un toque' },
importSub: { zh: '已安装对应客户端时,点击即自动导入订阅。', en: 'If the client is installed, tapping imports the subscription automatically.', ja: '対応クライアントがインストール済みなら、タップするだけでサブスクリプションが自動インポートされます。', ko: '해당 클라이언트가 설치되어 있으면 탭 한 번으로 구독이 자동 가져오기 됩니다.', ru: 'Если клиент установлен, нажатие автоматически импортирует подписку.', es: 'Si el cliente está instalado, al tocar se importa la suscripción automáticamente.' },
fmtNote: { zh: '同一链接同时兼容 sing-box / Clash.Meta / v2ray 格式。', en: 'One link serves sing-box / Clash.Meta / v2ray formats.', ja: '1つのリンクで sing-box / Clash.Meta / v2ray 形式に対応します。', ko: '하나의 링크로 sing-box / Clash.Meta / v2ray 형식을 모두 지원합니다.', ru: 'Одна ссылка работает с форматами sing-box / Clash.Meta / v2ray.', es: 'Un mismo enlace sirve para los formatos sing-box / Clash.Meta / v2ray.' },
/* redeem */
redeemTitle: { zh: '兑换激活码', en: 'Redeem a code', ja: 'コードを引き換える', ko: '코드 등록', ru: 'Активировать код', es: 'Canjear un código' },
redeemPh: { zh: '输入激活码', en: 'Enter activation code', ja: 'アクティベーションコードを入力', ko: '인증 코드 입력', ru: 'Введите код активации', es: 'Introduce el código de activación' },
redeemBtn: { zh: '激活', en: 'Redeem', ja: '引き換える', ko: '등록', ru: 'Активировать', es: 'Canjear' },
redeemOk: { zh: '激活成功 · 套餐已到账', en: 'Activated — plan applied', ja: 'アクティベート完了 — プランが適用されました', ko: '활성화 완료 — 요금제가 적용되었습니다', ru: 'Активировано — тариф применён', es: 'Activado: plan aplicado' },
buyTitle: { zh: '购买渠道', en: 'Where to buy', ja: '購入方法', ko: '구매처', ru: 'Где купить', es: 'Dónde comprar' },
buySub: { zh: '本站不直接收款。通过以下渠道购买激活码,回到本页兑换即可:', en: 'We never take payment on this site. Buy a code via a channel below, then redeem here:', ja: '当サイトでは直接お支払いを受け付けていません。以下のチャネルでコードを購入し、このページで引き換えてください:', ko: '본 사이트에서는 직접 결제를 받지 않습니다. 아래 채널에서 코드를 구매한 뒤 이 페이지에서 등록하세요:', ru: 'Мы не принимаем оплату на этом сайте. Купите код через один из каналов ниже и активируйте его здесь:', es: 'No aceptamos pagos en este sitio. Compra un código por uno de los canales de abajo y canjéalo aquí:' },
chStore: { zh: '自助发卡商店', en: 'Self-serve store', ja: 'セルフサービスストア', ko: '셀프 서비스 스토어', ru: 'Магазин самообслуживания', es: 'Tienda de autoservicio' },
chStoreSub: { zh: '支付宝 / 微信 · 自动发码', en: 'Alipay / WeChat · instant code', ja: 'Alipay / WeChat · コード即時発行', ko: 'Alipay / WeChat · 즉시 발급', ru: 'Alipay / WeChat · мгновенная выдача кода', es: 'Alipay / WeChat · código al instante' },
chUsdtSub: { zh: '链上转账 · 最隐私 · 自动发码', en: 'On-chain · most private · instant code', ja: 'オンチェーン送金 · 最もプライベート · コード即時発行', ko: '온체인 송금 · 가장 프라이빗 · 즉시 발급', ru: 'Ончейн-перевод · максимальная приватность · мгновенная выдача кода', es: 'En cadena · máxima privacidad · código al instante' },
chEmail: { zh: '邮箱客服', en: 'Email support', ja: 'メールサポート', ko: '이메일 고객지원', ru: 'Поддержка по эл. почте', es: 'Soporte por correo' },
/* invite */
inviteTitle: { zh: '邀请返利', en: 'Referral', ja: '紹介', ko: '추천', ru: 'Рефералы', es: 'Referidos' },
inviteSub: { zh: '好友通过你的链接注册并付费,你获得其首单 20% 余额返利,可抵扣续费。', en: 'When a friend signs up and pays via your link, you earn 20% of their first order as credit.', ja: '友達があなたのリンクから登録して支払うと、初回注文の20%がクレジットとして還元されます。', ko: '친구가 내 링크로 가입하고 결제하면 첫 주문 금액의 20%를 크레딧으로 받습니다.', ru: 'Когда друг регистрируется и платит по вашей ссылке, вы получаете 20% его первого заказа в виде бонуса.', es: 'Cuando un amigo se registra y paga con tu enlace, ganas el 20% de su primer pedido como saldo.' },
inviteLink: { zh: '我的邀请链接', en: 'My invite link', ja: 'マイ紹介リンク', ko: '내 초대 링크', ru: 'Моя реферальная ссылка', es: 'Mi enlace de invitación' },
invited: { zh: '已邀请', en: 'Invited', ja: '招待済み', ko: '초대함', ru: 'Приглашено', es: 'Invitados' },
paidUsers: { zh: '已付费', en: 'Converted', ja: '成約', ko: '전환됨', ru: 'Оплатили', es: 'Convertidos' },
earned: { zh: '累计返利', en: 'Credit earned', ja: '獲得クレジット', ko: '적립 크레딧', ru: 'Начислено бонусов', es: 'Saldo ganado' },
inviteRecord: { zh: '邀请记录', en: 'History', ja: '履歴', ko: '내역', ru: 'История', es: 'Historial' },
recEmpty: { zh: '暂无邀请记录', en: 'No referrals yet', ja: 'まだ紹介はありません', ko: '아직 추천 내역이 없습니다', ru: 'Пока нет рефералов', es: 'Aún no hay referidos' },
recRegistered: { zh: '已注册', en: 'Registered', ja: '登録済み', ko: '가입함', ru: 'Зарегистрирован', es: 'Registrado' },
recPaid: { zh: '已付费', en: 'Paid', ja: '支払い済み', ko: '결제함', ru: 'Оплачено', es: 'Pagado' },
/* 2FA login step */
twoFATitle: { zh: '双重认证', en: 'Two-factor auth', ja: '二要素認証', ko: '2단계 인증', ru: 'Двухфакторная аутентификация', es: 'Verificación en dos pasos' },
twoFAHint: { zh: '你的账户已开启双重认证。请输入身份验证器 App 中的 6 位动态码。', en: 'Two-factor auth is on for this account. Enter the 6-digit code from your authenticator app.', ja: 'このアカウントでは二要素認証が有効です。認証アプリの6桁コードを入力してください。', ko: '이 계정은 2단계 인증이 켜져 있습니다. 인증 앱의 6자리 코드를 입력하세요.', ru: 'Для этого аккаунта включена двухфакторная аутентификация. Введите 6-значный код из приложения-аутентификатора.', es: 'La verificación en dos pasos está activada en esta cuenta. Introduce el código de 6 dígitos de tu app de autenticación.' },
twoFAConfirm: { zh: '验证并登录', en: 'Verify & log in', ja: '確認してログイン', ko: '인증 후 로그인', ru: 'Подтвердить и войти', es: 'Verificar e iniciar sesión' },
twoFABack: { zh: '返回上一步', en: 'Back', ja: '戻る', ko: '뒤로', ru: 'Назад', es: 'Atrás' },
twoFAWrong: { zh: '动态码不正确,请重试', en: 'Invalid code, try again', ja: 'コードが正しくありません。もう一度お試しください', ko: '코드가 올바르지 않습니다. 다시 시도하세요', ru: 'Неверный код, попробуйте снова', es: 'Código no válido, inténtalo de nuevo' },
/* settings */
settingsTitle: { zh: '设置', en: 'Settings', ja: '設定', ko: '설정', ru: 'Настройки', es: 'Ajustes' },
prefTitle: { zh: '偏好', en: 'Preferences', ja: '環境設定', ko: '환경설정', ru: 'Параметры', es: 'Preferencias' },
prefLang: { zh: '显示语言', en: 'Language', ja: '表示言語', ko: '언어', ru: 'Язык', es: 'Idioma' },
prefTheme: { zh: '外观主题', en: 'Theme', ja: 'テーマ', ko: '테마', ru: 'Тема', es: 'Tema' },
themeLight: { zh: '浅色', en: 'Light', ja: 'ライト', ko: '라이트', ru: 'Светлая', es: 'Claro' },
themeDark: { zh: '深色', en: 'Dark', ja: 'ダーク', ko: '다크', ru: 'Тёмная', es: 'Oscuro' },
/* devices */
devTitle: { zh: '设备管理', en: 'Devices', ja: 'デバイス', ko: '기기', ru: 'Устройства', es: 'Dispositivos' },
devSub: { zh: '管理已登录本账户的设备,移除后该设备需重新登录。', en: 'Devices signed in to your account. Removing one signs it out.', ja: 'このアカウントにログイン中のデバイスです。削除するとそのデバイスはログアウトされます。', ko: '이 계정에 로그인된 기기입니다. 제거하면 해당 기기는 로그아웃됩니다.', ru: 'Устройства, вошедшие в ваш аккаунт. Удаление приведёт к выходу из аккаунта на устройстве.', es: 'Dispositivos con sesión iniciada en tu cuenta. Al quitar uno, se cierra su sesión.' },
devCurrent: { zh: '当前设备', en: 'This device', ja: 'このデバイス', ko: '현재 기기', ru: 'Это устройство', es: 'Este dispositivo' },
devLastActive: { zh: '最近活跃', en: 'Last active', ja: '最終アクティブ', ko: '마지막 활동', ru: 'Последняя активность', es: 'Última actividad' },
devRemove: { zh: '移除', en: 'Remove', ja: '削除', ko: '제거', ru: 'Удалить', es: 'Quitar' },
devRemoveConfirmTitle: { zh: '移除此设备?', en: 'Remove this device?', ja: 'このデバイスを削除しますか?', ko: '이 기기를 제거할까요?', ru: 'Удалить это устройство?', es: '¿Quitar este dispositivo?' },
devRemoveConfirmSub: { zh: '该设备将被登出,需重新输入凭证才能再次连接。', en: 'It will be signed out and must re-authenticate to connect again.', ja: 'このデバイスはログアウトされ、再接続には再認証が必要になります。', ko: '해당 기기는 로그아웃되며 다시 연결하려면 재인증이 필요합니다.', ru: 'Устройство выйдет из аккаунта и должно будет пройти повторную аутентификацию для подключения.', es: 'Se cerrará su sesión y deberá volver a autenticarse para conectarse de nuevo.' },
devEmpty: { zh: '暂无其他设备', en: 'No devices yet', ja: '他のデバイスはありません', ko: '다른 기기가 없습니다', ru: 'Пока нет устройств', es: 'Aún no hay dispositivos' },
cancel: { zh: '取消', en: 'Cancel', ja: 'キャンセル', ko: '취소', ru: 'Отмена', es: 'Cancelar' },
confirm: { zh: '确认移除', en: 'Remove', ja: '削除する', ko: '제거', ru: 'Удалить', es: 'Quitar' },
/* TOTP management */
totpTitle: { zh: '双重认证 (TOTP)', en: 'Two-factor auth (TOTP)', ja: '二要素認証 (TOTP)', ko: '2단계 인증 (TOTP)', ru: 'Двухфакторная аутентификация (TOTP)', es: 'Verificación en dos pasos (TOTP)' },
totpOff: { zh: '未开启', en: 'Off', ja: 'オフ', ko: '꺼짐', ru: 'Выкл.', es: 'Desactivado' },
totpOn: { zh: '已开启', en: 'On', ja: 'オン', ko: '켜짐', ru: 'Вкл.', es: 'Activado' },
totpDesc: { zh: '用身份验证器 App 生成的一次性动态码保护登录,显著提升账户安全。', en: 'Protect sign-in with one-time codes from an authenticator app.', ja: '認証アプリのワンタイムコードでログインを保護します。', ko: '인증 앱의 일회용 코드로 로그인을 보호하세요.', ru: 'Защитите вход одноразовыми кодами из приложения-аутентификатора.', es: 'Protege el inicio de sesión con códigos de un solo uso de una app de autenticación.' },
totpEnable: { zh: '开启双重认证', en: 'Enable 2FA', ja: '二要素認証を有効にする', ko: '2단계 인증 켜기', ru: 'Включить 2FA', es: 'Activar 2FA' },
totpDisableBtn: { zh: '关闭双重认证', en: 'Disable 2FA', ja: '二要素認証を無効にする', ko: '2단계 인증 끄기', ru: 'Отключить 2FA', es: 'Desactivar 2FA' },
totpStep1: { zh: '1. 用身份验证器扫码,或手动输入密钥', en: '1. Scan with an authenticator, or enter the key manually', ja: '1. 認証アプリでスキャンするか、キーを手動で入力してください', ko: '1. 인증 앱으로 스캔하거나 키를 직접 입력하세요', ru: '1. Отсканируйте в приложении-аутентификаторе или введите ключ вручную', es: '1. Escanea con una app de autenticación o introduce la clave manualmente' },
totpSecretLabel: { zh: '手动密钥', en: 'Manual key', ja: '手動入力キー', ko: '수동 키', ru: 'Ключ для ручного ввода', es: 'Clave manual' },
totpStep2: { zh: '2. 输入 App 显示的 6 位动态码以确认', en: '2. Enter the 6-digit code shown in the app to confirm', ja: '2. アプリに表示された6桁コードを入力して確認してください', ko: '2. 앱에 표시된 6자리 코드를 입력해 확인하세요', ru: '2. Введите 6-значный код из приложения для подтверждения', es: '2. Introduce el código de 6 dígitos que muestra la app para confirmar' },
totpVerifyBtn: { zh: '确认开启', en: 'Confirm & enable', ja: '確認して有効にする', ko: '확인 후 켜기', ru: 'Подтвердить и включить', es: 'Confirmar y activar' },
totpEnabledOk: { zh: '双重认证已开启', en: 'Two-factor auth enabled', ja: '二要素認証を有効にしました', ko: '2단계 인증이 켜졌습니다', ru: 'Двухфакторная аутентификация включена', es: 'Verificación en dos pasos activada' },
totpDisabledOk: { zh: '双重认证已关闭', en: 'Two-factor auth disabled', ja: '二要素認証を無効にしました', ko: '2단계 인증이 꺼졌습니다', ru: 'Двухфакторная аутентификация отключена', es: 'Verificación en dos pasos desactivada' },
totpDisableHint: { zh: '输入当前动态码以关闭双重认证。', en: 'Enter your current code to disable 2FA.', ja: '二要素認証を無効にするには現在のコードを入力してください。', ko: '2단계 인증을 끄려면 현재 코드를 입력하세요.', ru: 'Введите текущий код, чтобы отключить 2FA.', es: 'Introduce tu código actual para desactivar la 2FA.' },
copyKey: { zh: '复制密钥', en: 'Copy key', ja: 'キーをコピー', ko: '키 복사', ru: 'Копировать ключ', es: 'Copiar clave' },
/* generic errors */
errNetwork: { zh: '网络异常,请稍后重试', en: 'Network error, please retry', ja: 'ネットワークエラーです。後でもう一度お試しください', ko: '네트워크 오류입니다. 잠시 후 다시 시도하세요', ru: 'Ошибка сети, повторите попытку', es: 'Error de red, vuelve a intentarlo' },
errUnknown: { zh: '操作失败,请稍后重试', en: 'Something went wrong, please retry', ja: '問題が発生しました。後でもう一度お試しください', ko: '문제가 발생했습니다. 잠시 후 다시 시도하세요', ru: 'Что-то пошло не так, повторите попытку', es: 'Algo salió mal, vuelve a intentarlo' },
loading: { zh: '加载中…', en: 'Loading…', ja: '読み込み中…', ko: '불러오는 중…', ru: 'Загрузка…', es: 'Cargando…' },
};
export type TFn = (key: string, vars?: Record<string, string | number>) => string;
export function makeT(lang: Lang): TFn {
return (key, vars) => {
const e = STRINGS[key];
let s = e ? e[lang] || e.en || key : key;
if (vars) for (const k of Object.keys(vars)) s = s.replace(`{${k}}`, String(vars[k]));
return s;
};
}