feat(i18n): ④ l10n 漂移闸 + codegen 改纯 node 确定性输出

- gen_l10n_dart.mjs 去掉 dart format:其输出依赖包解析状态、跨环境不稳(首次 4 空格、
  重跑 8 空格),会让漂移闸误报。改为确定性终态格式(list 单行 const [])。
  analyze 不校验格式,不影响;gen 现零 dart 依赖、纯 node 确定。
- ci/check-codegen-drift.sh 增 l10n 段:重生成 strings_*.dart → git diff 非空即 fail
  (与 token 段同法,纯 node)。
- 验证:往返无损、重生成零 diff(闸稳定)、flutter analyze 0 issue、244 测试全绿。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A79VtQA1BwTuQN1ThpvYpo
This commit is contained in:
wangjia
2026-07-27 08:55:21 +08:00
parent f76e3cdffd
commit 13e0ba6d47
8 changed files with 110 additions and 263 deletions
+15
View File
@@ -31,3 +31,18 @@ if ! git diff --quiet -- "$GEN"; then
fi fi
echo "✅ codegen 无漂移:token 生成物与 CSS 真相源一致。" echo "✅ codegen 无漂移:token 生成物与 CSS 真相源一致。"
# ── l10n 单源:design/i18n/strings.json → client/lib/l10n/strings_*.dart ──
# gen 为纯 node 确定性输出(不跑 dart format),故本段与 token 段一样零 dart 依赖。
L10N_GEN="client/lib/l10n"
echo "→ 重新生成 l10n (design/codegen/gen_l10n_dart.mjs) ..."
node design/codegen/gen_l10n_dart.mjs
if ! git diff --quiet -- "$L10N_GEN"/strings_*.dart; then
echo "❌ l10n 漂移:strings_*.dart 与 design/i18n/strings.json 不一致。" >&2
echo " 原因:改了 strings.json 却没重生成,或手改了生成物(勿手改 strings_*.dart)。" >&2
echo " 修复:node design/codegen/gen_l10n_dart.mjs 后提交生成物。" >&2
echo " ── diff(前 40 行)──" >&2
git --no-pager diff -- "$L10N_GEN"/strings_*.dart | head -40 >&2
exit 1
fi
echo "✅ l10n 无漂移:strings_*.dart 与 strings.json 单源一致。"
+19 -50
View File
@@ -61,8 +61,7 @@ class StringsEn extends AppText {
@override @override
String get watchAdMore => 'Watch ad for more time'; String get watchAdMore => 'Watch ad for more time';
@override @override
String get quotaExhaustedNotice => String get quotaExhaustedNotice => 'You\'ve used today\'s free time. Upgrade to keep connecting.';
'You\'ve used today\'s free time. Upgrade to keep connecting.';
@override @override
String get adPlaying => 'Ad playing…'; String get adPlaying => 'Ad playing…';
@override @override
@@ -70,8 +69,7 @@ class StringsEn extends AppText {
@override @override
String get quotaDesktopTitle => 'Daily free time used up'; String get quotaDesktopTitle => 'Daily free time used up';
@override @override
String get quotaDesktopBody => String get quotaDesktopBody => 'Watch ads in the mobile app to add time, or upgrade for unlimited access.';
'Watch ads in the mobile app to add time, or upgrade for unlimited access.';
@override @override
String get gotIt => 'Got it'; String get gotIt => 'Got it';
@override @override
@@ -81,16 +79,13 @@ class StringsEn extends AppText {
@override @override
String get smartSelect => 'Smart select'; String get smartSelect => 'Smart select';
@override @override
String get nodeSwitchBody => String get nodeSwitchBody => 'This disconnects the current link and reconnects to this node.';
'This disconnects the current link and reconnects to this node.';
@override @override
String get nodeSwitchConfirm => 'Switch'; String get nodeSwitchConfirm => 'Switch';
@override @override
String get nodeUnhealthySwitched => String get nodeUnhealthySwitched => 'Current node unreachable — switched automatically';
'Current node unreachable — switched automatically';
@override @override
String get nodeUnhealthyError => String get nodeUnhealthyError => 'Current node is unreachable — reconnect or switch node';
'Current node is unreachable — reconnect or switch node';
@override @override
String get nodeReconnecting => 'Network unstable, reconnecting…'; String get nodeReconnecting => 'Network unstable, reconnecting…';
@override @override
@@ -148,8 +143,7 @@ class StringsEn extends AppText {
@override @override
String get deviceLimitTitle => 'Device limit reached'; String get deviceLimitTitle => 'Device limit reached';
@override @override
String get deviceLimitDesc => String get deviceLimitDesc => 'Your plan allows up to %s devices. Remove one to continue.';
'Your plan allows up to %s devices. Remove one to continue.';
@override @override
String get deviceLimitRemoveOldest => 'Remove least recently used'; String get deviceLimitRemoveOldest => 'Remove least recently used';
@override @override
@@ -179,18 +173,15 @@ class StringsEn extends AppText {
@override @override
String get devSave => 'Save'; String get devSave => 'Save';
@override @override
String get devForceLogoutConfirm => 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.';
'This revokes the device\'s login session — it will be kicked offline and must sign in again. The device stays in the list.';
@override @override
String get devClearLoginConfirm => 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.';
'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 @override
String get devCancel => 'Cancel'; String get devCancel => 'Cancel';
@override @override
String get sessionRevokedTitle => 'This device was signed out'; String get sessionRevokedTitle => 'This device was signed out';
@override @override
String get sessionRevokedBody => String get sessionRevokedBody => 'Your account signed this device out from another device. Please sign in again.';
'Your account signed this device out from another device. Please sign in again.';
@override @override
String get sessionRevokedOk => 'Sign in again'; String get sessionRevokedOk => 'Sign in again';
@override @override
@@ -232,8 +223,7 @@ class StringsEn extends AppText {
@override @override
String get webUserCenter => 'User Center (Web)'; String get webUserCenter => 'User Center (Web)';
@override @override
String get updateNotesFallback => String get updateNotesFallback => 'This update includes fixes and stability improvements.';
'This update includes fixes and stability improvements.';
@override @override
String get updateLater => 'Later'; String get updateLater => 'Later';
@override @override
@@ -279,8 +269,7 @@ class StringsEn extends AppText {
@override @override
String get contactTitle => 'Contact us'; String get contactTitle => 'Contact us';
@override @override
String get contactIntro => String get contactIntro => 'Need help? Reach us via any channel below — usually replies in minutes.';
'Need help? Reach us via any channel below — usually replies in minutes.';
@override @override
String get contactEmail => 'Email support'; String get contactEmail => 'Email support';
@override @override
@@ -338,8 +327,7 @@ class StringsEn extends AppText {
@override @override
String get ob2Title => 'Allow network setup'; String get ob2Title => 'Allow network setup';
@override @override
String get ob2Sub => String get ob2Sub => 'The system will ask to add a network profile to build the encrypted tunnel.';
'The system will ask to add a network profile to build the encrypted tunnel.';
@override @override
String get ob3Title => 'Private by design'; String get ob3Title => 'Private by design';
@override @override
@@ -373,18 +361,15 @@ class StringsEn extends AppText {
@override @override
String get payNetworkLabel => 'Network'; String get payNetworkLabel => 'Network';
@override @override
String get payExactAmountHint => String get payExactAmountHint => 'Send the exact amount; activates automatically';
'Send the exact amount; activates automatically';
@override @override
String get copied => 'Copied'; String get copied => 'Copied';
@override @override
String get openAlipay => 'Pay with Alipay'; String get openAlipay => 'Pay with Alipay';
@override @override
String get openAlipayHint => String get openAlipayHint => 'Return after paying; this page refreshes automatically';
'Return after paying; this page refreshes automatically';
@override @override
String get openAlipayFailed => String get openAlipayFailed => 'Couldn\'t open Alipay; check it\'s installed or try again';
'Couldn\'t open Alipay; check it\'s installed or try again';
@override @override
String get awaitingPayment => 'Awaiting payment'; String get awaitingPayment => 'Awaiting payment';
@override @override
@@ -520,26 +505,11 @@ class StringsEn extends AppText {
@override @override
List<String> get days7 => const ['M', 'T', 'W', 'T', 'F', 'S', 'S']; List<String> get days7 => const ['M', 'T', 'W', 'T', 'F', 'S', 'S'];
@override @override
List<String> get featsFree => const [ List<String> get featsFree => const ['1 basic node only', '10 min per day', 'Watch an ad to start', '7-day free trial on sign-up'];
'1 basic node only',
'10 min per day',
'Watch an ad to start',
'7-day free trial on sign-up',
];
@override @override
List<String> get featsPro => const [ List<String> get featsPro => const ['80+ locations', 'Unlimited · fast', '5 devices', 'Streaming optimized'];
'80+ locations',
'Unlimited · fast',
'5 devices',
'Streaming optimized',
];
@override @override
List<String> get featsTeam => const [ List<String> get featsTeam => const ['Everything in Pro', '10 seats', 'Central billing', 'Priority support'];
'Everything in Pro',
'10 seats',
'Central billing',
'Priority support',
];
@override @override
String adRewarded(int minutes) => '+$minutes min added'; String adRewarded(int minutes) => '+$minutes min added';
@override @override
@@ -547,8 +517,7 @@ class StringsEn extends AppText {
@override @override
String autoConnectingTo(String name) => 'Auto-connecting to $name'; String autoConnectingTo(String name) => 'Auto-connecting to $name';
@override @override
String updateAvailableTitle(String version) => String updateAvailableTitle(String version) => 'New version v$version available';
'New version v$version available';
@override @override
String codeSentTo(String email) => 'Code sent to $email'; String codeSentTo(String email) => 'Code sent to $email';
} }
+27 -66
View File
@@ -61,8 +61,7 @@ class StringsEs extends AppText {
@override @override
String get watchAdMore => 'Mira un anuncio para más tiempo'; String get watchAdMore => 'Mira un anuncio para más tiempo';
@override @override
String get quotaExhaustedNotice => String get quotaExhaustedNotice => 'Tiempo gratis diario agotado. Mejora tu plan para seguir conectado.';
'Tiempo gratis diario agotado. Mejora tu plan para seguir conectado.';
@override @override
String get adPlaying => 'Reproduciendo anuncio…'; String get adPlaying => 'Reproduciendo anuncio…';
@override @override
@@ -70,8 +69,7 @@ class StringsEs extends AppText {
@override @override
String get quotaDesktopTitle => 'Tiempo gratis diario agotado'; String get quotaDesktopTitle => 'Tiempo gratis diario agotado';
@override @override
String get quotaDesktopBody => String get quotaDesktopBody => 'Mira anuncios en la app móvil para añadir tiempo, o mejora tu plan para acceso ilimitado.';
'Mira anuncios en la app móvil para añadir tiempo, o mejora tu plan para acceso ilimitado.';
@override @override
String get gotIt => 'Entendido'; String get gotIt => 'Entendido';
@override @override
@@ -81,16 +79,13 @@ class StringsEs extends AppText {
@override @override
String get smartSelect => 'Selección inteligente'; String get smartSelect => 'Selección inteligente';
@override @override
String get nodeSwitchBody => String get nodeSwitchBody => 'Esto desconecta el enlace actual y reconecta a este nodo.';
'Esto desconecta el enlace actual y reconecta a este nodo.';
@override @override
String get nodeSwitchConfirm => 'Cambiar'; String get nodeSwitchConfirm => 'Cambiar';
@override @override
String get nodeUnhealthySwitched => String get nodeUnhealthySwitched => 'Nodo actual inaccesible: cambiado automáticamente';
'Nodo actual inaccesible: cambiado automáticamente';
@override @override
String get nodeUnhealthyError => String get nodeUnhealthyError => 'El nodo actual está inaccesible: reconecta o cambia de nodo';
'El nodo actual está inaccesible: reconecta o cambia de nodo';
@override @override
String get nodeReconnecting => 'Red inestable, reconectando…'; String get nodeReconnecting => 'Red inestable, reconectando…';
@override @override
@@ -148,8 +143,7 @@ class StringsEs extends AppText {
@override @override
String get deviceLimitTitle => 'Límite de dispositivos alcanzado'; String get deviceLimitTitle => 'Límite de dispositivos alcanzado';
@override @override
String get deviceLimitDesc => String get deviceLimitDesc => 'Tu plan permite hasta %s dispositivos. Elimina uno para continuar.';
'Tu plan permite hasta %s dispositivos. Elimina uno para continuar.';
@override @override
String get deviceLimitRemoveOldest => 'Eliminar el menos usado recientemente'; String get deviceLimitRemoveOldest => 'Eliminar el menos usado recientemente';
@override @override
@@ -179,18 +173,15 @@ class StringsEs extends AppText {
@override @override
String get devSave => 'Guardar'; String get devSave => 'Guardar';
@override @override
String get devForceLogoutConfirm => 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.';
'Esto revoca la sesión del dispositivo: será desconectado y deberá iniciar sesión de nuevo. El dispositivo permanece en la lista.';
@override @override
String get devClearLoginConfirm => 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.';
'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 @override
String get devCancel => 'Cancelar'; String get devCancel => 'Cancelar';
@override @override
String get sessionRevokedTitle => 'Se cerró la sesión de este dispositivo'; String get sessionRevokedTitle => 'Se cerró la sesión de este dispositivo';
@override @override
String get sessionRevokedBody => String get sessionRevokedBody => 'Tu cuenta cerró la sesión de este dispositivo desde otro dispositivo. Inicia sesión de nuevo.';
'Tu cuenta cerró la sesión de este dispositivo desde otro dispositivo. Inicia sesión de nuevo.';
@override @override
String get sessionRevokedOk => 'Iniciar sesión de nuevo'; String get sessionRevokedOk => 'Iniciar sesión de nuevo';
@override @override
@@ -232,8 +223,7 @@ class StringsEs extends AppText {
@override @override
String get webUserCenter => 'Centro de usuario (Web)'; String get webUserCenter => 'Centro de usuario (Web)';
@override @override
String get updateNotesFallback => String get updateNotesFallback => 'Esta actualización incluye correcciones y mejoras de estabilidad.';
'Esta actualización incluye correcciones y mejoras de estabilidad.';
@override @override
String get updateLater => 'Más tarde'; String get updateLater => 'Más tarde';
@override @override
@@ -241,8 +231,7 @@ class StringsEs extends AppText {
@override @override
String get updateUpToDate => 'Ya tienes la última versión'; String get updateUpToDate => 'Ya tienes la última versión';
@override @override
String get updateCheckFailed => String get updateCheckFailed => 'Falló la búsqueda de actualizaciones, inténtalo más tarde';
'Falló la búsqueda de actualizaciones, inténtalo más tarde';
@override @override
String get freePlan => 'Gratis'; String get freePlan => 'Gratis';
@override @override
@@ -272,8 +261,7 @@ class StringsEs extends AppText {
@override @override
String get buyTitle => 'Dónde comprar'; String get buyTitle => 'Dónde comprar';
@override @override
String get buySub => String get buySub => 'El pago dentro de la app no está disponible. Consigue un código mediante:';
'El pago dentro de la app no está disponible. Consigue un código mediante:';
@override @override
String get chStore => 'Tienda autoservicio'; String get chStore => 'Tienda autoservicio';
@override @override
@@ -281,8 +269,7 @@ class StringsEs extends AppText {
@override @override
String get contactTitle => 'Contáctanos'; String get contactTitle => 'Contáctanos';
@override @override
String get contactIntro => String get contactIntro => '¿Necesitas ayuda? Escríbenos por cualquiera de los canales de abajo: solemos responder en minutos.';
'¿Necesitas ayuda? Escríbenos por cualquiera de los canales de abajo: solemos responder en minutos.';
@override @override
String get contactEmail => 'Soporte por correo'; String get contactEmail => 'Soporte por correo';
@override @override
@@ -324,8 +311,7 @@ class StringsEs extends AppText {
@override @override
String get forgotPw => '¿Olvidaste la contraseña?'; String get forgotPw => '¿Olvidaste la contraseña?';
@override @override
String get tos => String get tos => 'Al continuar aceptas nuestros Términos y Política de privacidad';
'Al continuar aceptas nuestros Términos y Política de privacidad';
@override @override
String get obSkip => 'Omitir'; String get obSkip => 'Omitir';
@override @override
@@ -337,18 +323,15 @@ class StringsEs extends AppText {
@override @override
String get ob1Title => 'Conéctate en todo el mundo'; String get ob1Title => 'Conéctate en todo el mundo';
@override @override
String get ob1Sub => String get ob1Sub => 'Más de 80 ubicaciones, enrutamiento al más rápido automático, sólido como una roca.';
'Más de 80 ubicaciones, enrutamiento al más rápido automático, sólido como una roca.';
@override @override
String get ob2Title => 'Permite la configuración de red'; String get ob2Title => 'Permite la configuración de red';
@override @override
String get ob2Sub => String get ob2Sub => 'El sistema te pedirá añadir un perfil de red para crear el túnel cifrado.';
'El sistema te pedirá añadir un perfil de red para crear el túnel cifrado.';
@override @override
String get ob3Title => 'Privado por diseño'; String get ob3Title => 'Privado por diseño';
@override @override
String get ob3Sub => String get ob3Sub => 'Cifrado de extremo a extremo. No guardamos ningún registro de navegación.';
'Cifrado de extremo a extremo. No guardamos ningún registro de navegación.';
@override @override
String get purchaseTitle => 'Comprar plan'; String get purchaseTitle => 'Comprar plan';
@override @override
@@ -378,18 +361,15 @@ class StringsEs extends AppText {
@override @override
String get payNetworkLabel => 'Red'; String get payNetworkLabel => 'Red';
@override @override
String get payExactAmountHint => String get payExactAmountHint => 'Envía el importe exacto; se activa automáticamente';
'Envía el importe exacto; se activa automáticamente';
@override @override
String get copied => 'Copiado'; String get copied => 'Copiado';
@override @override
String get openAlipay => 'Pagar con Alipay'; String get openAlipay => 'Pagar con Alipay';
@override @override
String get openAlipayHint => String get openAlipayHint => 'Vuelve tras pagar; esta página se actualiza automáticamente';
'Vuelve tras pagar; esta página se actualiza automáticamente';
@override @override
String get openAlipayFailed => String get openAlipayFailed => 'No se pudo abrir Alipay; comprueba que esté instalado o inténtalo de nuevo';
'No se pudo abrir Alipay; comprueba que esté instalado o inténtalo de nuevo';
@override @override
String get awaitingPayment => 'Esperando el pago'; String get awaitingPayment => 'Esperando el pago';
@override @override
@@ -409,11 +389,9 @@ class StringsEs extends AppText {
@override @override
String get qrNotSupported => 'Copia el contenido y ábrelo en Alipay'; String get qrNotSupported => 'Copia el contenido y ábrelo en Alipay';
@override @override
String get orderCreateFailed => String get orderCreateFailed => 'Error al crear el pedido, inténtalo de nuevo más tarde';
'Error al crear el pedido, inténtalo de nuevo más tarde';
@override @override
String get payUnsupportedRenderType => String get payUnsupportedRenderType => 'No compatible, cambia el método de pago';
'No compatible, cambia el método de pago';
@override @override
String get payNoActiveOrder => 'No hay pedidos activos'; String get payNoActiveOrder => 'No hay pedidos activos';
@override @override
@@ -473,8 +451,7 @@ class StringsEs extends AppText {
@override @override
String get inviteRewardRegister => 'Registro de amigo · +3 días cada uno'; String get inviteRewardRegister => 'Registro de amigo · +3 días cada uno';
@override @override
String get inviteRewardFirstBuy => String get inviteRewardFirstBuy => 'Primera compra del amigo · +7 días cada uno';
'Primera compra del amigo · +7 días cada uno';
@override @override
String get inviteRewardJoinTg => 'Unirse al canal de TG · +3 días'; String get inviteRewardJoinTg => 'Unirse al canal de TG · +3 días';
@override @override
@@ -528,26 +505,11 @@ class StringsEs extends AppText {
@override @override
List<String> get days7 => const ['L', 'M', 'X', 'J', 'V', 'S', 'D']; List<String> get days7 => const ['L', 'M', 'X', 'J', 'V', 'S', 'D'];
@override @override
List<String> get featsFree => const [ List<String> 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'];
'Solo 1 nodo básico',
'10 min al día',
'Mira un anuncio para empezar',
'Prueba gratis de 7 días al registrarte',
];
@override @override
List<String> get featsPro => const [ List<String> get featsPro => const ['Más de 80 ubicaciones', 'Ilimitado · rápido', '5 dispositivos', 'Optimizado para streaming'];
'Más de 80 ubicaciones',
'Ilimitado · rápido',
'5 dispositivos',
'Optimizado para streaming',
];
@override @override
List<String> get featsTeam => const [ List<String> get featsTeam => const ['Todo lo de Pro', '10 plazas', 'Facturación centralizada', 'Soporte prioritario'];
'Todo lo de Pro',
'10 plazas',
'Facturación centralizada',
'Soporte prioritario',
];
@override @override
String adRewarded(int minutes) => '+$minutes min añadidos'; String adRewarded(int minutes) => '+$minutes min añadidos';
@override @override
@@ -555,8 +517,7 @@ class StringsEs extends AppText {
@override @override
String autoConnectingTo(String name) => 'Conectando automáticamente a $name'; String autoConnectingTo(String name) => 'Conectando automáticamente a $name';
@override @override
String updateAvailableTitle(String version) => String updateAvailableTitle(String version) => 'Nueva versión v$version disponible';
'Nueva versión v$version disponible';
@override @override
String codeSentTo(String email) => 'Código enviado a $email'; String codeSentTo(String email) => 'Código enviado a $email';
} }
+7 -26
View File
@@ -69,8 +69,7 @@ class StringsJa extends AppText {
@override @override
String get quotaDesktopTitle => '本日の無料時間を使い切りました'; String get quotaDesktopTitle => '本日の無料時間を使い切りました';
@override @override
String get quotaDesktopBody => String get quotaDesktopBody => 'モバイルアプリで広告を見ると時間を追加できます。無制限に使うにはアップグレードしてください。';
'モバイルアプリで広告を見ると時間を追加できます。無制限に使うにはアップグレードしてください。';
@override @override
String get gotIt => '了解'; String get gotIt => '了解';
@override @override
@@ -174,18 +173,15 @@ class StringsJa extends AppText {
@override @override
String get devSave => '保存'; String get devSave => '保存';
@override @override
String get devForceLogoutConfirm => String get devForceLogoutConfirm => 'このデバイスのログインセッションを無効にします。オフラインになり、再度サインインが必要になります。デバイスはリストに残ります。';
'このデバイスのログインセッションを無効にします。オフラインになり、再度サインインが必要になります。デバイスはリストに残ります。';
@override @override
String get devClearLoginConfirm => String get devClearLoginConfirm => 'このデバイスをリストから完全に削除し、ログインセッションとデータ通信の認証情報を無効にします。再び利用するには、もう一度サインインが必要です。';
'このデバイスをリストから完全に削除し、ログインセッションとデータ通信の認証情報を無効にします。再び利用するには、もう一度サインインが必要です。';
@override @override
String get devCancel => 'キャンセル'; String get devCancel => 'キャンセル';
@override @override
String get sessionRevokedTitle => 'このデバイスはサインアウトされました'; String get sessionRevokedTitle => 'このデバイスはサインアウトされました';
@override @override
String get sessionRevokedBody => String get sessionRevokedBody => '別のデバイスから、このデバイスがサインアウトされました。もう一度サインインしてください。';
'別のデバイスから、このデバイスがサインアウトされました。もう一度サインインしてください。';
@override @override
String get sessionRevokedOk => '再度サインイン'; String get sessionRevokedOk => '再度サインイン';
@override @override
@@ -509,26 +505,11 @@ class StringsJa extends AppText {
@override @override
List<String> get days7 => const ['', '', '', '', '', '', '']; List<String> get days7 => const ['', '', '', '', '', '', ''];
@override @override
List<String> get featsFree => const [ List<String> get featsFree => const ['基本ノード1つのみ', '1日10分', '広告を見て開始', '登録で7日間無料トライアル'];
'基本ノード1つのみ',
'1日10分',
'広告を見て開始',
'登録で7日間無料トライアル',
];
@override @override
List<String> get featsPro => const [ List<String> get featsPro => const ['80以上の拠点', '無制限 · 高速', '5台のデバイス', 'ストリーミング最適化'];
'80以上の拠点',
'無制限 · 高速',
'5台のデバイス',
'ストリーミング最適化',
];
@override @override
List<String> get featsTeam => const [ List<String> get featsTeam => const ['Proのすべての機能', '10ライセンス', '一括請求', '優先サポート'];
'Proのすべての機能',
'10ライセンス',
'一括請求',
'優先サポート',
];
@override @override
String adRewarded(int minutes) => '+$minutes 分を追加しました'; String adRewarded(int minutes) => '+$minutes 分を追加しました';
@override @override
+7 -26
View File
@@ -61,8 +61,7 @@ class StringsKo extends AppText {
@override @override
String get watchAdMore => '광고 시청으로 시간 추가'; String get watchAdMore => '광고 시청으로 시간 추가';
@override @override
String get quotaExhaustedNotice => String get quotaExhaustedNotice => '오늘 무료 시간을 모두 사용했습니다. 업그레이드하면 계속 연결할 수 있습니다.';
'오늘 무료 시간을 모두 사용했습니다. 업그레이드하면 계속 연결할 수 있습니다.';
@override @override
String get adPlaying => '광고 재생 중…'; String get adPlaying => '광고 재생 중…';
@override @override
@@ -70,8 +69,7 @@ class StringsKo extends AppText {
@override @override
String get quotaDesktopTitle => '오늘 무료 시간을 모두 사용함'; String get quotaDesktopTitle => '오늘 무료 시간을 모두 사용함';
@override @override
String get quotaDesktopBody => String get quotaDesktopBody => '모바일 앱에서 광고를 시청해 시간을 추가하거나, 업그레이드하여 무제한으로 이용하세요.';
'모바일 앱에서 광고를 시청해 시간을 추가하거나, 업그레이드하여 무제한으로 이용하세요.';
@override @override
String get gotIt => '확인'; String get gotIt => '확인';
@override @override
@@ -175,11 +173,9 @@ class StringsKo extends AppText {
@override @override
String get devSave => '저장'; String get devSave => '저장';
@override @override
String get devForceLogoutConfirm => String get devForceLogoutConfirm => '이 기기의 로그인 세션을 무효화합니다 — 오프라인으로 강제 종료되며 다시 로그인해야 합니다. 기기는 목록에 남아 있습니다.';
'이 기기의 로그인 세션을 무효화합니다 — 오프라인으로 강제 종료되며 다시 로그인해야 합니다. 기기는 목록에 남아 있습니다.';
@override @override
String get devClearLoginConfirm => String get devClearLoginConfirm => '이 기기를 목록에서 완전히 삭제하고 로그인 세션과 데이터 전송 자격 증명을 무효화합니다. 다시 사용하려면 재로그인해야 합니다.';
'이 기기를 목록에서 완전히 삭제하고 로그인 세션과 데이터 전송 자격 증명을 무효화합니다. 다시 사용하려면 재로그인해야 합니다.';
@override @override
String get devCancel => '취소'; String get devCancel => '취소';
@override @override
@@ -509,26 +505,11 @@ class StringsKo extends AppText {
@override @override
List<String> get days7 => const ['', '', '', '', '', '', '']; List<String> get days7 => const ['', '', '', '', '', '', ''];
@override @override
List<String> get featsFree => const [ List<String> get featsFree => const ['기본 노드 1개', '하루 10분', '광고 시청 후 시작', '가입 시 7일 무료 체험'];
'기본 노드 1개',
'하루 10분',
'광고 시청 후 시작',
'가입 시 7일 무료 체험',
];
@override @override
List<String> get featsPro => const [ List<String> get featsPro => const ['80개 이상 지역', '무제한 · 빠른 속도', '기기 5대', '스트리밍 최적화'];
'80개 이상 지역',
'무제한 · 빠른 속도',
'기기 5대',
'스트리밍 최적화',
];
@override @override
List<String> get featsTeam => const [ List<String> get featsTeam => const ['Pro의 모든 기능', '10인 좌석', '통합 결제', '우선 지원'];
'Pro의 모든 기능',
'10인 좌석',
'통합 결제',
'우선 지원',
];
@override @override
String adRewarded(int minutes) => '+$minutes분 추가됨'; String adRewarded(int minutes) => '+$minutes분 추가됨';
@override @override
+27 -66
View File
@@ -61,8 +61,7 @@ class StringsRu extends AppText {
@override @override
String get watchAdMore => 'Смотрите рекламу, чтобы добавить время'; String get watchAdMore => 'Смотрите рекламу, чтобы добавить время';
@override @override
String get quotaExhaustedNotice => String get quotaExhaustedNotice => 'Дневной бесплатный лимит исчерпан. Оформите подписку, чтобы продолжить подключение.';
'Дневной бесплатный лимит исчерпан. Оформите подписку, чтобы продолжить подключение.';
@override @override
String get adPlaying => 'Реклама воспроизводится…'; String get adPlaying => 'Реклама воспроизводится…';
@override @override
@@ -70,8 +69,7 @@ class StringsRu extends AppText {
@override @override
String get quotaDesktopTitle => 'Дневной бесплатный лимит исчерпан'; String get quotaDesktopTitle => 'Дневной бесплатный лимит исчерпан';
@override @override
String get quotaDesktopBody => String get quotaDesktopBody => 'Смотрите рекламу в мобильном приложении, чтобы добавить время, или оформите подписку для безлимитного доступа.';
'Смотрите рекламу в мобильном приложении, чтобы добавить время, или оформите подписку для безлимитного доступа.';
@override @override
String get gotIt => 'Понятно'; String get gotIt => 'Понятно';
@override @override
@@ -81,16 +79,13 @@ class StringsRu extends AppText {
@override @override
String get smartSelect => 'Умный выбор'; String get smartSelect => 'Умный выбор';
@override @override
String get nodeSwitchBody => String get nodeSwitchBody => 'Текущее соединение будет разорвано и восстановлено через этот узел.';
'Текущее соединение будет разорвано и восстановлено через этот узел.';
@override @override
String get nodeSwitchConfirm => 'Переключить'; String get nodeSwitchConfirm => 'Переключить';
@override @override
String get nodeUnhealthySwitched => String get nodeUnhealthySwitched => 'Текущий узел недоступен — переключение выполнено автоматически';
'Текущий узел недоступен — переключение выполнено автоматически';
@override @override
String get nodeUnhealthyError => String get nodeUnhealthyError => 'Текущий узел недоступен — переподключитесь или смените узел';
'Текущий узел недоступен — переподключитесь или смените узел';
@override @override
String get nodeReconnecting => 'Сеть нестабильна, переподключение…'; String get nodeReconnecting => 'Сеть нестабильна, переподключение…';
@override @override
@@ -148,8 +143,7 @@ class StringsRu extends AppText {
@override @override
String get deviceLimitTitle => 'Достигнут лимит устройств'; String get deviceLimitTitle => 'Достигнут лимит устройств';
@override @override
String get deviceLimitDesc => String get deviceLimitDesc => 'Ваш тариф допускает до %s устройств. Удалите одно, чтобы продолжить.';
'Ваш тариф допускает до %s устройств. Удалите одно, чтобы продолжить.';
@override @override
String get deviceLimitRemoveOldest => 'Удалить давно неиспользуемое'; String get deviceLimitRemoveOldest => 'Удалить давно неиспользуемое';
@override @override
@@ -179,18 +173,15 @@ class StringsRu extends AppText {
@override @override
String get devSave => 'Сохранить'; String get devSave => 'Сохранить';
@override @override
String get devForceLogoutConfirm => String get devForceLogoutConfirm => 'Это завершит сеанс входа устройства — оно будет отключено и потребует повторного входа. Устройство останется в списке.';
'Это завершит сеанс входа устройства — оно будет отключено и потребует повторного входа. Устройство останется в списке.';
@override @override
String get devClearLoginConfirm => String get devClearLoginConfirm => 'Это полностью удалит устройство из списка и отзовёт его сеанс входа и учётные данные передачи данных. Чтобы снова использовать устройство, нужно войти заново.';
'Это полностью удалит устройство из списка и отзовёт его сеанс входа и учётные данные передачи данных. Чтобы снова использовать устройство, нужно войти заново.';
@override @override
String get devCancel => 'Отмена'; String get devCancel => 'Отмена';
@override @override
String get sessionRevokedTitle => 'Это устройство было отключено'; String get sessionRevokedTitle => 'Это устройство было отключено';
@override @override
String get sessionRevokedBody => String get sessionRevokedBody => 'Вход с этого устройства завершён с другого устройства вашего аккаунта. Пожалуйста, войдите снова.';
'Вход с этого устройства завершён с другого устройства вашего аккаунта. Пожалуйста, войдите снова.';
@override @override
String get sessionRevokedOk => 'Войти снова'; String get sessionRevokedOk => 'Войти снова';
@override @override
@@ -222,8 +213,7 @@ class StringsRu extends AppText {
@override @override
String get smartRoute => 'Умная маршрутизация'; String get smartRoute => 'Умная маршрутизация';
@override @override
String get smartRouteSub => String get smartRouteSub => 'Ускорение для зарубежных ресурсов, прямой доступ для локальных';
'Ускорение для зарубежных ресурсов, прямой доступ для локальных';
@override @override
String get killSwitch => 'Kill Switch'; String get killSwitch => 'Kill Switch';
@override @override
@@ -233,8 +223,7 @@ class StringsRu extends AppText {
@override @override
String get webUserCenter => 'Личный кабинет (веб)'; String get webUserCenter => 'Личный кабинет (веб)';
@override @override
String get updateNotesFallback => String get updateNotesFallback => 'Это обновление включает исправления и улучшения стабильности.';
'Это обновление включает исправления и улучшения стабильности.';
@override @override
String get updateLater => 'Позже'; String get updateLater => 'Позже';
@override @override
@@ -242,8 +231,7 @@ class StringsRu extends AppText {
@override @override
String get updateUpToDate => 'У вас установлена последняя версия'; String get updateUpToDate => 'У вас установлена последняя версия';
@override @override
String get updateCheckFailed => String get updateCheckFailed => 'Не удалось проверить обновления, попробуйте позже';
'Не удалось проверить обновления, попробуйте позже';
@override @override
String get freePlan => 'Бесплатный'; String get freePlan => 'Бесплатный';
@override @override
@@ -273,8 +261,7 @@ class StringsRu extends AppText {
@override @override
String get buyTitle => 'Где купить'; String get buyTitle => 'Где купить';
@override @override
String get buySub => String get buySub => 'Оплата внутри приложения недоступна. Получить код можно через:';
'Оплата внутри приложения недоступна. Получить код можно через:';
@override @override
String get chStore => 'Магазин самообслуживания'; String get chStore => 'Магазин самообслуживания';
@override @override
@@ -282,8 +269,7 @@ class StringsRu extends AppText {
@override @override
String get contactTitle => 'Связаться с нами'; String get contactTitle => 'Связаться с нами';
@override @override
String get contactIntro => String get contactIntro => 'Нужна помощь? Свяжитесь с нами любым способом ниже — обычно отвечаем за считанные минуты.';
'Нужна помощь? Свяжитесь с нами любым способом ниже — обычно отвечаем за считанные минуты.';
@override @override
String get contactEmail => 'Поддержка по эл. почте'; String get contactEmail => 'Поддержка по эл. почте';
@override @override
@@ -325,8 +311,7 @@ class StringsRu extends AppText {
@override @override
String get forgotPw => 'Забыли пароль?'; String get forgotPw => 'Забыли пароль?';
@override @override
String get tos => String get tos => 'Продолжая, вы принимаете наши Условия и Политику конфиденциальности';
'Продолжая, вы принимаете наши Условия и Политику конфиденциальности';
@override @override
String get obSkip => 'Пропустить'; String get obSkip => 'Пропустить';
@override @override
@@ -338,13 +323,11 @@ class StringsRu extends AppText {
@override @override
String get ob1Title => 'Подключение по всему миру'; String get ob1Title => 'Подключение по всему миру';
@override @override
String get ob1Sub => String get ob1Sub => '80+ локаций, автоматический выбор самого быстрого маршрута, надёжная стабильность.';
'80+ локаций, автоматический выбор самого быстрого маршрута, надёжная стабильность.';
@override @override
String get ob2Title => 'Разрешите настройку сети'; String get ob2Title => 'Разрешите настройку сети';
@override @override
String get ob2Sub => String get ob2Sub => 'Система запросит добавление сетевого профиля для создания зашифрованного туннеля.';
'Система запросит добавление сетевого профиля для создания зашифрованного туннеля.';
@override @override
String get ob3Title => 'Приватность по умолчанию'; String get ob3Title => 'Приватность по умолчанию';
@override @override
@@ -378,18 +361,15 @@ class StringsRu extends AppText {
@override @override
String get payNetworkLabel => 'Сеть'; String get payNetworkLabel => 'Сеть';
@override @override
String get payExactAmountHint => String get payExactAmountHint => 'Отправьте точную сумму — активация произойдёт автоматически';
'Отправьте точную сумму — активация произойдёт автоматически';
@override @override
String get copied => 'Скопировано'; String get copied => 'Скопировано';
@override @override
String get openAlipay => 'Оплатить через Alipay'; String get openAlipay => 'Оплатить через Alipay';
@override @override
String get openAlipayHint => String get openAlipayHint => 'Вернитесь после оплаты — страница обновится автоматически';
'Вернитесь после оплаты — страница обновится автоматически';
@override @override
String get openAlipayFailed => String get openAlipayFailed => 'Не удалось открыть Alipay. Проверьте, установлен ли он, или повторите';
'Не удалось открыть Alipay. Проверьте, установлен ли он, или повторите';
@override @override
String get awaitingPayment => 'Ожидание оплаты'; String get awaitingPayment => 'Ожидание оплаты';
@override @override
@@ -409,11 +389,9 @@ class StringsRu extends AppText {
@override @override
String get qrNotSupported => 'Скопируйте и откройте в Alipay'; String get qrNotSupported => 'Скопируйте и откройте в Alipay';
@override @override
String get orderCreateFailed => String get orderCreateFailed => 'Не удалось создать заказ, повторите попытку позже';
'Не удалось создать заказ, повторите попытку позже';
@override @override
String get payUnsupportedRenderType => String get payUnsupportedRenderType => 'Не поддерживается, смените способ оплаты';
'Не поддерживается, смените способ оплаты';
@override @override
String get payNoActiveOrder => 'Нет активных заказов'; String get payNoActiveOrder => 'Нет активных заказов';
@override @override
@@ -457,8 +435,7 @@ class StringsRu extends AppText {
@override @override
String get inviteEntry => 'Приглашайте друзей и получайте членство'; String get inviteEntry => 'Приглашайте друзей и получайте членство';
@override @override
String get inviteEntrySub => String get inviteEntrySub => 'Друг регистрируется · оба получают дни подписки';
'Друг регистрируется · оба получают дни подписки';
@override @override
String get inviteTitle => 'Пригласить друзей'; String get inviteTitle => 'Пригласить друзей';
@override @override
@@ -528,26 +505,11 @@ class StringsRu extends AppText {
@override @override
List<String> get days7 => const ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс']; List<String> get days7 => const ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'];
@override @override
List<String> get featsFree => const [ List<String> get featsFree => const ['Только 1 базовый узел', '10 минут в день', 'Смотрите рекламу, чтобы начать', '7 дней бесплатно при регистрации'];
'Только 1 базовый узел',
'10 минут в день',
'Смотрите рекламу, чтобы начать',
'7 дней бесплатно при регистрации',
];
@override @override
List<String> get featsPro => const [ List<String> get featsPro => const ['80+ локаций', 'Безлимитно · быстро', '5 устройств', 'Оптимизация для стриминга'];
'80+ локаций',
'Безлимитно · быстро',
'5 устройств',
'Оптимизация для стриминга',
];
@override @override
List<String> get featsTeam => const [ List<String> get featsTeam => const ['Всё из Pro', '10 мест', 'Единый счёт', 'Приоритетная поддержка'];
'Всё из Pro',
'10 мест',
'Единый счёт',
'Приоритетная поддержка',
];
@override @override
String adRewarded(int minutes) => '+$minutes мин добавлено'; String adRewarded(int minutes) => '+$minutes мин добавлено';
@override @override
@@ -555,8 +517,7 @@ class StringsRu extends AppText {
@override @override
String autoConnectingTo(String name) => 'Автоподключение к $name'; String autoConnectingTo(String name) => 'Автоподключение к $name';
@override @override
String updateAvailableTitle(String version) => String updateAvailableTitle(String version) => 'Доступна новая версия v$version';
'Доступна новая версия v$version';
@override @override
String codeSentTo(String email) => 'Код отправлен на $email'; String codeSentTo(String email) => 'Код отправлен на $email';
} }
+4 -20
View File
@@ -175,8 +175,7 @@ class StringsZh extends AppText {
@override @override
String get devForceLogoutConfirm => '将吊销该设备的登录会话,它会被踢下线、需重新登录。设备仍保留在列表中。'; String get devForceLogoutConfirm => '将吊销该设备的登录会话,它会被踢下线、需重新登录。设备仍保留在列表中。';
@override @override
String get devClearLoginConfirm => String get devClearLoginConfirm => '将从设备列表彻底移除此设备,并吊销其登录会话与数据面凭证。该设备需重新登录才能再次使用。';
'将从设备列表彻底移除此设备,并吊销其登录会话与数据面凭证。该设备需重新登录才能再次使用。';
@override @override
String get devCancel => '取消'; String get devCancel => '取消';
@override @override
@@ -506,26 +505,11 @@ class StringsZh extends AppText {
@override @override
List<String> get days7 => const ['', '', '', '', '', '', '']; List<String> get days7 => const ['', '', '', '', '', '', ''];
@override @override
List<String> get featsFree => const [ List<String> get featsFree => const ['仅 1 个基础节点', '每日 10 分钟时长', '使用前观看广告', '注册享 7 天免费试用'];
'仅 1 个基础节点',
'每日 10 分钟时长',
'使用前观看广告',
'注册享 7 天免费试用',
];
@override @override
List<String> get featsPro => const [ List<String> get featsPro => const ['80+ 全球节点', '无限流量 · 极速', '5 台设备同时在线', '流媒体优化'];
'80+ 全球节点',
'无限流量 · 极速',
'5 台设备同时在线',
'流媒体优化',
];
@override @override
List<String> get featsTeam => const [ List<String> get featsTeam => const ['专业版全部功能', '10 个成员席位', '集中计费与管理', '优先客服'];
'专业版全部功能',
'10 个成员席位',
'集中计费与管理',
'优先客服',
];
@override @override
String adRewarded(int minutes) => '已加 $minutes 分钟'; String adRewarded(int minutes) => '已加 $minutes 分钟';
@override @override
+4 -9
View File
@@ -7,12 +7,14 @@
// AppLangLabel / AppLangMisc 逻辑)。Dart 编译器强制 concrete⊆abstract 兜底漂移。 // AppLangLabel / AppLangMisc 逻辑)。Dart 编译器强制 concrete⊆abstract 兜底漂移。
// //
// 用法:node design/codegen/gen_l10n_dart.mjs // 用法:node design/codegen/gen_l10n_dart.mjs
// 生成后自动 `dart format`,保证 CI「重生成零 diff」闸稳定(漂移闸 = 重生成 + git diff)。 // 输出为确定性终态格式,不跑 dart format(其结果依赖包解析状态、跨环境不稳,
// 会让漂移闸误报)。漂移闸 = 纯 node 重生成 + git diff,无 dart 依赖。
// analyze 不校验格式,故手控格式即可;若单独启用 `dart format --set-exit-if-changed`,
// 需让本文件输出与之对齐(当前未启用)。
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import url from 'node:url'; import url from 'node:url';
import { execFileSync } from 'node:child_process';
const ROOT = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)), '..', '..'); const ROOT = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)), '..', '..');
const SRC = path.join(ROOT, 'design/i18n/strings.json'); const SRC = path.join(ROOT, 'design/i18n/strings.json');
@@ -72,12 +74,5 @@ for (const lang of LANGS) {
fs.writeFileSync(out, genFile(lang)); fs.writeFileSync(out, genFile(lang));
written.push(out); 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; 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}`); console.log(`✓ 生成 ${written.length} 份:getters=${g} lists=${l} methods=${me} × ${LANGS.length}`);