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
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
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
@@ -70,8 +69,7 @@ class StringsEn extends AppText {
@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
@@ -81,16 +79,13 @@ class StringsEn extends AppText {
@override
String get smartSelect => 'Smart select';
@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
@@ -148,8 +143,7 @@ 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
@@ -179,18 +173,15 @@ 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
@@ -232,8 +223,7 @@ class StringsEn extends AppText {
@override
String get webUserCenter => 'User Center (Web)';
@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
@@ -279,8 +269,7 @@ class StringsEn extends AppText {
@override
String get contactTitle => 'Contact us';
@override
String get contactIntro =>
'Need help? Reach us via any channel below — usually replies in minutes.';
String get contactIntro => 'Need help? Reach us via any channel below — usually replies in minutes.';
@override
String get contactEmail => 'Email support';
@override
@@ -338,8 +327,7 @@ 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
@@ -373,18 +361,15 @@ 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
@@ -520,26 +505,11 @@ class StringsEn extends AppText {
@override
List<String> get days7 => const ['M', 'T', 'W', 'T', 'F', 'S', 'S'];
@override
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',
];
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'];
@override
List<String> get featsPro => const [
'80+ locations',
'Unlimited · fast',
'5 devices',
'Streaming optimized',
];
List<String> get featsPro => const ['80+ locations', 'Unlimited · fast', '5 devices', 'Streaming optimized'];
@override
List<String> get featsTeam => const [
'Everything in Pro',
'10 seats',
'Central billing',
'Priority support',
];
List<String> get featsTeam => const ['Everything in Pro', '10 seats', 'Central billing', 'Priority support'];
@override
String adRewarded(int minutes) => '+$minutes min added';
@override
@@ -547,8 +517,7 @@ class StringsEn extends AppText {
@override
String autoConnectingTo(String name) => 'Auto-connecting to $name';
@override
String updateAvailableTitle(String version) =>
'New version v$version available';
String updateAvailableTitle(String version) => 'New version v$version available';
@override
String codeSentTo(String email) => 'Code sent to $email';
}
+27 -66
View File
@@ -61,8 +61,7 @@ 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
@@ -70,8 +69,7 @@ class StringsEs extends AppText {
@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
@@ -81,16 +79,13 @@ class StringsEs extends AppText {
@override
String get smartSelect => 'Selección inteligente';
@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
@@ -148,8 +143,7 @@ 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
@@ -179,18 +173,15 @@ 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
@@ -232,8 +223,7 @@ class StringsEs extends AppText {
@override
String get webUserCenter => 'Centro de usuario (Web)';
@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
@@ -241,8 +231,7 @@ 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
@@ -272,8 +261,7 @@ 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
@@ -281,8 +269,7 @@ class StringsEs extends AppText {
@override
String get contactTitle => 'Contáctanos';
@override
String get contactIntro =>
'¿Necesitas ayuda? Escríbenos por cualquiera de los canales de abajo: solemos responder en minutos.';
String get contactIntro => '¿Necesitas ayuda? Escríbenos por cualquiera de los canales de abajo: solemos responder en minutos.';
@override
String get contactEmail => 'Soporte por correo';
@override
@@ -324,8 +311,7 @@ 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
@@ -337,18 +323,15 @@ 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
@@ -378,18 +361,15 @@ 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
@@ -409,11 +389,9 @@ 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';
@override
@@ -473,8 +451,7 @@ 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
@@ -528,26 +505,11 @@ class StringsEs extends AppText {
@override
List<String> get days7 => const ['L', 'M', 'X', 'J', 'V', 'S', 'D'];
@override
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',
];
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'];
@override
List<String> get featsPro => const [
'Más de 80 ubicaciones',
'Ilimitado · rápido',
'5 dispositivos',
'Optimizado para streaming',
];
List<String> get featsPro => const ['Más de 80 ubicaciones', 'Ilimitado · rápido', '5 dispositivos', 'Optimizado para streaming'];
@override
List<String> get featsTeam => const [
'Todo lo de Pro',
'10 plazas',
'Facturación centralizada',
'Soporte prioritario',
];
List<String> get featsTeam => const ['Todo lo de Pro', '10 plazas', 'Facturación centralizada', 'Soporte prioritario'];
@override
String adRewarded(int minutes) => '+$minutes min añadidos';
@override
@@ -555,8 +517,7 @@ class StringsEs extends AppText {
@override
String autoConnectingTo(String name) => 'Conectando automáticamente a $name';
@override
String updateAvailableTitle(String version) =>
'Nueva versión v$version disponible';
String updateAvailableTitle(String version) => 'Nueva versión v$version disponible';
@override
String codeSentTo(String email) => 'Código enviado a $email';
}
+7 -26
View File
@@ -69,8 +69,7 @@ class StringsJa extends AppText {
@override
String get quotaDesktopTitle => '本日の無料時間を使い切りました';
@override
String get quotaDesktopBody =>
'モバイルアプリで広告を見ると時間を追加できます。無制限に使うにはアップグレードしてください。';
String get quotaDesktopBody => 'モバイルアプリで広告を見ると時間を追加できます。無制限に使うにはアップグレードしてください。';
@override
String get gotIt => '了解';
@override
@@ -174,18 +173,15 @@ 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
@@ -509,26 +505,11 @@ class StringsJa extends AppText {
@override
List<String> get days7 => const ['', '', '', '', '', '', ''];
@override
List<String> get featsFree => const [
'基本ノード1つのみ',
'1日10分',
'広告を見て開始',
'登録で7日間無料トライアル',
];
List<String> get featsFree => const ['基本ノード1つのみ', '1日10分', '広告を見て開始', '登録で7日間無料トライアル'];
@override
List<String> get featsPro => const [
'80以上の拠点',
'無制限 · 高速',
'5台のデバイス',
'ストリーミング最適化',
];
List<String> get featsPro => const ['80以上の拠点', '無制限 · 高速', '5台のデバイス', 'ストリーミング最適化'];
@override
List<String> get featsTeam => const [
'Proのすべての機能',
'10ライセンス',
'一括請求',
'優先サポート',
];
List<String> get featsTeam => const ['Proのすべての機能', '10ライセンス', '一括請求', '優先サポート'];
@override
String adRewarded(int minutes) => '+$minutes 分を追加しました';
@override
+7 -26
View File
@@ -61,8 +61,7 @@ class StringsKo extends AppText {
@override
String get watchAdMore => '광고 시청으로 시간 추가';
@override
String get quotaExhaustedNotice =>
'오늘 무료 시간을 모두 사용했습니다. 업그레이드하면 계속 연결할 수 있습니다.';
String get quotaExhaustedNotice => '오늘 무료 시간을 모두 사용했습니다. 업그레이드하면 계속 연결할 수 있습니다.';
@override
String get adPlaying => '광고 재생 중…';
@override
@@ -70,8 +69,7 @@ class StringsKo extends AppText {
@override
String get quotaDesktopTitle => '오늘 무료 시간을 모두 사용함';
@override
String get quotaDesktopBody =>
'모바일 앱에서 광고를 시청해 시간을 추가하거나, 업그레이드하여 무제한으로 이용하세요.';
String get quotaDesktopBody => '모바일 앱에서 광고를 시청해 시간을 추가하거나, 업그레이드하여 무제한으로 이용하세요.';
@override
String get gotIt => '확인';
@override
@@ -175,11 +173,9 @@ 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
@@ -509,26 +505,11 @@ class StringsKo extends AppText {
@override
List<String> get days7 => const ['', '', '', '', '', '', ''];
@override
List<String> get featsFree => const [
'기본 노드 1개',
'하루 10분',
'광고 시청 후 시작',
'가입 시 7일 무료 체험',
];
List<String> get featsFree => const ['기본 노드 1개', '하루 10분', '광고 시청 후 시작', '가입 시 7일 무료 체험'];
@override
List<String> get featsPro => const [
'80개 이상 지역',
'무제한 · 빠른 속도',
'기기 5대',
'스트리밍 최적화',
];
List<String> get featsPro => const ['80개 이상 지역', '무제한 · 빠른 속도', '기기 5대', '스트리밍 최적화'];
@override
List<String> get featsTeam => const [
'Pro의 모든 기능',
'10인 좌석',
'통합 결제',
'우선 지원',
];
List<String> get featsTeam => const ['Pro의 모든 기능', '10인 좌석', '통합 결제', '우선 지원'];
@override
String adRewarded(int minutes) => '+$minutes분 추가됨';
@override
+27 -66
View File
@@ -61,8 +61,7 @@ class StringsRu extends AppText {
@override
String get watchAdMore => 'Смотрите рекламу, чтобы добавить время';
@override
String get quotaExhaustedNotice =>
'Дневной бесплатный лимит исчерпан. Оформите подписку, чтобы продолжить подключение.';
String get quotaExhaustedNotice => 'Дневной бесплатный лимит исчерпан. Оформите подписку, чтобы продолжить подключение.';
@override
String get adPlaying => 'Реклама воспроизводится…';
@override
@@ -70,8 +69,7 @@ class StringsRu extends AppText {
@override
String get quotaDesktopTitle => 'Дневной бесплатный лимит исчерпан';
@override
String get quotaDesktopBody =>
'Смотрите рекламу в мобильном приложении, чтобы добавить время, или оформите подписку для безлимитного доступа.';
String get quotaDesktopBody => 'Смотрите рекламу в мобильном приложении, чтобы добавить время, или оформите подписку для безлимитного доступа.';
@override
String get gotIt => 'Понятно';
@override
@@ -81,16 +79,13 @@ class StringsRu extends AppText {
@override
String get smartSelect => 'Умный выбор';
@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
@@ -148,8 +143,7 @@ class StringsRu extends AppText {
@override
String get deviceLimitTitle => 'Достигнут лимит устройств';
@override
String get deviceLimitDesc =>
'Ваш тариф допускает до %s устройств. Удалите одно, чтобы продолжить.';
String get deviceLimitDesc => 'Ваш тариф допускает до %s устройств. Удалите одно, чтобы продолжить.';
@override
String get deviceLimitRemoveOldest => 'Удалить давно неиспользуемое';
@override
@@ -179,18 +173,15 @@ 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
@@ -222,8 +213,7 @@ class StringsRu extends AppText {
@override
String get smartRoute => 'Умная маршрутизация';
@override
String get smartRouteSub =>
'Ускорение для зарубежных ресурсов, прямой доступ для локальных';
String get smartRouteSub => 'Ускорение для зарубежных ресурсов, прямой доступ для локальных';
@override
String get killSwitch => 'Kill Switch';
@override
@@ -233,8 +223,7 @@ class StringsRu extends AppText {
@override
String get webUserCenter => 'Личный кабинет (веб)';
@override
String get updateNotesFallback =>
'Это обновление включает исправления и улучшения стабильности.';
String get updateNotesFallback => 'Это обновление включает исправления и улучшения стабильности.';
@override
String get updateLater => 'Позже';
@override
@@ -242,8 +231,7 @@ class StringsRu extends AppText {
@override
String get updateUpToDate => 'У вас установлена последняя версия';
@override
String get updateCheckFailed =>
'Не удалось проверить обновления, попробуйте позже';
String get updateCheckFailed => 'Не удалось проверить обновления, попробуйте позже';
@override
String get freePlan => 'Бесплатный';
@override
@@ -273,8 +261,7 @@ class StringsRu extends AppText {
@override
String get buyTitle => 'Где купить';
@override
String get buySub =>
'Оплата внутри приложения недоступна. Получить код можно через:';
String get buySub => 'Оплата внутри приложения недоступна. Получить код можно через:';
@override
String get chStore => 'Магазин самообслуживания';
@override
@@ -282,8 +269,7 @@ class StringsRu extends AppText {
@override
String get contactTitle => 'Связаться с нами';
@override
String get contactIntro =>
'Нужна помощь? Свяжитесь с нами любым способом ниже — обычно отвечаем за считанные минуты.';
String get contactIntro => 'Нужна помощь? Свяжитесь с нами любым способом ниже — обычно отвечаем за считанные минуты.';
@override
String get contactEmail => 'Поддержка по эл. почте';
@override
@@ -325,8 +311,7 @@ class StringsRu extends AppText {
@override
String get forgotPw => 'Забыли пароль?';
@override
String get tos =>
'Продолжая, вы принимаете наши Условия и Политику конфиденциальности';
String get tos => 'Продолжая, вы принимаете наши Условия и Политику конфиденциальности';
@override
String get obSkip => 'Пропустить';
@override
@@ -338,13 +323,11 @@ 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
@@ -378,18 +361,15 @@ 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
@@ -409,11 +389,9 @@ 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 => 'Нет активных заказов';
@override
@@ -457,8 +435,7 @@ class StringsRu extends AppText {
@override
String get inviteEntry => 'Приглашайте друзей и получайте членство';
@override
String get inviteEntrySub =>
'Друг регистрируется · оба получают дни подписки';
String get inviteEntrySub => 'Друг регистрируется · оба получают дни подписки';
@override
String get inviteTitle => 'Пригласить друзей';
@override
@@ -528,26 +505,11 @@ class StringsRu extends AppText {
@override
List<String> get days7 => const ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'];
@override
List<String> get featsFree => const [
'Только 1 базовый узел',
'10 минут в день',
'Смотрите рекламу, чтобы начать',
'7 дней бесплатно при регистрации',
];
List<String> get featsFree => const ['Только 1 базовый узел', '10 минут в день', 'Смотрите рекламу, чтобы начать', '7 дней бесплатно при регистрации'];
@override
List<String> get featsPro => const [
'80+ локаций',
'Безлимитно · быстро',
'5 устройств',
'Оптимизация для стриминга',
];
List<String> get featsPro => const ['80+ локаций', 'Безлимитно · быстро', '5 устройств', 'Оптимизация для стриминга'];
@override
List<String> get featsTeam => const [
'Всё из Pro',
'10 мест',
'Единый счёт',
'Приоритетная поддержка',
];
List<String> get featsTeam => const ['Всё из Pro', '10 мест', 'Единый счёт', 'Приоритетная поддержка'];
@override
String adRewarded(int minutes) => '+$minutes мин добавлено';
@override
@@ -555,8 +517,7 @@ class StringsRu extends AppText {
@override
String autoConnectingTo(String name) => 'Автоподключение к $name';
@override
String updateAvailableTitle(String version) =>
'Доступна новая версия v$version';
String updateAvailableTitle(String version) => 'Доступна новая версия v$version';
@override
String codeSentTo(String email) => 'Код отправлен на $email';
}
+4 -20
View File
@@ -175,8 +175,7 @@ class StringsZh extends AppText {
@override
String get devForceLogoutConfirm => '将吊销该设备的登录会话,它会被踢下线、需重新登录。设备仍保留在列表中。';
@override
String get devClearLoginConfirm =>
'将从设备列表彻底移除此设备,并吊销其登录会话与数据面凭证。该设备需重新登录才能再次使用。';
String get devClearLoginConfirm => '将从设备列表彻底移除此设备,并吊销其登录会话与数据面凭证。该设备需重新登录才能再次使用。';
@override
String get devCancel => '取消';
@override
@@ -506,26 +505,11 @@ class StringsZh extends AppText {
@override
List<String> get days7 => const ['', '', '', '', '', '', ''];
@override
List<String> get featsFree => const [
'仅 1 个基础节点',
'每日 10 分钟时长',
'使用前观看广告',
'注册享 7 天免费试用',
];
List<String> get featsFree => const ['仅 1 个基础节点', '每日 10 分钟时长', '使用前观看广告', '注册享 7 天免费试用'];
@override
List<String> get featsPro => const [
'80+ 全球节点',
'无限流量 · 极速',
'5 台设备同时在线',
'流媒体优化',
];
List<String> get featsPro => const ['80+ 全球节点', '无限流量 · 极速', '5 台设备同时在线', '流媒体优化'];
@override
List<String> get featsTeam => const [
'专业版全部功能',
'10 个成员席位',
'集中计费与管理',
'优先客服',
];
List<String> get featsTeam => const ['专业版全部功能', '10 个成员席位', '集中计费与管理', '优先客服'];
@override
String adRewarded(int minutes) => '已加 $minutes 分钟';
@override
+4 -9
View File
@@ -7,12 +7,14 @@
// AppLangLabel / AppLangMisc 逻辑)。Dart 编译器强制 concrete⊆abstract 兜底漂移。
//
// 用法: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 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');
@@ -72,12 +74,5 @@ for (const lang of LANGS) {
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}`);