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:
@@ -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 单源一致。"
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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} 语`);
|
||||
|
||||
Reference in New Issue
Block a user