From f76e3cdffdaa9726c16d7d2c3a90844e17a72b72 Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Mon, 27 Jul 2026 08:47:44 +0800 Subject: [PATCH] =?UTF-8?q?feat(i18n):=20l10n=20codegen=20=E2=80=94=20stri?= =?UTF-8?q?ngs.json=20=E5=8D=95=E6=BA=90=E7=94=9F=E6=88=90=206=20=E4=BB=BD?= =?UTF-8?q?=20strings=5F*.dart?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ② 单源 codegen 落地: - strings.json 补全 lists 段(days7/featsFree/featsPro/featsTeam), 之前 extraction 漏了 4 个 List getter;现 246 getter+4 list+5 method×6 语。 - design/codegen/gen_l10n_dart.mjs:strings.json → strings_{zh,en,ja,ko,ru,es}.dart, getter 转义 \$、method 保留 $var 插值,dart format 定型(保证漂移闸零 diff)。 - app_text.dart 保持手写(enum/abstract 声明/AppLangMisc 逻辑),编译器强制 concrete⊆abstract 兜底。 - 往返无损:生成的 Dart 重新抽取 == strings.json 逐字节一致;flutter analyze 0 issue、flutter test 244 全绿。 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01A79VtQA1BwTuQN1ThpvYpo --- client/lib/l10n/strings_en.dart | 124 +++++++++--------- client/lib/l10n/strings_es.dart | 149 +++++++++++---------- client/lib/l10n/strings_ja.dart | 97 +++++++------- client/lib/l10n/strings_ko.dart | 97 +++++++------- client/lib/l10n/strings_ru.dart | 149 +++++++++++---------- client/lib/l10n/strings_zh.dart | 79 ++++++----- design/codegen/gen_l10n_dart.mjs | 83 ++++++++++++ design/codegen/migrate_l10n_to_json.py | 82 +++++++----- design/i18n/strings.json | 174 ++++++++++++++++++++++++- 9 files changed, 662 insertions(+), 372 deletions(-) create mode 100644 design/codegen/gen_l10n_dart.mjs diff --git a/client/lib/l10n/strings_en.dart b/client/lib/l10n/strings_en.dart index a521855..7f60d33 100644 --- a/client/lib/l10n/strings_en.dart +++ b/client/lib/l10n/strings_en.dart @@ -1,8 +1,7 @@ -// strings_en.dart — English copy resources (single-language display) +// strings_en.dart — English文案(自动生成 · 勿手改) // -// Red-line words forbidden — see design/CLAUDE.md §1 rule 13 (no censorship-circumvention terms). -// Positioning: network acceleration / experience optimization + privacy. -// Plan numbers follow design/CLAUDE.md §7. +// GENERATED by design/codegen/gen_l10n_dart.mjs from design/i18n/strings.json. +// 改文案:改 strings.json 后重跑 codegen;新增 key 需同步 app_text.dart 抽象声明。 import 'app_text.dart'; class StringsEn extends AppText { @@ -27,7 +26,6 @@ class StringsEn extends AppText { String get connectNow => 'CONNECT'; @override String get secure => 'SECURE'; - @override String get tabConnect => 'Connect'; @override @@ -36,7 +34,6 @@ class StringsEn extends AppText { String get tabStats => 'Stats'; @override String get tabMe => 'Account'; - @override String get currentNode => 'Current node'; @override @@ -45,7 +42,6 @@ class StringsEn extends AppText { String get upload => 'Up'; @override String get latency => 'Ping'; - @override String get quotaToday => 'Left today'; @override @@ -65,20 +61,19 @@ class StringsEn extends AppText { @override String get watchAdMore => 'Watch ad for more time'; @override - String get quotaExhaustedNotice => "You've used today's free time. Upgrade to keep connecting."; + String get quotaExhaustedNotice => + 'You\'ve used today\'s free time. Upgrade to keep connecting.'; @override String get adPlaying => 'Ad playing…'; @override - String adRewarded(int minutes) => '+$minutes min added'; - @override String get adFailed => 'Failed to add time, please retry'; @override String get quotaDesktopTitle => 'Daily free time used up'; @override - String get quotaDesktopBody => 'Watch ads in the mobile app to add time, or upgrade for unlimited access.'; + String get quotaDesktopBody => + 'Watch ads in the mobile app to add time, or upgrade for unlimited access.'; @override String get gotIt => 'Got it'; - @override String get chooseNode => 'Choose server'; @override @@ -86,22 +81,22 @@ class StringsEn extends AppText { @override String get smartSelect => 'Smart select'; @override - String nodeSwitchTitle(String name) => 'Switch to $name?'; - @override - String get nodeSwitchBody => 'This disconnects the current link and reconnects to this node.'; + String get nodeSwitchBody => + 'This disconnects the current link and reconnects to this node.'; @override String get nodeSwitchConfirm => 'Switch'; @override - String get nodeUnhealthySwitched => 'Current node unreachable — switched automatically'; + String get nodeUnhealthySwitched => + 'Current node unreachable — switched automatically'; @override - String get nodeUnhealthyError => 'Current node is unreachable — reconnect or switch node'; + String get nodeUnhealthyError => + 'Current node is unreachable — reconnect or switch node'; @override String get nodeReconnecting => 'Network unstable, reconnecting…'; @override String get smartSub => 'Picks the best node for your network'; @override String get recommended => 'Recommended'; - @override String get statsTitle => 'Statistics'; @override @@ -113,8 +108,6 @@ class StringsEn extends AppText { @override String get weekTraffic => 'This week (GB)'; @override - List get days7 => const ['M', 'T', 'W', 'T', 'F', 'S', 'S']; - @override String get byDevice => 'By device'; @override String get noDeviceUsage => 'No device usage yet'; @@ -134,7 +127,6 @@ class StringsEn extends AppText { String get chartTwoWeeks => 'Last 2 weeks'; @override String get allDevices => 'All devices'; - @override String get meTitle => 'Account'; @override @@ -156,7 +148,8 @@ class StringsEn extends AppText { @override String get deviceLimitTitle => 'Device limit reached'; @override - String get deviceLimitDesc => 'Your plan allows up to %s devices. Remove one to continue.'; + String get deviceLimitDesc => + 'Your plan allows up to %s devices. Remove one to continue.'; @override String get deviceLimitRemoveOldest => 'Remove least recently used'; @override @@ -186,15 +179,18 @@ class StringsEn extends AppText { @override String get devSave => 'Save'; @override - String get devForceLogoutConfirm => 'This revokes the device\'s login session — it will be kicked offline and must sign in again. The device stays in the list.'; + String get devForceLogoutConfirm => + 'This revokes the device\'s login session — it will be kicked offline and must sign in again. The device stays in the list.'; @override - String get devClearLoginConfirm => 'This removes the device from the list entirely and revokes its login session and data-plane credential. The device must sign in again to be used.'; + String get devClearLoginConfirm => + 'This removes the device from the list entirely and revokes its login session and data-plane credential. The device must sign in again to be used.'; @override String get devCancel => 'Cancel'; @override String get sessionRevokedTitle => 'This device was signed out'; @override - String get sessionRevokedBody => 'Your account signed this device out from another device. Please sign in again.'; + String get sessionRevokedBody => + 'Your account signed this device out from another device. Please sign in again.'; @override String get sessionRevokedOk => 'Sign in again'; @override @@ -224,8 +220,6 @@ class StringsEn extends AppText { @override String get autoConnectSub => 'Connect automatically on launch'; @override - String autoConnectingTo(String name) => 'Auto-connecting to $name…'; - @override String get smartRoute => 'Smart routing'; @override String get smartRouteSub => 'Accelerate abroad, direct at home'; @@ -238,9 +232,8 @@ class StringsEn extends AppText { @override String get webUserCenter => 'User Center (Web)'; @override - String updateAvailableTitle(String version) => 'New version v$version available'; - @override - String get updateNotesFallback => 'This update includes fixes and stability improvements.'; + String get updateNotesFallback => + 'This update includes fixes and stability improvements.'; @override String get updateLater => 'Later'; @override @@ -249,7 +242,6 @@ class StringsEn extends AppText { String get updateUpToDate => 'You are up to date'; @override String get updateCheckFailed => 'Update check failed, try again later'; - @override String get freePlan => 'Free'; @override @@ -266,20 +258,6 @@ class StringsEn extends AppText { String get choose => 'Choose'; @override String get mostPopular => 'Popular'; - @override - List get featsFree => const [ - '1 basic node only', - '10 min per day', - 'Watch an ad to start', - '7-day free trial on sign-up', - ]; - @override - List get featsPro => - const ['80+ locations', 'Unlimited · fast', '5 devices', 'Streaming optimized']; - @override - List get featsTeam => - const ['Everything in Pro', '10 seats', 'Central billing', 'Priority support']; - @override String get redeemTitle => 'Redeem & buy'; @override @@ -298,7 +276,6 @@ class StringsEn extends AppText { String get chStore => 'Self-serve store'; @override String get chEmail => 'Email support'; - @override String get contactTitle => 'Contact us'; @override @@ -314,7 +291,6 @@ class StringsEn extends AppText { String get contactHoursTitle => 'Support hours'; @override String get contactHours => 'Daily 9:00 – 24:00 (GMT+8)'; - @override String get authTagline => 'Fast · Stable · Effortless'; @override @@ -334,8 +310,6 @@ class StringsEn extends AppText { @override String get codeLabel => 'Verification code'; @override - String codeSentTo(String email) => 'Code sent to $email'; - @override String get sendCode => 'Send code'; @override String get resend => 'Resend'; @@ -349,7 +323,6 @@ class StringsEn extends AppText { String get forgotPw => 'Forgot password?'; @override String get tos => 'By continuing you agree to our Terms & Privacy Policy'; - @override String get obSkip => 'Skip'; @override @@ -365,12 +338,12 @@ class StringsEn extends AppText { @override String get ob2Title => 'Allow network setup'; @override - String get ob2Sub => 'The system will ask to add a network profile to build the encrypted tunnel.'; + String get ob2Sub => + 'The system will ask to add a network profile to build the encrypted tunnel.'; @override String get ob3Title => 'Private by design'; @override String get ob3Sub => 'End-to-end encrypted. We keep zero browsing logs.'; - @override String get purchaseTitle => 'Purchase'; @override @@ -400,15 +373,18 @@ class StringsEn extends AppText { @override String get payNetworkLabel => 'Network'; @override - String get payExactAmountHint => 'Send the exact amount; activates automatically'; + String get payExactAmountHint => + 'Send the exact amount; activates automatically'; @override String get copied => 'Copied'; @override String get openAlipay => 'Pay with Alipay'; @override - String get openAlipayHint => 'Return after paying; this page refreshes automatically'; + String get openAlipayHint => + 'Return after paying; this page refreshes automatically'; @override - String get openAlipayFailed => "Couldn't open Alipay; check it's installed or try again"; + String get openAlipayFailed => + 'Couldn\'t open Alipay; check it\'s installed or try again'; @override String get awaitingPayment => 'Awaiting payment'; @override @@ -433,8 +409,6 @@ class StringsEn extends AppText { String get payUnsupportedRenderType => 'Unsupported, switch payment method'; @override String get payNoActiveOrder => 'No active order'; - - // ── 支付渠道 switch + 订单 ── @override String get payChannel => 'Payment channel'; @override @@ -473,8 +447,6 @@ class StringsEn extends AppText { String get orderRebuy => 'Buy again'; @override String get payMethodNezha => 'Nazhapay'; - - // ── 邀请 / 通知 / 设置分组 / 订单过期(Spec ②③占位) ── @override String get inviteEntry => 'Invite friends, earn membership'; @override @@ -545,4 +517,38 @@ class StringsEn extends AppText { String get promoBadge => 'Limited-time offer'; @override String get promoOriginalPrice => 'Was'; + @override + List get days7 => const ['M', 'T', 'W', 'T', 'F', 'S', 'S']; + @override + List get featsFree => const [ + '1 basic node only', + '10 min per day', + 'Watch an ad to start', + '7-day free trial on sign-up', + ]; + @override + List get featsPro => const [ + '80+ locations', + 'Unlimited · fast', + '5 devices', + 'Streaming optimized', + ]; + @override + List get featsTeam => const [ + 'Everything in Pro', + '10 seats', + 'Central billing', + 'Priority support', + ]; + @override + String adRewarded(int minutes) => '+$minutes min added'; + @override + String nodeSwitchTitle(String name) => 'Switch to $name?'; + @override + String autoConnectingTo(String name) => 'Auto-connecting to $name…'; + @override + String updateAvailableTitle(String version) => + 'New version v$version available'; + @override + String codeSentTo(String email) => 'Code sent to $email'; } diff --git a/client/lib/l10n/strings_es.dart b/client/lib/l10n/strings_es.dart index d20fe0e..10f72ae 100644 --- a/client/lib/l10n/strings_es.dart +++ b/client/lib/l10n/strings_es.dart @@ -1,8 +1,7 @@ -// strings_es.dart — Recursos de texto en español (visualización monolingüe) +// strings_es.dart — Español文案(自动生成 · 勿手改) // -// Palabras prohibidas: términos de evasión de censura. Lista en design/CLAUDE.md §1 regla 13. -// Posicionamiento: aceleración de red / optimización de la experiencia + privacidad. -// La numeración de planes sigue design/CLAUDE.md §7. +// GENERATED by design/codegen/gen_l10n_dart.mjs from design/i18n/strings.json. +// 改文案:改 strings.json 后重跑 codegen;新增 key 需同步 app_text.dart 抽象声明。 import 'app_text.dart'; class StringsEs extends AppText { @@ -27,7 +26,6 @@ class StringsEs extends AppText { String get connectNow => 'CONECTAR'; @override String get secure => 'SEGURO'; - @override String get tabConnect => 'Conectar'; @override @@ -36,7 +34,6 @@ class StringsEs extends AppText { String get tabStats => 'Estadísticas'; @override String get tabMe => 'Cuenta'; - @override String get currentNode => 'Nodo actual'; @override @@ -45,7 +42,6 @@ class StringsEs extends AppText { String get upload => 'Subida'; @override String get latency => 'Ping'; - @override String get quotaToday => 'Restante hoy'; @override @@ -65,20 +61,19 @@ class StringsEs extends AppText { @override String get watchAdMore => 'Mira un anuncio para más tiempo'; @override - String get quotaExhaustedNotice => 'Tiempo gratis diario agotado. Mejora tu plan para seguir conectado.'; + String get quotaExhaustedNotice => + 'Tiempo gratis diario agotado. Mejora tu plan para seguir conectado.'; @override String get adPlaying => 'Reproduciendo anuncio…'; @override - String adRewarded(int minutes) => '+$minutes min añadidos'; - @override String get adFailed => 'No se pudo añadir tiempo, inténtalo de nuevo'; @override String get quotaDesktopTitle => 'Tiempo gratis diario agotado'; @override - String get quotaDesktopBody => 'Mira anuncios en la app móvil para añadir tiempo, o mejora tu plan para acceso ilimitado.'; + String get quotaDesktopBody => + 'Mira anuncios en la app móvil para añadir tiempo, o mejora tu plan para acceso ilimitado.'; @override String get gotIt => 'Entendido'; - @override String get chooseNode => 'Elegir servidor'; @override @@ -86,22 +81,22 @@ class StringsEs extends AppText { @override String get smartSelect => 'Selección inteligente'; @override - String nodeSwitchTitle(String name) => '¿Cambiar a $name?'; - @override - String get nodeSwitchBody => 'Esto desconecta el enlace actual y reconecta a este nodo.'; + String get nodeSwitchBody => + 'Esto desconecta el enlace actual y reconecta a este nodo.'; @override String get nodeSwitchConfirm => 'Cambiar'; @override - String get nodeUnhealthySwitched => 'Nodo actual inaccesible: cambiado automáticamente'; + String get nodeUnhealthySwitched => + 'Nodo actual inaccesible: cambiado automáticamente'; @override - String get nodeUnhealthyError => 'El nodo actual está inaccesible: reconecta o cambia de nodo'; + String get nodeUnhealthyError => + 'El nodo actual está inaccesible: reconecta o cambia de nodo'; @override String get nodeReconnecting => 'Red inestable, reconectando…'; @override String get smartSub => 'Elige el mejor nodo para tu red'; @override String get recommended => 'Recomendado'; - @override String get statsTitle => 'Estadísticas'; @override @@ -113,8 +108,6 @@ class StringsEs extends AppText { @override String get weekTraffic => 'Esta semana (GB)'; @override - List get days7 => const ['L', 'M', 'X', 'J', 'V', 'S', 'D']; - @override String get byDevice => 'Por dispositivo'; @override String get noDeviceUsage => 'Aún no hay uso por dispositivo'; @@ -134,7 +127,6 @@ class StringsEs extends AppText { String get chartTwoWeeks => 'Últimas 2 semanas'; @override String get allDevices => 'Todos los dispositivos'; - @override String get meTitle => 'Cuenta'; @override @@ -156,7 +148,8 @@ class StringsEs extends AppText { @override String get deviceLimitTitle => 'Límite de dispositivos alcanzado'; @override - String get deviceLimitDesc => 'Tu plan permite hasta %s dispositivos. Elimina uno para continuar.'; + String get deviceLimitDesc => + 'Tu plan permite hasta %s dispositivos. Elimina uno para continuar.'; @override String get deviceLimitRemoveOldest => 'Eliminar el menos usado recientemente'; @override @@ -186,15 +179,18 @@ class StringsEs extends AppText { @override String get devSave => 'Guardar'; @override - String get devForceLogoutConfirm => 'Esto revoca la sesión del dispositivo: será desconectado y deberá iniciar sesión de nuevo. El dispositivo permanece en la lista.'; + String get devForceLogoutConfirm => + 'Esto revoca la sesión del dispositivo: será desconectado y deberá iniciar sesión de nuevo. El dispositivo permanece en la lista.'; @override - String get devClearLoginConfirm => 'Esto elimina el dispositivo de la lista por completo y revoca su sesión y su credencial del plano de datos. El dispositivo deberá iniciar sesión de nuevo para poder usarse.'; + String get devClearLoginConfirm => + 'Esto elimina el dispositivo de la lista por completo y revoca su sesión y su credencial del plano de datos. El dispositivo deberá iniciar sesión de nuevo para poder usarse.'; @override String get devCancel => 'Cancelar'; @override String get sessionRevokedTitle => 'Se cerró la sesión de este dispositivo'; @override - String get sessionRevokedBody => 'Tu cuenta cerró la sesión de este dispositivo desde otro dispositivo. Inicia sesión de nuevo.'; + String get sessionRevokedBody => + 'Tu cuenta cerró la sesión de este dispositivo desde otro dispositivo. Inicia sesión de nuevo.'; @override String get sessionRevokedOk => 'Iniciar sesión de nuevo'; @override @@ -224,8 +220,6 @@ class StringsEs extends AppText { @override String get autoConnectSub => 'Conectar automáticamente al abrir'; @override - String autoConnectingTo(String name) => 'Conectando automáticamente a $name…'; - @override String get smartRoute => 'Enrutamiento inteligente'; @override String get smartRouteSub => 'Acelera en el extranjero, directo en casa'; @@ -238,9 +232,8 @@ class StringsEs extends AppText { @override String get webUserCenter => 'Centro de usuario (Web)'; @override - String updateAvailableTitle(String version) => 'Nueva versión v$version disponible'; - @override - String get updateNotesFallback => 'Esta actualización incluye correcciones y mejoras de estabilidad.'; + String get updateNotesFallback => + 'Esta actualización incluye correcciones y mejoras de estabilidad.'; @override String get updateLater => 'Más tarde'; @override @@ -248,8 +241,8 @@ class StringsEs extends AppText { @override String get updateUpToDate => 'Ya tienes la última versión'; @override - String get updateCheckFailed => 'Falló la búsqueda de actualizaciones, inténtalo más tarde'; - + String get updateCheckFailed => + 'Falló la búsqueda de actualizaciones, inténtalo más tarde'; @override String get freePlan => 'Gratis'; @override @@ -266,20 +259,6 @@ class StringsEs extends AppText { String get choose => 'Elegir'; @override String get mostPopular => 'Popular'; - @override - List get featsFree => const [ - 'Solo 1 nodo básico', - '10 min al día', - 'Mira un anuncio para empezar', - 'Prueba gratis de 7 días al registrarte', - ]; - @override - List get featsPro => - const ['Más de 80 ubicaciones', 'Ilimitado · rápido', '5 dispositivos', 'Optimizado para streaming']; - @override - List get featsTeam => - const ['Todo lo de Pro', '10 plazas', 'Facturación centralizada', 'Soporte prioritario']; - @override String get redeemTitle => 'Canjear y comprar'; @override @@ -293,12 +272,12 @@ class StringsEs extends AppText { @override String get buyTitle => 'Dónde comprar'; @override - String get buySub => 'El pago dentro de la app no está disponible. Consigue un código mediante:'; + String get buySub => + 'El pago dentro de la app no está disponible. Consigue un código mediante:'; @override String get chStore => 'Tienda autoservicio'; @override String get chEmail => 'Soporte por correo'; - @override String get contactTitle => 'Contáctanos'; @override @@ -314,7 +293,6 @@ class StringsEs extends AppText { String get contactHoursTitle => 'Horario de soporte'; @override String get contactHours => 'Todos los días 9:00 – 24:00 (GMT+8)'; - @override String get authTagline => 'Rápido · Estable · Sin complicaciones'; @override @@ -334,8 +312,6 @@ class StringsEs extends AppText { @override String get codeLabel => 'Código de verificación'; @override - String codeSentTo(String email) => 'Código enviado a $email'; - @override String get sendCode => 'Enviar código'; @override String get resend => 'Reenviar'; @@ -348,8 +324,8 @@ class StringsEs extends AppText { @override String get forgotPw => '¿Olvidaste la contraseña?'; @override - String get tos => 'Al continuar aceptas nuestros Términos y Política de privacidad'; - + String get tos => + 'Al continuar aceptas nuestros Términos y Política de privacidad'; @override String get obSkip => 'Omitir'; @override @@ -361,17 +337,18 @@ class StringsEs extends AppText { @override String get ob1Title => 'Conéctate en todo el mundo'; @override - String get ob1Sub => 'Más de 80 ubicaciones, enrutamiento al más rápido automático, sólido como una roca.'; + String get ob1Sub => + 'Más de 80 ubicaciones, enrutamiento al más rápido automático, sólido como una roca.'; @override String get ob2Title => 'Permite la configuración de red'; @override - String get ob2Sub => 'El sistema te pedirá añadir un perfil de red para crear el túnel cifrado.'; + String get ob2Sub => + 'El sistema te pedirá añadir un perfil de red para crear el túnel cifrado.'; @override String get ob3Title => 'Privado por diseño'; @override - String get ob3Sub => 'Cifrado de extremo a extremo. No guardamos ningún registro de navegación.'; - - // ── 购买 / 支付 ── + String get ob3Sub => + 'Cifrado de extremo a extremo. No guardamos ningún registro de navegación.'; @override String get purchaseTitle => 'Comprar plan'; @override @@ -401,15 +378,18 @@ class StringsEs extends AppText { @override String get payNetworkLabel => 'Red'; @override - String get payExactAmountHint => 'Envía el importe exacto; se activa automáticamente'; + String get payExactAmountHint => + 'Envía el importe exacto; se activa automáticamente'; @override String get copied => 'Copiado'; @override String get openAlipay => 'Pagar con Alipay'; @override - String get openAlipayHint => 'Vuelve tras pagar; esta página se actualiza automáticamente'; + String get openAlipayHint => + 'Vuelve tras pagar; esta página se actualiza automáticamente'; @override - String get openAlipayFailed => 'No se pudo abrir Alipay; comprueba que esté instalado o inténtalo de nuevo'; + String get openAlipayFailed => + 'No se pudo abrir Alipay; comprueba que esté instalado o inténtalo de nuevo'; @override String get awaitingPayment => 'Esperando el pago'; @override @@ -429,13 +409,13 @@ class StringsEs extends AppText { @override String get qrNotSupported => 'Copia el contenido y ábrelo en Alipay'; @override - String get orderCreateFailed => 'Error al crear el pedido, inténtalo de nuevo más tarde'; + String get orderCreateFailed => + 'Error al crear el pedido, inténtalo de nuevo más tarde'; @override - String get payUnsupportedRenderType => 'No compatible, cambia el método de pago'; + String get payUnsupportedRenderType => + 'No compatible, cambia el método de pago'; @override String get payNoActiveOrder => 'No hay pedidos activos'; - - // ── 支付渠道 switch + 订单 ── @override String get payChannel => 'Canal de pago'; @override @@ -474,8 +454,6 @@ class StringsEs extends AppText { String get orderRebuy => 'Comprar de nuevo'; @override String get payMethodNezha => 'Nazhapay'; - - // ── 邀请 / 通知 / 设置分组 / 订单过期(Spec ②③占位) ── @override String get inviteEntry => 'Invita a amigos y gana membresía'; @override @@ -495,7 +473,8 @@ class StringsEs extends AppText { @override String get inviteRewardRegister => 'Registro de amigo · +3 días cada uno'; @override - String get inviteRewardFirstBuy => 'Primera compra del amigo · +7 días cada uno'; + String get inviteRewardFirstBuy => + 'Primera compra del amigo · +7 días cada uno'; @override String get inviteRewardJoinTg => 'Unirse al canal de TG · +3 días'; @override @@ -546,4 +525,38 @@ class StringsEs extends AppText { String get promoBadge => 'Oferta especial'; @override String get promoOriginalPrice => 'Antes'; + @override + List get days7 => const ['L', 'M', 'X', 'J', 'V', 'S', 'D']; + @override + List get featsFree => const [ + 'Solo 1 nodo básico', + '10 min al día', + 'Mira un anuncio para empezar', + 'Prueba gratis de 7 días al registrarte', + ]; + @override + List get featsPro => const [ + 'Más de 80 ubicaciones', + 'Ilimitado · rápido', + '5 dispositivos', + 'Optimizado para streaming', + ]; + @override + List get featsTeam => const [ + 'Todo lo de Pro', + '10 plazas', + 'Facturación centralizada', + 'Soporte prioritario', + ]; + @override + String adRewarded(int minutes) => '+$minutes min añadidos'; + @override + String nodeSwitchTitle(String name) => '¿Cambiar a $name?'; + @override + String autoConnectingTo(String name) => 'Conectando automáticamente a $name…'; + @override + String updateAvailableTitle(String version) => + 'Nueva versión v$version disponible'; + @override + String codeSentTo(String email) => 'Código enviado a $email'; } diff --git a/client/lib/l10n/strings_ja.dart b/client/lib/l10n/strings_ja.dart index f5762bc..9f48d1f 100644 --- a/client/lib/l10n/strings_ja.dart +++ b/client/lib/l10n/strings_ja.dart @@ -1,8 +1,7 @@ -// strings_ja.dart — 日本語の文言リソース(単一言語表示) +// strings_ja.dart — 日本語文案(自动生成 · 勿手改) // -// 禁止ワード: 検閲回避・センシティブな表現。一覧は design/CLAUDE.md §1 ルール13を参照。 -// ポジショニング: ネットワーク高速化 / 体験最適化 + プライバシー。 -// プラン番号は design/CLAUDE.md §7 に準拠。 +// GENERATED by design/codegen/gen_l10n_dart.mjs from design/i18n/strings.json. +// 改文案:改 strings.json 后重跑 codegen;新增 key 需同步 app_text.dart 抽象声明。 import 'app_text.dart'; class StringsJa extends AppText { @@ -27,7 +26,6 @@ class StringsJa extends AppText { String get connectNow => '接続'; @override String get secure => 'セキュア'; - @override String get tabConnect => '接続'; @override @@ -36,7 +34,6 @@ class StringsJa extends AppText { String get tabStats => '統計'; @override String get tabMe => 'アカウント'; - @override String get currentNode => '現在のノード'; @override @@ -45,7 +42,6 @@ class StringsJa extends AppText { String get upload => '上り'; @override String get latency => 'Ping'; - @override String get quotaToday => '本日の残り'; @override @@ -69,16 +65,14 @@ class StringsJa extends AppText { @override String get adPlaying => '広告を再生中…'; @override - String adRewarded(int minutes) => '+$minutes 分を追加しました'; - @override String get adFailed => '時間の追加に失敗しました。もう一度お試しください'; @override String get quotaDesktopTitle => '本日の無料時間を使い切りました'; @override - String get quotaDesktopBody => 'モバイルアプリで広告を見ると時間を追加できます。無制限に使うにはアップグレードしてください。'; + String get quotaDesktopBody => + 'モバイルアプリで広告を見ると時間を追加できます。無制限に使うにはアップグレードしてください。'; @override String get gotIt => '了解'; - @override String get chooseNode => 'サーバーを選択'; @override @@ -86,8 +80,6 @@ class StringsJa extends AppText { @override String get smartSelect => 'スマート選択'; @override - String nodeSwitchTitle(String name) => '$name に切り替えますか?'; - @override String get nodeSwitchBody => '現在の接続を切断し、このノードに再接続します。'; @override String get nodeSwitchConfirm => '切り替える'; @@ -101,7 +93,6 @@ class StringsJa extends AppText { String get smartSub => 'お使いのネットワークに最適なノードを選びます'; @override String get recommended => 'おすすめ'; - @override String get statsTitle => '統計'; @override @@ -113,8 +104,6 @@ class StringsJa extends AppText { @override String get weekTraffic => '今週 (GB)'; @override - List get days7 => const ['月', '火', '水', '木', '金', '土', '日']; - @override String get byDevice => 'デバイス別'; @override String get noDeviceUsage => 'デバイスの利用履歴はまだありません'; @@ -134,7 +123,6 @@ class StringsJa extends AppText { String get chartTwoWeeks => '過去2週間'; @override String get allDevices => 'すべてのデバイス'; - @override String get meTitle => 'アカウント'; @override @@ -186,15 +174,18 @@ class StringsJa extends AppText { @override String get devSave => '保存'; @override - String get devForceLogoutConfirm => 'このデバイスのログインセッションを無効にします。オフラインになり、再度サインインが必要になります。デバイスはリストに残ります。'; + String get devForceLogoutConfirm => + 'このデバイスのログインセッションを無効にします。オフラインになり、再度サインインが必要になります。デバイスはリストに残ります。'; @override - String get devClearLoginConfirm => 'このデバイスをリストから完全に削除し、ログインセッションとデータ通信の認証情報を無効にします。再び利用するには、もう一度サインインが必要です。'; + String get devClearLoginConfirm => + 'このデバイスをリストから完全に削除し、ログインセッションとデータ通信の認証情報を無効にします。再び利用するには、もう一度サインインが必要です。'; @override String get devCancel => 'キャンセル'; @override String get sessionRevokedTitle => 'このデバイスはサインアウトされました'; @override - String get sessionRevokedBody => '別のデバイスから、このデバイスがサインアウトされました。もう一度サインインしてください。'; + String get sessionRevokedBody => + '別のデバイスから、このデバイスがサインアウトされました。もう一度サインインしてください。'; @override String get sessionRevokedOk => '再度サインイン'; @override @@ -224,8 +215,6 @@ class StringsJa extends AppText { @override String get autoConnectSub => '起動時に自動的に接続'; @override - String autoConnectingTo(String name) => '$name に自動接続中…'; - @override String get smartRoute => 'スマートルーティング'; @override String get smartRouteSub => '海外は高速化、国内は直接接続'; @@ -238,8 +227,6 @@ class StringsJa extends AppText { @override String get webUserCenter => 'ユーザーセンター (Web)'; @override - String updateAvailableTitle(String version) => '新しいバージョン v$version が利用可能です'; - @override String get updateNotesFallback => 'このアップデートには不具合の修正と安定性の向上が含まれています。'; @override String get updateLater => '後で'; @@ -249,7 +236,6 @@ class StringsJa extends AppText { String get updateUpToDate => '最新の状態です'; @override String get updateCheckFailed => 'アップデートの確認に失敗しました。後でもう一度お試しください'; - @override String get freePlan => '無料'; @override @@ -266,20 +252,6 @@ class StringsJa extends AppText { String get choose => '選択'; @override String get mostPopular => '人気'; - @override - List get featsFree => const [ - '基本ノード1つのみ', - '1日10分', - '広告を見て開始', - '登録で7日間無料トライアル', - ]; - @override - List get featsPro => - const ['80以上の拠点', '無制限 · 高速', '5台のデバイス', 'ストリーミング最適化']; - @override - List get featsTeam => - const ['Proのすべての機能', '10ライセンス', '一括請求', '優先サポート']; - @override String get redeemTitle => 'コード引き換え・購入'; @override @@ -298,12 +270,10 @@ class StringsJa extends AppText { String get chStore => 'セルフサービスストア'; @override String get chEmail => 'メールサポート'; - @override String get contactTitle => 'お問い合わせ'; @override - String get contactIntro => - 'お困りですか?以下のいずれかの方法でお問い合わせください。通常は数分以内に返信します。'; + String get contactIntro => 'お困りですか?以下のいずれかの方法でお問い合わせください。通常は数分以内に返信します。'; @override String get contactEmail => 'メールサポート'; @override @@ -314,7 +284,6 @@ class StringsJa extends AppText { String get contactHoursTitle => 'サポート時間'; @override String get contactHours => '毎日 9:00 – 24:00 (GMT+8)'; - @override String get authTagline => '高速 · 安定 · かんたん'; @override @@ -334,8 +303,6 @@ class StringsJa extends AppText { @override String get codeLabel => '確認コード'; @override - String codeSentTo(String email) => '$email に確認コードを送信しました'; - @override String get sendCode => 'コードを送信'; @override String get resend => '再送信'; @@ -349,7 +316,6 @@ class StringsJa extends AppText { String get forgotPw => 'パスワードをお忘れですか?'; @override String get tos => '続行すると、利用規約とプライバシーポリシーに同意したことになります'; - @override String get obSkip => 'スキップ'; @override @@ -370,8 +336,6 @@ class StringsJa extends AppText { String get ob3Title => '設計段階からプライバシー重視'; @override String get ob3Sub => 'エンドツーエンドで暗号化。閲覧ログは一切保持しません。'; - - // ── 购买 / 支付 ── @override String get purchaseTitle => 'プランを購入'; @override @@ -434,8 +398,6 @@ class StringsJa extends AppText { String get payUnsupportedRenderType => '未対応です。支払い方法を変更してください'; @override String get payNoActiveOrder => '進行中の注文はありません'; - - // ── 支付渠道 switch + 订单 ── @override String get payChannel => '支払いチャネル'; @override @@ -474,8 +436,6 @@ class StringsJa extends AppText { String get orderRebuy => '再購入'; @override String get payMethodNezha => 'Nazhapay'; - - // ── 邀请 / 通知 / 设置分组 / 订单过期(Spec ②③占位) ── @override String get inviteEntry => '友達を招待してメンバーシップを獲得'; @override @@ -546,4 +506,37 @@ class StringsJa extends AppText { String get promoBadge => '期間限定オファー'; @override String get promoOriginalPrice => '通常価格'; + @override + List get days7 => const ['月', '火', '水', '木', '金', '土', '日']; + @override + List get featsFree => const [ + '基本ノード1つのみ', + '1日10分', + '広告を見て開始', + '登録で7日間無料トライアル', + ]; + @override + List get featsPro => const [ + '80以上の拠点', + '無制限 · 高速', + '5台のデバイス', + 'ストリーミング最適化', + ]; + @override + List get featsTeam => const [ + 'Proのすべての機能', + '10ライセンス', + '一括請求', + '優先サポート', + ]; + @override + String adRewarded(int minutes) => '+$minutes 分を追加しました'; + @override + String nodeSwitchTitle(String name) => '$name に切り替えますか?'; + @override + String autoConnectingTo(String name) => '$name に自動接続中…'; + @override + String updateAvailableTitle(String version) => '新しいバージョン v$version が利用可能です'; + @override + String codeSentTo(String email) => '$email に確認コードを送信しました'; } diff --git a/client/lib/l10n/strings_ko.dart b/client/lib/l10n/strings_ko.dart index 78da5bc..1f5eebc 100644 --- a/client/lib/l10n/strings_ko.dart +++ b/client/lib/l10n/strings_ko.dart @@ -1,8 +1,7 @@ -// strings_ko.dart — 한국어 문구 리소스 (단일 언어 표시) +// strings_ko.dart — 한국어文案(自动生成 · 勿手改) // -// 금지어: 검열 우회·민감 표현. 목록은 design/CLAUDE.md §1 규칙 13 참조. -// 포지셔닝: 네트워크 가속 / 경험 최적화 + 프라이버시. -// 요금제 번호는 design/CLAUDE.md §7 을 따름. +// GENERATED by design/codegen/gen_l10n_dart.mjs from design/i18n/strings.json. +// 改文案:改 strings.json 后重跑 codegen;新增 key 需同步 app_text.dart 抽象声明。 import 'app_text.dart'; class StringsKo extends AppText { @@ -27,7 +26,6 @@ class StringsKo extends AppText { String get connectNow => '연결'; @override String get secure => '보안'; - @override String get tabConnect => '연결'; @override @@ -36,7 +34,6 @@ class StringsKo extends AppText { String get tabStats => '통계'; @override String get tabMe => '계정'; - @override String get currentNode => '현재 노드'; @override @@ -45,7 +42,6 @@ class StringsKo extends AppText { String get upload => '업'; @override String get latency => '지연'; - @override String get quotaToday => '오늘 남은 시간'; @override @@ -65,20 +61,19 @@ class StringsKo extends AppText { @override String get watchAdMore => '광고 시청으로 시간 추가'; @override - String get quotaExhaustedNotice => '오늘 무료 시간을 모두 사용했습니다. 업그레이드하면 계속 연결할 수 있습니다.'; + String get quotaExhaustedNotice => + '오늘 무료 시간을 모두 사용했습니다. 업그레이드하면 계속 연결할 수 있습니다.'; @override String get adPlaying => '광고 재생 중…'; @override - String adRewarded(int minutes) => '+$minutes분 추가됨'; - @override String get adFailed => '시간 추가에 실패했습니다. 다시 시도하세요'; @override String get quotaDesktopTitle => '오늘 무료 시간을 모두 사용함'; @override - String get quotaDesktopBody => '모바일 앱에서 광고를 시청해 시간을 추가하거나, 업그레이드하여 무제한으로 이용하세요.'; + String get quotaDesktopBody => + '모바일 앱에서 광고를 시청해 시간을 추가하거나, 업그레이드하여 무제한으로 이용하세요.'; @override String get gotIt => '확인'; - @override String get chooseNode => '서버 선택'; @override @@ -86,8 +81,6 @@ class StringsKo extends AppText { @override String get smartSelect => '스마트 선택'; @override - String nodeSwitchTitle(String name) => '$name(으)로 전환할까요?'; - @override String get nodeSwitchBody => '현재 연결을 끊고 이 노드로 다시 연결합니다.'; @override String get nodeSwitchConfirm => '전환'; @@ -101,7 +94,6 @@ class StringsKo extends AppText { String get smartSub => '네트워크에 가장 적합한 노드를 선택합니다'; @override String get recommended => '추천'; - @override String get statsTitle => '통계'; @override @@ -113,8 +105,6 @@ class StringsKo extends AppText { @override String get weekTraffic => '이번 주 (GB)'; @override - List get days7 => const ['월', '화', '수', '목', '금', '토', '일']; - @override String get byDevice => '기기별'; @override String get noDeviceUsage => '아직 기기 사용 기록이 없습니다'; @@ -134,7 +124,6 @@ class StringsKo extends AppText { String get chartTwoWeeks => '최근 2주'; @override String get allDevices => '모든 기기'; - @override String get meTitle => '계정'; @override @@ -186,9 +175,11 @@ class StringsKo extends AppText { @override String get devSave => '저장'; @override - String get devForceLogoutConfirm => '이 기기의 로그인 세션을 무효화합니다 — 오프라인으로 강제 종료되며 다시 로그인해야 합니다. 기기는 목록에 남아 있습니다.'; + String get devForceLogoutConfirm => + '이 기기의 로그인 세션을 무효화합니다 — 오프라인으로 강제 종료되며 다시 로그인해야 합니다. 기기는 목록에 남아 있습니다.'; @override - String get devClearLoginConfirm => '이 기기를 목록에서 완전히 삭제하고 로그인 세션과 데이터 전송 자격 증명을 무효화합니다. 다시 사용하려면 재로그인해야 합니다.'; + String get devClearLoginConfirm => + '이 기기를 목록에서 완전히 삭제하고 로그인 세션과 데이터 전송 자격 증명을 무효화합니다. 다시 사용하려면 재로그인해야 합니다.'; @override String get devCancel => '취소'; @override @@ -224,8 +215,6 @@ class StringsKo extends AppText { @override String get autoConnectSub => '실행 시 자동으로 연결'; @override - String autoConnectingTo(String name) => '$name(으)로 자동 연결 중…'; - @override String get smartRoute => '스마트 라우팅'; @override String get smartRouteSub => '해외는 가속, 국내는 직접 연결'; @@ -238,8 +227,6 @@ class StringsKo extends AppText { @override String get webUserCenter => '사용자 센터 (웹)'; @override - String updateAvailableTitle(String version) => '새 버전 v$version 사용 가능'; - @override String get updateNotesFallback => '이번 업데이트에는 버그 수정과 안정성 개선이 포함되어 있습니다.'; @override String get updateLater => '나중에'; @@ -249,7 +236,6 @@ class StringsKo extends AppText { String get updateUpToDate => '최신 버전입니다'; @override String get updateCheckFailed => '업데이트 확인에 실패했습니다. 잠시 후 다시 시도하세요'; - @override String get freePlan => '무료'; @override @@ -266,20 +252,6 @@ class StringsKo extends AppText { String get choose => '선택'; @override String get mostPopular => '인기'; - @override - List get featsFree => const [ - '기본 노드 1개', - '하루 10분', - '광고 시청 후 시작', - '가입 시 7일 무료 체험', - ]; - @override - List get featsPro => - const ['80개 이상 지역', '무제한 · 빠른 속도', '기기 5대', '스트리밍 최적화']; - @override - List get featsTeam => - const ['Pro의 모든 기능', '10인 좌석', '통합 결제', '우선 지원']; - @override String get redeemTitle => '코드 등록 및 구매'; @override @@ -298,12 +270,10 @@ class StringsKo extends AppText { String get chStore => '셀프 스토어'; @override String get chEmail => '이메일 지원'; - @override String get contactTitle => '문의하기'; @override - String get contactIntro => - '도움이 필요하신가요? 아래 채널로 문의하세요 — 보통 몇 분 안에 답변드립니다.'; + String get contactIntro => '도움이 필요하신가요? 아래 채널로 문의하세요 — 보통 몇 분 안에 답변드립니다.'; @override String get contactEmail => '이메일 지원'; @override @@ -314,7 +284,6 @@ class StringsKo extends AppText { String get contactHoursTitle => '지원 시간'; @override String get contactHours => '매일 9:00 – 24:00 (GMT+8)'; - @override String get authTagline => '빠르게 · 안정적으로 · 손쉽게'; @override @@ -334,8 +303,6 @@ class StringsKo extends AppText { @override String get codeLabel => '인증 코드'; @override - String codeSentTo(String email) => '$email(으)로 코드를 보냈습니다'; - @override String get sendCode => '코드 전송'; @override String get resend => '재전송'; @@ -349,7 +316,6 @@ class StringsKo extends AppText { String get forgotPw => '비밀번호를 잊으셨나요?'; @override String get tos => '계속하면 이용약관 및 개인정보 처리방침에 동의하는 것입니다'; - @override String get obSkip => '건너뛰기'; @override @@ -370,8 +336,6 @@ class StringsKo extends AppText { String get ob3Title => '설계부터 프라이버시'; @override String get ob3Sub => '종단 간 암호화. 브라우징 기록을 전혀 저장하지 않습니다.'; - - // ── 购买 / 支付 ── @override String get purchaseTitle => '요금제 구매'; @override @@ -434,8 +398,6 @@ class StringsKo extends AppText { String get payUnsupportedRenderType => '지원되지 않습니다. 결제 방법을 변경하세요'; @override String get payNoActiveOrder => '진행 중인 주문이 없습니다'; - - // ── 支付渠道 switch + 订单 ── @override String get payChannel => '결제 채널'; @override @@ -474,8 +436,6 @@ class StringsKo extends AppText { String get orderRebuy => '다시 구매'; @override String get payMethodNezha => 'Nazhapay'; - - // ── 邀请 / 通知 / 设置分组 / 订单过期(Spec ②③占位) ── @override String get inviteEntry => '친구 초대하고 멤버십 받기'; @override @@ -546,4 +506,37 @@ class StringsKo extends AppText { String get promoBadge => '한정 특가'; @override String get promoOriginalPrice => '정가'; + @override + List get days7 => const ['월', '화', '수', '목', '금', '토', '일']; + @override + List get featsFree => const [ + '기본 노드 1개', + '하루 10분', + '광고 시청 후 시작', + '가입 시 7일 무료 체험', + ]; + @override + List get featsPro => const [ + '80개 이상 지역', + '무제한 · 빠른 속도', + '기기 5대', + '스트리밍 최적화', + ]; + @override + List get featsTeam => const [ + 'Pro의 모든 기능', + '10인 좌석', + '통합 결제', + '우선 지원', + ]; + @override + String adRewarded(int minutes) => '+$minutes분 추가됨'; + @override + String nodeSwitchTitle(String name) => '$name(으)로 전환할까요?'; + @override + String autoConnectingTo(String name) => '$name(으)로 자동 연결 중…'; + @override + String updateAvailableTitle(String version) => '새 버전 v$version 사용 가능'; + @override + String codeSentTo(String email) => '$email(으)로 코드를 보냈습니다'; } diff --git a/client/lib/l10n/strings_ru.dart b/client/lib/l10n/strings_ru.dart index f421ebc..373c004 100644 --- a/client/lib/l10n/strings_ru.dart +++ b/client/lib/l10n/strings_ru.dart @@ -1,8 +1,7 @@ -// strings_ru.dart — русские текстовые ресурсы (отображение на одном языке) +// strings_ru.dart — Русский文案(自动生成 · 勿手改) // -// Запрещённые слова: термины обхода цензуры. Список см. design/CLAUDE.md §1 правило 13. -// Позиционирование: ускорение сети / оптимизация опыта + приватность. -// Номера тарифов соответствуют design/CLAUDE.md §7. +// GENERATED by design/codegen/gen_l10n_dart.mjs from design/i18n/strings.json. +// 改文案:改 strings.json 后重跑 codegen;新增 key 需同步 app_text.dart 抽象声明。 import 'app_text.dart'; class StringsRu extends AppText { @@ -27,7 +26,6 @@ class StringsRu extends AppText { String get connectNow => 'ПОДКЛЮЧИТЬ'; @override String get secure => 'ЗАЩИЩЕНО'; - @override String get tabConnect => 'Подключение'; @override @@ -36,7 +34,6 @@ class StringsRu extends AppText { String get tabStats => 'Статистика'; @override String get tabMe => 'Аккаунт'; - @override String get currentNode => 'Текущий узел'; @override @@ -45,7 +42,6 @@ class StringsRu extends AppText { String get upload => 'Отдача'; @override String get latency => 'Пинг'; - @override String get quotaToday => 'Осталось сегодня'; @override @@ -65,20 +61,19 @@ class StringsRu extends AppText { @override String get watchAdMore => 'Смотрите рекламу, чтобы добавить время'; @override - String get quotaExhaustedNotice => 'Дневной бесплатный лимит исчерпан. Оформите подписку, чтобы продолжить подключение.'; + String get quotaExhaustedNotice => + 'Дневной бесплатный лимит исчерпан. Оформите подписку, чтобы продолжить подключение.'; @override String get adPlaying => 'Реклама воспроизводится…'; @override - String adRewarded(int minutes) => '+$minutes мин добавлено'; - @override String get adFailed => 'Не удалось добавить время, попробуйте ещё раз'; @override String get quotaDesktopTitle => 'Дневной бесплатный лимит исчерпан'; @override - String get quotaDesktopBody => 'Смотрите рекламу в мобильном приложении, чтобы добавить время, или оформите подписку для безлимитного доступа.'; + String get quotaDesktopBody => + 'Смотрите рекламу в мобильном приложении, чтобы добавить время, или оформите подписку для безлимитного доступа.'; @override String get gotIt => 'Понятно'; - @override String get chooseNode => 'Выбор сервера'; @override @@ -86,22 +81,22 @@ class StringsRu extends AppText { @override String get smartSelect => 'Умный выбор'; @override - String nodeSwitchTitle(String name) => 'Переключиться на $name?'; - @override - String get nodeSwitchBody => 'Текущее соединение будет разорвано и восстановлено через этот узел.'; + String get nodeSwitchBody => + 'Текущее соединение будет разорвано и восстановлено через этот узел.'; @override String get nodeSwitchConfirm => 'Переключить'; @override - String get nodeUnhealthySwitched => 'Текущий узел недоступен — переключение выполнено автоматически'; + String get nodeUnhealthySwitched => + 'Текущий узел недоступен — переключение выполнено автоматически'; @override - String get nodeUnhealthyError => 'Текущий узел недоступен — переподключитесь или смените узел'; + String get nodeUnhealthyError => + 'Текущий узел недоступен — переподключитесь или смените узел'; @override String get nodeReconnecting => 'Сеть нестабильна, переподключение…'; @override String get smartSub => 'Подбирает лучший узел для вашей сети'; @override String get recommended => 'Рекомендуется'; - @override String get statsTitle => 'Статистика'; @override @@ -113,8 +108,6 @@ class StringsRu extends AppText { @override String get weekTraffic => 'На этой неделе (ГБ)'; @override - List get days7 => const ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс']; - @override String get byDevice => 'По устройствам'; @override String get noDeviceUsage => 'Пока нет данных по устройствам'; @@ -134,7 +127,6 @@ class StringsRu extends AppText { String get chartTwoWeeks => 'Последние 2 недели'; @override String get allDevices => 'Все устройства'; - @override String get meTitle => 'Аккаунт'; @override @@ -156,7 +148,8 @@ class StringsRu extends AppText { @override String get deviceLimitTitle => 'Достигнут лимит устройств'; @override - String get deviceLimitDesc => 'Ваш тариф допускает до %s устройств. Удалите одно, чтобы продолжить.'; + String get deviceLimitDesc => + 'Ваш тариф допускает до %s устройств. Удалите одно, чтобы продолжить.'; @override String get deviceLimitRemoveOldest => 'Удалить давно неиспользуемое'; @override @@ -186,15 +179,18 @@ class StringsRu extends AppText { @override String get devSave => 'Сохранить'; @override - String get devForceLogoutConfirm => 'Это завершит сеанс входа устройства — оно будет отключено и потребует повторного входа. Устройство останется в списке.'; + String get devForceLogoutConfirm => + 'Это завершит сеанс входа устройства — оно будет отключено и потребует повторного входа. Устройство останется в списке.'; @override - String get devClearLoginConfirm => 'Это полностью удалит устройство из списка и отзовёт его сеанс входа и учётные данные передачи данных. Чтобы снова использовать устройство, нужно войти заново.'; + String get devClearLoginConfirm => + 'Это полностью удалит устройство из списка и отзовёт его сеанс входа и учётные данные передачи данных. Чтобы снова использовать устройство, нужно войти заново.'; @override String get devCancel => 'Отмена'; @override String get sessionRevokedTitle => 'Это устройство было отключено'; @override - String get sessionRevokedBody => 'Вход с этого устройства завершён с другого устройства вашего аккаунта. Пожалуйста, войдите снова.'; + String get sessionRevokedBody => + 'Вход с этого устройства завершён с другого устройства вашего аккаунта. Пожалуйста, войдите снова.'; @override String get sessionRevokedOk => 'Войти снова'; @override @@ -224,11 +220,10 @@ class StringsRu extends AppText { @override String get autoConnectSub => 'Подключаться автоматически при запуске'; @override - String autoConnectingTo(String name) => 'Автоподключение к $name…'; - @override String get smartRoute => 'Умная маршрутизация'; @override - String get smartRouteSub => 'Ускорение для зарубежных ресурсов, прямой доступ для локальных'; + String get smartRouteSub => + 'Ускорение для зарубежных ресурсов, прямой доступ для локальных'; @override String get killSwitch => 'Kill Switch'; @override @@ -238,9 +233,8 @@ class StringsRu extends AppText { @override String get webUserCenter => 'Личный кабинет (веб)'; @override - String updateAvailableTitle(String version) => 'Доступна новая версия v$version'; - @override - String get updateNotesFallback => 'Это обновление включает исправления и улучшения стабильности.'; + String get updateNotesFallback => + 'Это обновление включает исправления и улучшения стабильности.'; @override String get updateLater => 'Позже'; @override @@ -248,8 +242,8 @@ class StringsRu extends AppText { @override String get updateUpToDate => 'У вас установлена последняя версия'; @override - String get updateCheckFailed => 'Не удалось проверить обновления, попробуйте позже'; - + String get updateCheckFailed => + 'Не удалось проверить обновления, попробуйте позже'; @override String get freePlan => 'Бесплатный'; @override @@ -266,20 +260,6 @@ class StringsRu extends AppText { String get choose => 'Выбрать'; @override String get mostPopular => 'Популярный'; - @override - List get featsFree => const [ - 'Только 1 базовый узел', - '10 минут в день', - 'Смотрите рекламу, чтобы начать', - '7 дней бесплатно при регистрации', - ]; - @override - List get featsPro => - const ['80+ локаций', 'Безлимитно · быстро', '5 устройств', 'Оптимизация для стриминга']; - @override - List get featsTeam => - const ['Всё из Pro', '10 мест', 'Единый счёт', 'Приоритетная поддержка']; - @override String get redeemTitle => 'Активация и покупка'; @override @@ -293,12 +273,12 @@ class StringsRu extends AppText { @override String get buyTitle => 'Где купить'; @override - String get buySub => 'Оплата внутри приложения недоступна. Получить код можно через:'; + String get buySub => + 'Оплата внутри приложения недоступна. Получить код можно через:'; @override String get chStore => 'Магазин самообслуживания'; @override String get chEmail => 'Поддержка по эл. почте'; - @override String get contactTitle => 'Связаться с нами'; @override @@ -314,7 +294,6 @@ class StringsRu extends AppText { String get contactHoursTitle => 'Часы поддержки'; @override String get contactHours => 'Ежедневно 9:00 – 24:00 (GMT+8)'; - @override String get authTagline => 'Быстро · Стабильно · Без усилий'; @override @@ -334,8 +313,6 @@ class StringsRu extends AppText { @override String get codeLabel => 'Код подтверждения'; @override - String codeSentTo(String email) => 'Код отправлен на $email'; - @override String get sendCode => 'Отправить код'; @override String get resend => 'Отправить снова'; @@ -348,8 +325,8 @@ class StringsRu extends AppText { @override String get forgotPw => 'Забыли пароль?'; @override - String get tos => 'Продолжая, вы принимаете наши Условия и Политику конфиденциальности'; - + String get tos => + 'Продолжая, вы принимаете наши Условия и Политику конфиденциальности'; @override String get obSkip => 'Пропустить'; @override @@ -361,17 +338,17 @@ class StringsRu extends AppText { @override String get ob1Title => 'Подключение по всему миру'; @override - String get ob1Sub => '80+ локаций, автоматический выбор самого быстрого маршрута, надёжная стабильность.'; + String get ob1Sub => + '80+ локаций, автоматический выбор самого быстрого маршрута, надёжная стабильность.'; @override String get ob2Title => 'Разрешите настройку сети'; @override - String get ob2Sub => 'Система запросит добавление сетевого профиля для создания зашифрованного туннеля.'; + String get ob2Sub => + 'Система запросит добавление сетевого профиля для создания зашифрованного туннеля.'; @override String get ob3Title => 'Приватность по умолчанию'; @override String get ob3Sub => 'Сквозное шифрование. Мы не храним журналы просмотров.'; - - // ── 购买 / 支付 ── @override String get purchaseTitle => 'Покупка тарифа'; @override @@ -401,15 +378,18 @@ class StringsRu extends AppText { @override String get payNetworkLabel => 'Сеть'; @override - String get payExactAmountHint => 'Отправьте точную сумму — активация произойдёт автоматически'; + String get payExactAmountHint => + 'Отправьте точную сумму — активация произойдёт автоматически'; @override String get copied => 'Скопировано'; @override String get openAlipay => 'Оплатить через Alipay'; @override - String get openAlipayHint => 'Вернитесь после оплаты — страница обновится автоматически'; + String get openAlipayHint => + 'Вернитесь после оплаты — страница обновится автоматически'; @override - String get openAlipayFailed => 'Не удалось открыть Alipay. Проверьте, установлен ли он, или повторите'; + String get openAlipayFailed => + 'Не удалось открыть Alipay. Проверьте, установлен ли он, или повторите'; @override String get awaitingPayment => 'Ожидание оплаты'; @override @@ -429,13 +409,13 @@ class StringsRu extends AppText { @override String get qrNotSupported => 'Скопируйте и откройте в Alipay'; @override - String get orderCreateFailed => 'Не удалось создать заказ, повторите попытку позже'; + String get orderCreateFailed => + 'Не удалось создать заказ, повторите попытку позже'; @override - String get payUnsupportedRenderType => 'Не поддерживается, смените способ оплаты'; + String get payUnsupportedRenderType => + 'Не поддерживается, смените способ оплаты'; @override String get payNoActiveOrder => 'Нет активных заказов'; - - // ── 支付渠道 switch + 订单 ── @override String get payChannel => 'Способ оплаты'; @override @@ -474,12 +454,11 @@ class StringsRu extends AppText { String get orderRebuy => 'Купить снова'; @override String get payMethodNezha => 'Nazhapay'; - - // ── 邀请 / 通知 / 设置分组 / 订单过期(Spec ②③占位) ── @override String get inviteEntry => 'Приглашайте друзей и получайте членство'; @override - String get inviteEntrySub => 'Друг регистрируется · оба получают дни подписки'; + String get inviteEntrySub => + 'Друг регистрируется · оба получают дни подписки'; @override String get inviteTitle => 'Пригласить друзей'; @override @@ -546,4 +525,38 @@ class StringsRu extends AppText { String get promoBadge => 'Спецпредложение'; @override String get promoOriginalPrice => 'Было'; + @override + List get days7 => const ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс']; + @override + List get featsFree => const [ + 'Только 1 базовый узел', + '10 минут в день', + 'Смотрите рекламу, чтобы начать', + '7 дней бесплатно при регистрации', + ]; + @override + List get featsPro => const [ + '80+ локаций', + 'Безлимитно · быстро', + '5 устройств', + 'Оптимизация для стриминга', + ]; + @override + List get featsTeam => const [ + 'Всё из Pro', + '10 мест', + 'Единый счёт', + 'Приоритетная поддержка', + ]; + @override + String adRewarded(int minutes) => '+$minutes мин добавлено'; + @override + String nodeSwitchTitle(String name) => 'Переключиться на $name?'; + @override + String autoConnectingTo(String name) => 'Автоподключение к $name…'; + @override + String updateAvailableTitle(String version) => + 'Доступна новая версия v$version'; + @override + String codeSentTo(String email) => 'Код отправлен на $email'; } diff --git a/client/lib/l10n/strings_zh.dart b/client/lib/l10n/strings_zh.dart index c1a100a..3182f2e 100644 --- a/client/lib/l10n/strings_zh.dart +++ b/client/lib/l10n/strings_zh.dart @@ -1,7 +1,7 @@ -// strings_zh.dart — 中文文案资源(单显) +// strings_zh.dart — 中文文案(自动生成 · 勿手改) // -// 红线词禁用:审查规避/敏感定位词,清单见 design/CLAUDE.md §1 铁律 13。 -// 脱敏口径:网络加速 / 体验优化 + 隐私保护。套餐数字以 design/CLAUDE.md §7 为准。 +// GENERATED by design/codegen/gen_l10n_dart.mjs from design/i18n/strings.json. +// 改文案:改 strings.json 后重跑 codegen;新增 key 需同步 app_text.dart 抽象声明。 import 'app_text.dart'; class StringsZh extends AppText { @@ -26,7 +26,6 @@ class StringsZh extends AppText { String get connectNow => '点击连接'; @override String get secure => '已加密'; - @override String get tabConnect => '连接'; @override @@ -35,7 +34,6 @@ class StringsZh extends AppText { String get tabStats => '统计'; @override String get tabMe => '我的'; - @override String get currentNode => '当前节点'; @override @@ -44,7 +42,6 @@ class StringsZh extends AppText { String get upload => '上传'; @override String get latency => '延迟'; - @override String get quotaToday => '今日剩余'; @override @@ -68,8 +65,6 @@ class StringsZh extends AppText { @override String get adPlaying => '广告播放中…'; @override - String adRewarded(int minutes) => '已加 $minutes 分钟'; - @override String get adFailed => '加时失败,请重试'; @override String get quotaDesktopTitle => '今日免费时长已用完'; @@ -77,7 +72,6 @@ class StringsZh extends AppText { String get quotaDesktopBody => '前往移动端 App 看广告加时,或升级会员畅享无限时长。'; @override String get gotIt => '知道了'; - @override String get chooseNode => '选择节点'; @override @@ -85,8 +79,6 @@ class StringsZh extends AppText { @override String get smartSelect => '智能选择'; @override - String nodeSwitchTitle(String name) => '切换到「$name」?'; - @override String get nodeSwitchBody => '当前连接将断开并重连到该节点。'; @override String get nodeSwitchConfirm => '切换'; @@ -100,7 +92,6 @@ class StringsZh extends AppText { String get smartSub => '根据当前网络环境,自动选择最优节点'; @override String get recommended => '推荐'; - @override String get statsTitle => '使用统计'; @override @@ -112,8 +103,6 @@ class StringsZh extends AppText { @override String get weekTraffic => '本周流量 (GB)'; @override - List get days7 => const ['一', '二', '三', '四', '五', '六', '日']; - @override String get byDevice => '设备明细'; @override String get noDeviceUsage => '暂无设备用量'; @@ -133,7 +122,6 @@ class StringsZh extends AppText { String get chartTwoWeeks => '最近两周流量'; @override String get allDevices => '全部设备'; - @override String get meTitle => '我的'; @override @@ -187,7 +175,8 @@ class StringsZh extends AppText { @override String get devForceLogoutConfirm => '将吊销该设备的登录会话,它会被踢下线、需重新登录。设备仍保留在列表中。'; @override - String get devClearLoginConfirm => '将从设备列表彻底移除此设备,并吊销其登录会话与数据面凭证。该设备需重新登录才能再次使用。'; + String get devClearLoginConfirm => + '将从设备列表彻底移除此设备,并吊销其登录会话与数据面凭证。该设备需重新登录才能再次使用。'; @override String get devCancel => '取消'; @override @@ -223,8 +212,6 @@ class StringsZh extends AppText { @override String get autoConnectSub => '启动时自动连接节点'; @override - String autoConnectingTo(String name) => '正在自动连接到 $name…'; - @override String get smartRoute => '智能分流'; @override String get smartRouteSub => '海外应用走加速,国内直连'; @@ -237,8 +224,6 @@ class StringsZh extends AppText { @override String get webUserCenter => '用户中心(网页)'; @override - String updateAvailableTitle(String version) => '发现新版本 v$version'; - @override String get updateNotesFallback => '本次更新修复了一些问题并提升了稳定性,建议尽快更新。'; @override String get updateLater => '稍后'; @@ -248,7 +233,6 @@ class StringsZh extends AppText { String get updateUpToDate => '已是最新版本'; @override String get updateCheckFailed => '检查更新失败,请稍后重试'; - @override String get freePlan => '免费版'; @override @@ -265,16 +249,6 @@ class StringsZh extends AppText { String get choose => '选择'; @override String get mostPopular => '最受欢迎'; - @override - List get featsFree => - const ['仅 1 个基础节点', '每日 10 分钟时长', '使用前观看广告', '注册享 7 天免费试用']; - @override - List get featsPro => - const ['80+ 全球节点', '无限流量 · 极速', '5 台设备同时在线', '流媒体优化']; - @override - List get featsTeam => - const ['专业版全部功能', '10 个成员席位', '集中计费与管理', '优先客服']; - @override String get redeemTitle => '兑换 & 购买'; @override @@ -293,7 +267,6 @@ class StringsZh extends AppText { String get chStore => '自助发卡商店'; @override String get chEmail => '邮箱客服'; - @override String get contactTitle => '联系我们'; @override @@ -308,7 +281,6 @@ class StringsZh extends AppText { String get contactHoursTitle => '服务时间'; @override String get contactHours => '每日 9:00 – 24:00 (GMT+8)'; - @override String get authTagline => '极速 · 稳定 · 省心'; @override @@ -328,8 +300,6 @@ class StringsZh extends AppText { @override String get codeLabel => '邮箱验证码'; @override - String codeSentTo(String email) => '验证码已发送至 $email'; - @override String get sendCode => '发送验证码'; @override String get resend => '重新发送'; @@ -343,7 +313,6 @@ class StringsZh extends AppText { String get forgotPw => '忘记密码?'; @override String get tos => '继续即代表同意《服务条款》与《隐私政策》'; - @override String get obSkip => '跳过'; @override @@ -364,7 +333,6 @@ class StringsZh extends AppText { String get ob3Title => '安全 · 无日志'; @override String get ob3Sub => '端到端加密,我们不记录你的任何浏览数据。'; - @override String get purchaseTitle => '购买套餐'; @override @@ -427,8 +395,6 @@ class StringsZh extends AppText { String get payUnsupportedRenderType => '暂不支持,请换支付方式'; @override String get payNoActiveOrder => '暂无进行中的订单'; - - // ── 支付渠道 switch + 订单 ── @override String get payChannel => '支付渠道'; @override @@ -467,8 +433,6 @@ class StringsZh extends AppText { String get orderRebuy => '重新购买'; @override String get payMethodNezha => '哪吒支付'; - - // ── 邀请 / 通知 / 设置分组 / 订单过期(Spec ②③占位) ── @override String get inviteEntry => '邀请好友得会员'; @override @@ -539,4 +503,37 @@ class StringsZh extends AppText { String get promoBadge => '限时优惠'; @override String get promoOriginalPrice => '原价'; + @override + List get days7 => const ['一', '二', '三', '四', '五', '六', '日']; + @override + List get featsFree => const [ + '仅 1 个基础节点', + '每日 10 分钟时长', + '使用前观看广告', + '注册享 7 天免费试用', + ]; + @override + List get featsPro => const [ + '80+ 全球节点', + '无限流量 · 极速', + '5 台设备同时在线', + '流媒体优化', + ]; + @override + List get featsTeam => const [ + '专业版全部功能', + '10 个成员席位', + '集中计费与管理', + '优先客服', + ]; + @override + String adRewarded(int minutes) => '已加 $minutes 分钟'; + @override + String nodeSwitchTitle(String name) => '切换到「$name」?'; + @override + String autoConnectingTo(String name) => '正在自动连接到 $name…'; + @override + String updateAvailableTitle(String version) => '发现新版本 v$version'; + @override + String codeSentTo(String email) => '验证码已发送至 $email'; } diff --git a/design/codegen/gen_l10n_dart.mjs b/design/codegen/gen_l10n_dart.mjs new file mode 100644 index 0000000..cf35b83 --- /dev/null +++ b/design/codegen/gen_l10n_dart.mjs @@ -0,0 +1,83 @@ +#!/usr/bin/env node +// gen_l10n_dart.mjs — l10n 单源 codegen +// +// 真源:design/i18n/strings.json(getters + lists + methods × 6 语)。 +// 生成:client/lib/l10n/strings_{zh,en,ja,ko,ru,es}.dart(concrete AppText 实现)。 +// 手写保留:client/lib/l10n/app_text.dart(enum AppLang / abstract AppText 声明 / +// AppLangLabel / AppLangMisc 逻辑)。Dart 编译器强制 concrete⊆abstract 兜底漂移。 +// +// 用法:node design/codegen/gen_l10n_dart.mjs +// 生成后自动 `dart format`,保证 CI「重生成零 diff」闸稳定(漂移闸 = 重生成 + git diff)。 + +import fs from 'node:fs'; +import path from 'node:path'; +import url from 'node:url'; +import { execFileSync } from 'node:child_process'; + +const ROOT = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)), '..', '..'); +const SRC = path.join(ROOT, 'design/i18n/strings.json'); +const OUT_DIR = path.join(ROOT, 'client/lib/l10n'); + +const LANGS = ['zh', 'en', 'ja', 'ko', 'ru', 'es']; +const CLASS = { zh: 'StringsZh', en: 'StringsEn', ja: 'StringsJa', ko: 'StringsKo', ru: 'StringsRu', es: 'StringsEs' }; +const HEADER_NOTE = { zh: '中文', en: 'English', ja: '日本語', ko: '한국어', ru: 'Русский', es: 'Español' }; + +// 字面量转义(getter / list 元素):无插值,$ 一律转义为字面量。 +function litStr(s) { + return s.replace(/\\/g, '\\\\').replace(/\$/g, '\\$').replace(/'/g, "\\'").replace(/\n/g, '\\n'); +} +// 模板转义(method):保留 $var 插值,仅转义反斜杠/引号/换行。 +function tplStr(s) { + return s.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, '\\n'); +} + +const data = JSON.parse(fs.readFileSync(SRC, 'utf8')); +const { getters, lists, methods } = data; + +function genFile(lang) { + const L = []; + L.push(`// strings_${lang}.dart — ${HEADER_NOTE[lang]}文案(自动生成 · 勿手改)`); + L.push('//'); + L.push('// GENERATED by design/codegen/gen_l10n_dart.mjs from design/i18n/strings.json.'); + L.push('// 改文案:改 strings.json 后重跑 codegen;新增 key 需同步 app_text.dart 抽象声明。'); + L.push(`import 'app_text.dart';`); + L.push(''); + L.push(`class ${CLASS[lang]} extends AppText {`); + L.push(` const ${CLASS[lang]}();`); + L.push(''); + L.push(' @override'); + L.push(` AppLang get lang => AppLang.${lang};`); + L.push(''); + for (const [k, m] of Object.entries(getters)) { + L.push(' @override'); + L.push(` String get ${k} => '${litStr(m[lang])}';`); + } + for (const [k, m] of Object.entries(lists)) { + const items = m[lang].map((x) => `'${litStr(x)}'`).join(', '); + L.push(' @override'); + L.push(` List get ${k} => const [${items}];`); + } + for (const [k, m] of Object.entries(methods)) { + L.push(' @override'); + L.push(` String ${k}(${m.args}) => '${tplStr(m.tpl[lang])}';`); + } + L.push('}'); + L.push(''); + return L.join('\n'); +} + +const written = []; +for (const lang of LANGS) { + const out = path.join(OUT_DIR, `strings_${lang}.dart`); + fs.writeFileSync(out, genFile(lang)); + written.push(out); +} + +// dart format 定型,保证零 diff 闸稳定 +try { + execFileSync('dart', ['format', ...written], { cwd: ROOT, stdio: 'inherit' }); +} catch (e) { + console.warn('⚠ dart format 失败(dart 不在 PATH?),已生成未格式化文件:', e.message); +} +const g = Object.keys(getters).length, l = Object.keys(lists).length, me = Object.keys(methods).length; +console.log(`✓ 生成 ${written.length} 份:getters=${g} lists=${l} methods=${me} × ${LANGS.length} 语`); diff --git a/design/codegen/migrate_l10n_to_json.py b/design/codegen/migrate_l10n_to_json.py index 03976f1..8e03121 100644 --- a/design/codegen/migrate_l10n_to_json.py +++ b/design/codegen/migrate_l10n_to_json.py @@ -1,55 +1,75 @@ #!/usr/bin/env python3 -# 从 6 份手写 strings_*.dart 抽取 l10n 到单源 JSON。只读,不改 Dart。 -import re, json, sys, os +# 完整抽取 l10n → 单源 JSON:getters(String get) + lists(List get) + methods(带参)。 +# 只读 Dart,不改。与已提交 strings.json 的 getters/methods 校验无回归,新增 lists 段。 +import re, json, os BASE = "/Users/wangjia/code/pangolin/.claude/worktrees/configurable-proxy/client/lib/l10n" +OUT = "/Users/wangjia/code/pangolin/.claude/worktrees/configurable-proxy/design/i18n/strings.json" LANGS = ["zh", "en", "ja", "ko", "ru", "es"] -# Dart 字符串字面量:单引号或双引号(各自允许 \ 转义);getter 与带参方法 STR = r"""(?:'((?:[^'\\]|\\.)*)'|"((?:[^"\\]|\\.)*)")""" +STR_RE = re.compile(STR) GET_RE = re.compile(r"String\s+get\s+(\w+)\s*=>\s*" + STR + r"\s*;") MET_RE = re.compile(r"String\s+(\w+)\(([^)]*)\)\s*=>\s*" + STR + r"\s*;") +# List get name => const [ ... ]; —— 跨行,惰性到第一个 '];' +LIST_RE = re.compile(r"List\s+get\s+(\w+)\s*=>\s*const\s*\[(.*?)\]\s*;", re.DOTALL) + def unescape(s): - return s.replace("\\'", "'").replace('\\"', '"').replace('\\\\', '\\').replace('\\n', '\n').replace('\\$', '$') + return (s.replace("\\'", "'").replace('\\"', '"') + .replace('\\n', '\n').replace('\\$', '$').replace('\\\\', '\\')) -getters = {} # key -> {lang: val} -methods = {} # key -> {args, tpl:{lang:val}} -counts = {} +getters, lists, methods, counts = {}, {}, {}, {} for lang in LANGS: - path = os.path.join(BASE, f"strings_{lang}.dart") - src = open(path, encoding="utf-8").read() - g = 0 - for m in GET_RE.finditer(src): - k = m.group(1); v = unescape(m.group(2) if m.group(2) is not None else m.group(3)) - getters.setdefault(k, {})[lang] = v - g += 1 - mm = 0 - for m in MET_RE.finditer(src): - k = m.group(1); args = m.group(2).strip() - v = unescape(m.group(3) if m.group(3) is not None else m.group(4)) + src = open(os.path.join(BASE, f"strings_{lang}.dart"), encoding="utf-8").read() + g = m = ln = 0 + for mt in GET_RE.finditer(src): + k = mt.group(1) + v = unescape(mt.group(2) if mt.group(2) is not None else mt.group(3)) + getters.setdefault(k, {})[lang] = v; g += 1 + for mt in MET_RE.finditer(src): + k = mt.group(1); args = mt.group(2).strip() + v = unescape(mt.group(3) if mt.group(3) is not None else mt.group(4)) methods.setdefault(k, {"args": args, "tpl": {}}) - methods[k]["tpl"][lang] = v - mm += 1 - counts[lang] = {"getters": g, "methods": mm} + methods[k]["tpl"][lang] = v; m += 1 + for mt in LIST_RE.finditer(src): + k = mt.group(1); body = mt.group(2) + items = [unescape(a if a is not None else b) for a, b in STR_RE.findall(body)] + lists.setdefault(k, {})[lang] = items; ln += 1 + counts[lang] = {"getters": g, "methods": m, "lists": ln} -# 完整性校验:每个 key 六语齐全 +# 完整性:每 key 六语齐全 missing = [] for k, d in getters.items(): for l in LANGS: if l not in d: missing.append(f"getter {k}:{l}") +for k, d in lists.items(): + for l in LANGS: + if l not in d: missing.append(f"list {k}:{l}") for k, d in methods.items(): for l in LANGS: if l not in d["tpl"]: missing.append(f"method {k}:{l}") -out = {"_note": "l10n 单源(唯一真相源)。codegen 生成 client/lib/l10n/strings_*.dart + app_text.dart + 原型读同源。勿手改生成物。", - "getters": getters, "methods": methods} -os.makedirs("/Users/wangjia/code/pangolin/.claude/worktrees/configurable-proxy/design/i18n", exist_ok=True) -outpath = "/Users/wangjia/code/pangolin/.claude/worktrees/configurable-proxy/design/i18n/strings.json" -json.dump(out, open(outpath, "w", encoding="utf-8"), ensure_ascii=False, indent=2) +# 与已提交 JSON 的 getters/methods 校验无回归 +old = json.load(open(OUT, encoding="utf-8")) if os.path.exists(OUT) else {} +regress = [] +if old: + if set(old.get("getters", {})) != set(getters): + regress.append("getter 键集变化") + for k, d in old.get("getters", {}).items(): + if getters.get(k) != d: regress.append(f"getter 值变化:{k}") + if set(old.get("methods", {})) != set(methods): + regress.append("method 键集变化") -print("每语言抽取数:", counts) -print(f"合并后:getters={len(getters)} methods={len(methods)}") -print(f"缺失(应为0):{len(missing)}", missing[:5]) -print("输出:", outpath) +out = {"_note": "l10n 单源(唯一真相源)。design/codegen/gen_l10n_dart.mjs 生成 client/lib/l10n/strings_*.dart;" + "原型 serve.mjs 读同源。勿手改生成物;abstract 声明由 app_text.dart 手写(编译器强制 concrete⊆abstract)。", + "getters": getters, "lists": lists, "methods": methods} +json.dump(out, open(OUT, "w", encoding="utf-8"), ensure_ascii=False, indent=2) + +print("每语言:", counts) +print(f"合并:getters={len(getters)} lists={len(lists)} methods={len(methods)}") +print(f"缺失(应0):{len(missing)}", missing[:5]) +print(f"回归(应0):{len(regress)}", regress[:5]) +print("lists 键:", list(lists)) +print("输出:", OUT) diff --git a/design/i18n/strings.json b/design/i18n/strings.json index b6f16ba..89aa60d 100644 --- a/design/i18n/strings.json +++ b/design/i18n/strings.json @@ -1,5 +1,5 @@ { - "_note": "l10n 单源(唯一真相源)。codegen 生成 client/lib/l10n/strings_*.dart + app_text.dart + 原型读同源。勿手改生成物。", + "_note": "l10n 单源(唯一真相源)。design/codegen/gen_l10n_dart.mjs 生成 client/lib/l10n/strings_*.dart;原型 serve.mjs 读同源。勿手改生成物;abstract 声明由 app_text.dart 手写(编译器强制 concrete⊆abstract)。", "getters": { "brand": { "zh": "穿山甲", @@ -1970,6 +1970,178 @@ "es": "Antes" } }, + "lists": { + "days7": { + "zh": [ + "一", + "二", + "三", + "四", + "五", + "六", + "日" + ], + "en": [ + "M", + "T", + "W", + "T", + "F", + "S", + "S" + ], + "ja": [ + "月", + "火", + "水", + "木", + "金", + "土", + "日" + ], + "ko": [ + "월", + "화", + "수", + "목", + "금", + "토", + "일" + ], + "ru": [ + "Пн", + "Вт", + "Ср", + "Чт", + "Пт", + "Сб", + "Вс" + ], + "es": [ + "L", + "M", + "X", + "J", + "V", + "S", + "D" + ] + }, + "featsFree": { + "zh": [ + "仅 1 个基础节点", + "每日 10 分钟时长", + "使用前观看广告", + "注册享 7 天免费试用" + ], + "en": [ + "1 basic node only", + "10 min per day", + "Watch an ad to start", + "7-day free trial on sign-up" + ], + "ja": [ + "基本ノード1つのみ", + "1日10分", + "広告を見て開始", + "登録で7日間無料トライアル" + ], + "ko": [ + "기본 노드 1개", + "하루 10분", + "광고 시청 후 시작", + "가입 시 7일 무료 체험" + ], + "ru": [ + "Только 1 базовый узел", + "10 минут в день", + "Смотрите рекламу, чтобы начать", + "7 дней бесплатно при регистрации" + ], + "es": [ + "Solo 1 nodo básico", + "10 min al día", + "Mira un anuncio para empezar", + "Prueba gratis de 7 días al registrarte" + ] + }, + "featsPro": { + "zh": [ + "80+ 全球节点", + "无限流量 · 极速", + "5 台设备同时在线", + "流媒体优化" + ], + "en": [ + "80+ locations", + "Unlimited · fast", + "5 devices", + "Streaming optimized" + ], + "ja": [ + "80以上の拠点", + "無制限 · 高速", + "5台のデバイス", + "ストリーミング最適化" + ], + "ko": [ + "80개 이상 지역", + "무제한 · 빠른 속도", + "기기 5대", + "스트리밍 최적화" + ], + "ru": [ + "80+ локаций", + "Безлимитно · быстро", + "5 устройств", + "Оптимизация для стриминга" + ], + "es": [ + "Más de 80 ubicaciones", + "Ilimitado · rápido", + "5 dispositivos", + "Optimizado para streaming" + ] + }, + "featsTeam": { + "zh": [ + "专业版全部功能", + "10 个成员席位", + "集中计费与管理", + "优先客服" + ], + "en": [ + "Everything in Pro", + "10 seats", + "Central billing", + "Priority support" + ], + "ja": [ + "Proのすべての機能", + "10ライセンス", + "一括請求", + "優先サポート" + ], + "ko": [ + "Pro의 모든 기능", + "10인 좌석", + "통합 결제", + "우선 지원" + ], + "ru": [ + "Всё из Pro", + "10 мест", + "Единый счёт", + "Приоритетная поддержка" + ], + "es": [ + "Todo lo de Pro", + "10 plazas", + "Facturación centralizada", + "Soporte prioritario" + ] + } + }, "methods": { "adRewarded": { "args": "int minutes",