Browse Source

fix(i18n): remove English placeholders in es_ES

newdev_shunjiawei
lxm 7 months ago
parent
commit
e8eb218a7d
  1. 51
      I18N_PLACEHOLDER_REMEDIATION.md
  2. 332
      lib/core/translations/language/es_es.dart
  3. 22
      test/es_es_translations_test.dart

51
I18N_PLACEHOLDER_REMEDIATION.md

@ -0,0 +1,51 @@
# I18N Placeholder Remediation
This document records the repeatable steps used to detect and fix leftover English placeholder text and invalid-quote issues in translation map files under `lib/core/translations/language/`.
## Problem Patterns
### 1) English placeholders inside non-English locales
Symptoms:
- UI shows English strings in a non-English locale.
- `tools/i18n_audit.py --mode englishy` reports items for a locale file.
Fix:
- Replace English values with accurate localized text.
- Keep placeholder syntax intact (examples: `@minutes`, `{count}`, `{filePath}`, `{error}`).
### 2) Invalid “smart quotes” used as Dart string delimiters
Symptoms:
- `flutter analyze` reports `illegal_character`, `expected_token`, `expression_in_map`, `undefined_identifier` in a language file.
Root cause:
- Curly quotes like `‘ ’` are not valid Dart string delimiters, so map entries stop being parsed as strings.
Fix:
- Replace `‘` and `’` with ASCII `'` and ensure every entry is in the form `'key': 'value',`.
## Verification Checklist
Run these commands from the repo root:
```bash
python3 tools/i18n_audit.py --mode englishy
flutter analyze lib/core/translations/language/<locale>.dart
dart format lib/core/translations/language/<locale>.dart
flutter test test/<locale>_translations_test.dart
```
Recommended additional checks:
```bash
rg "[‘’]" lib/core/translations/language/<locale>.dart
```
## Test Convention
For each locale file, add a `test/<locale>_translations_test.dart` test that asserts:
- The map can be imported and accessed.
- Its key set matches `enUS`.
- It contains no empty string values.

332
lib/core/translations/language/es_es.dart

@ -238,36 +238,36 @@ const Map<String, String> esES = {
"translationApi": "API de traducción",
"asr": "ASR",
"tts": "TTS",
"dataVolumeStats": "Data Volume Stats",
"sourceTextCharacters": "Source Text Characters",
"ttsTextCharacters": "TTS Text Characters",
"effectiveAudioDuration": "Effective Audio Duration",
"totalSessionDuration": "Total Session Duration",
"costStats": "Cost Stats",
"estimatedCost": "Estimated Cost",
"dailyCostThreshold": "Daily Cost Threshold",
"dataVolumeStats": "Estadísticas de volumen de datos",
"sourceTextCharacters": "Caracteres del texto fuente",
"ttsTextCharacters": "Caracteres de texto TTS",
"effectiveAudioDuration": "Duración de audio efectiva",
"totalSessionDuration": "Duración total de la sesión",
"costStats": "Estadísticas de coste",
"estimatedCost": "Coste estimado",
"dailyCostThreshold": "Umbral de coste diario",
"monthlyCostThreshold": "Umbral de costo mensual",
"usageBehaviorStats": "Usage Behavior Stats",
"mostUsedLanguagePair": "Most-used language pair",
"mostUsedMode": "Most-used mode",
"asrApiCalls": "ASR Calls",
"totalAudioDuration": "Total Audio Duration",
"usageBehaviorStats": "Estadísticas de comportamiento de uso",
"mostUsedLanguagePair": "Par de idiomas más usado",
"mostUsedMode": "Modo más usado",
"asrApiCalls": "Llamadas ASR",
"totalAudioDuration": "Duración total de audio",
"audioFileSize": "Tamaño del archivo de audio",
"transcriptBytes": "Bytes de transcripción",
"loadFailed": "Error al cargar",
"statsLoadFailedTryLater":
"No se pudieron cargar las estadísticas. Inténtalo de nuevo más tarde.",
"cleanupComplete": "Cleanup Complete",
"cleanedOldData": "Cleaned data older than 90 days",
"exportFeature": "Export Feature",
"exportInDevelopment": "Data export is under development",
"cleanupComplete": "Limpieza completada",
"cleanedOldData": "Datos de más de 90 días eliminados",
"exportFeature": "Función de exportación",
"exportInDevelopment": "La exportación de datos está en desarrollo",
// 商品模块 - 通用
'availablePackages': 'Available Packages',
'remainingQuota': 'Remaining Quota',
'totalQuota': 'Total Quota',
'remaining': 'Remaining',
'featureDescription': 'Feature Description',
'availablePackages': 'Paquetes disponibles',
'remainingQuota': 'Cuota restante',
'totalQuota': 'Cuota total',
'remaining': 'Restante',
'featureDescription': 'Descripción de función',
'paymentSuccess': 'Pago exitoso',
'paymentFailed': 'Pago fallido',
'gotIt': 'Entendido',
@ -335,101 +335,108 @@ const Map<String, String> esES = {
'La suscripción de IA ha caducado, ve a la página de recarga de membresía para renovarla.',
// 商品模块 - AI商品页
'musicAndRecommendation': 'Music & Recommendation',
'navigationAndRoutes': 'Navigation & Routes',
'nearbySearch': 'Nearby Search',
'newsInformation': 'News',
'communicationAssistant': 'Communication Assistant',
'musicAndRecommendation': 'Música y recomendaciones',
'navigationAndRoutes': 'Navegación y rutas',
'nearbySearch': 'Búsqueda cercana',
'newsInformation': 'Noticias',
'communicationAssistant': 'Asistente de comunicación',
'aiHeaderDesc1':
'Longer conversation quota, more tool calls, extended context length.',
'Mayor cuota de conversación, más llamadas de herramientas, mayor longitud de contexto.',
'aiHeaderDesc2':
'Suitable for agent Q&A, summarization, analysis, and task execution.',
'musicSearchPlaybackControl': 'Music library search and playback control',
'Ideal para preguntas de agente, resúmenes, análisis y ejecución de tareas.',
'musicSearchPlaybackControl':
'Búsqueda en biblioteca musical y control de reproducción',
'smartPlaylistAndSimilarRecommendations':
'Smart playlist and similar song recommendations',
'Lista de reproducción inteligente y recomendaciones de canciones similares',
'routePlanningAndCongestionAvoidance':
'Route planning and congestion avoidance',
'voiceBroadcastAndWaypoints': 'Voice broadcast and waypoint settings',
'realTimeWeatherForecast': 'Real-time weather and forecast',
'clothingAdviceAndTravelReminders': 'Clothing advice and travel reminders',
'poiSearchFoodGasParking': 'POI search: dining / gas / parking',
'ratingsHoursAndDirectRoutes': 'Ratings, business hours, and direct routing',
'hotNewsAndTopicOverview': 'Hot news and topic overview',
'longArticleSummariesAndKeyPoints': 'Long-article summaries and key points',
'helpCallAndSMS': 'Help with calls and SMS',
'Planificación de rutas y evitación de congestión',
'voiceBroadcastAndWaypoints':
'Emisión de voz y configuración de puntos de paso',
'realTimeWeatherForecast': 'Tiempo en tiempo real y previsión meteorológica',
'clothingAdviceAndTravelReminders':
'Consejos de ropa y recordatorios de viaje',
'poiSearchFoodGasParking':
'Búsqueda de POI: restaurantes / gasolineras / aparcamiento',
'ratingsHoursAndDirectRoutes':
'Valoraciones, horarios de apertura y ruta directa',
'hotNewsAndTopicOverview': 'Noticias destacadas y resumen de temas',
'longArticleSummariesAndKeyPoints':
'Resúmenes de artículos largos y puntos clave',
'helpCallAndSMS': 'Ayuda con llamadas y SMS',
'templatedMessagesAndScheduleReminders':
'Mensajes de plantilla y recordatorios programados',
'aiDisclaimer':
'Las funciones dependen de los permisos del dispositivo, la disponibilidad del servicio y las condiciones de la red.',
// 商品模块 - 翻译商品页
'realtimeTranscription': 'Real-time Transcription',
'sceneLexicon': 'Scenario Glossary',
'subtitleDisplay': 'Subtitles',
'realtimeTranscription': 'Transcripción en tiempo real',
'sceneLexicon': 'Glosario de escenarios',
'subtitleDisplay': 'Subtítulos',
'transHeaderDesc1':
'Interpretación simultánea en tiempo real y traducción de texto con voz de baja latencia.',
'transHeaderDesc2': 'Suitable for travel, business, speeches, and teaching.',
'transHeaderDesc2': 'Ideal para viajes, negocios, conferencias y enseñanza.',
'translateZhEn': 'Traducción chino–inglés',
'lowLatencyVoiceBroadcast': 'Low-latency voice broadcast',
'lowLatencyVoiceBroadcast': 'Emisión de voz de baja latencia',
'multiLangTextTranslation': 'Traducción de texto multilingüe',
'termOptimizationContextPreserve':
'Terminology optimization and context preservation',
'lexiconTourBusinessLife': 'Travel/Business/Life lexicons',
'personalVocabularyMemory': 'Personal vocabulary memory',
'conversationSubtitleLargeText': 'Conversation subtitles / Large-text mode',
'colorHighContrast': 'High color contrast',
'Optimización de terminología y preservación del contexto',
'lexiconTourBusinessLife': 'Léxicos de viaje/negocios/vida cotidiana',
'personalVocabularyMemory': 'Memoria de vocabulario personal',
'conversationSubtitleLargeText':
'Subtítulos de conversación / Modo texto grande',
'colorHighContrast': 'Alto contraste de color',
'transDisclaimer':
'El rendimiento de la traducción depende del micrófono, la latencia de la red y el ruido ambiental.',
// 商品模块 - 会议商品页
'meetingRecording': 'Grabación de reuniones',
'transcriptionAndSummary': 'Transcription & Summary',
'keywordSearch': 'Keyword Search',
'collaborationSharing': 'Collaboration & Sharing',
'transcriptionAndSummary': 'Transcripción y resumen',
'keywordSearch': 'Búsqueda por palabra clave',
'collaborationSharing': 'Colaboración y uso compartido',
'meetingHeaderDesc1':
'Grabación de audio, transcripción, búsqueda por palabras clave y resumen automático para mejorar la eficiencia de las reuniones.',
'meetingHeaderDesc2':
'Suitable for business meetings, class notes, and interview minutes.',
'Ideal para reuniones de negocios, apuntes de clase y actas de entrevistas.',
'meetingAutoRecording': 'Grabación y segmentación automática',
'meetingSpeakerTagging': 'Speaker labeling',
'meetingLongAudioTranscription': 'Long audio transcription',
'meetingKeyPointSummary': 'Key point extraction',
'meetingTopicNameSearch': 'Search by topic/name',
'meetingTimelineJump': 'Timeline jump',
'meetingExportSummary': 'Export minutes',
'meetingMultiPlatformShare': 'Share across platforms',
'meetingSpeakerTagging': 'Etiquetado de hablantes',
'meetingLongAudioTranscription': 'Transcripción de audio largo',
'meetingKeyPointSummary': 'Extracción de puntos clave',
'meetingTopicNameSearch': 'Búsqueda por tema/nombre',
'meetingTimelineJump': 'Salto en la línea de tiempo',
'meetingExportSummary': 'Exportar actas',
'meetingMultiPlatformShare': 'Compartir en múltiples plataformas',
'meetingDisclaimer':
'Úsalo en entornos silenciosos; el rendimiento depende del micrófono y la red.',
// 商品模块 - VIP会员页
'benefitsOverview': 'Benefits overview',
'higherQuota': 'Higher usage quota',
'lowerUnitPrice': 'Lower unit price',
'priorityService': 'Priority service',
'exclusiveSupportChannel': 'Dedicated support channel',
'fastResponse': 'Fast issue response',
'eventsAndDiscounts': 'Events and discounts',
'memberExclusiveEvents': 'Member-exclusive events',
'longTermDiscountPlan': 'Long-term discount plan',
'generalPrivileges': 'General privileges',
'earlyAccessNewFeatures': 'Early access to new features',
'benefitsOverview': 'Resumen de beneficios',
'higherQuota': 'Mayor cuota de uso',
'lowerUnitPrice': 'Menor precio unitario',
'priorityService': 'Servicio prioritario',
'exclusiveSupportChannel': 'Canal de soporte exclusivo',
'fastResponse': 'Respuesta rápida a incidencias',
'eventsAndDiscounts': 'Eventos y descuentos',
'memberExclusiveEvents': 'Eventos exclusivos para miembros',
'longTermDiscountPlan': 'Plan de descuento a largo plazo',
'generalPrivileges': 'Privilegios generales',
'earlyAccessNewFeatures': 'Acceso anticipado a nuevas funciones',
'crossDeviceSync': 'Sincronización entre dispositivos',
'vipDisclaimer':
'Actual benefits depend on the package details and may change with versions.',
'Los beneficios reales dependen de los detalles del paquete y pueden variar con las versiones.',
// 商品模块 - 商品名称与描述
'ai_001_name': 'AI Basic 100',
'ai_002_name': 'AI Basic 500',
'ai_003_name': 'AI Basic 1000',
'meet_001_name': 'Meeting Basic 100',
'meet_002_name': 'Meeting Basic 500',
'meet_003_name': 'Meeting Basic 1000',
'ai_001_name': 'IA Básico 100',
'ai_002_name': 'IA Básico 500',
'ai_003_name': 'IA Básico 1000',
'meet_001_name': 'Reunión Básica 100',
'meet_002_name': 'Reunión Básica 500',
'meet_003_name': 'Reunión Básica 1000',
'trans_001_name': 'Traducción Básica 100',
'trans_002_name': 'Traducción Básica 500',
'trans_003_name': 'Traducción Básica 1000',
'ai_001_desc': 'Includes 100 uses. Access core AI features.',
'ai_002_desc': 'Includes 500 uses. Access core AI features.',
'ai_003_desc': 'Includes 1000 uses. Access core AI features.',
'ai_001_desc': 'Incluye 100 usos. Accede a las funciones principales de IA.',
'ai_002_desc': 'Incluye 500 usos. Accede a las funciones principales de IA.',
'ai_003_desc': 'Incluye 1000 usos. Accede a las funciones principales de IA.',
'meet_001_desc': 'Incluye 100 grabaciones/transcripciones de reuniones.',
'meet_002_desc': 'Incluye 500 grabaciones/transcripciones de reuniones.',
'meet_003_desc': 'Incluye 1000 grabaciones/transcripciones de reuniones.',
@ -443,41 +450,44 @@ const Map<String, String> esES = {
'copyrightVoitrans': '© 2025 Voitrans Technologies',
// 新手引导
'skip': "Omitir",
'next': 'Next',
'sayLilyHint': "Say \"Lily Lily\" to the mic",
'imHere': "I’m here~",
'guideWelcomeWakeAI': "Welcome to Voitrans! First, wake your AI “Lily”.",
'aiReplyCompleted': "Lily has finished the reply!",
'aiThinking': "Lily is thinking…",
'guideAwakenSuccess': "Great! Lily is awake—ask it to help.",
'confirmInput': "Confirm Input",
'guideClickSendPrefix': "Tap send, ",
'guideWeatherStart': "Tell Lily: “What’s the weather today?”",
'guideWeatherInput': "What’s the weather today?",
'guideWeatherSend': "Lily will report the weather in real time",
'guideWeatherEnd': "Lily can report weather live—try other commands",
'guideMusicStart': "Tell Lily: “Play some rock music”",
'guideMusicInput': "Play some rock music",
'guideMusicSend': "Lily will find music for you",
'guideMusicEnd': "Lily found music—try other commands",
'guideNavStart': "Tell Lily: “Navigate to Shenzhen Airport”",
'guideNavInput': "Navigate to Shenzhen Airport",
'guideNavSend': "Lily will plan the route",
'guideNavEnd': "Navigation set—try other commands",
'skip': 'Omitir',
'next': 'Siguiente',
'sayLilyHint': 'Di “Lily Lily” al micrófono',
'imHere': 'Aquí estoy~',
'guideWelcomeWakeAI':
'Bienvenido a Voitrans! Primero, despierta a tu IA “Lily”.',
'aiReplyCompleted': 'Lily ha terminado de responder!',
'aiThinking': 'Lily está pensando...',
'guideAwakenSuccess': 'Genial! Lily está despierta—pídele ayuda.',
'confirmInput': 'Confirmar entrada',
'guideClickSendPrefix': 'Toca enviar, ',
'guideWeatherStart': 'Dile a Lily: “¿Qué tiempo hace hoy?”',
'guideWeatherInput': '¿Qué tiempo hace hoy?',
'guideWeatherSend': 'Lily te informará del tiempo en tiempo real',
'guideWeatherEnd':
'Lily puede informar del tiempo en vivo—prueba otros comandos',
'guideMusicStart': 'Dile a Lily: “Pon algo de rock”',
'guideMusicInput': 'Pon algo de rock',
'guideMusicSend': 'Lily buscará música para ti',
'guideMusicEnd': 'Lily encontró música—prueba otros comandos',
'guideNavStart': 'Dile a Lily: “Navega al Aeropuerto de Barajas”',
'guideNavInput': 'Navega al Aeropuerto de Barajas',
'guideNavSend': 'Lily planificará la ruta',
'guideNavEnd': 'Navegación configurada—prueba otros comandos',
'guideMeetingStart':
"Tell Lily: “Create a meeting reminder for 3pm tomorrow”",
'guideMeetingInput': "Create a meeting reminder for 3pm tomorrow",
'guideMeetingSend': "Lily will create a meeting reminder",
'guideMeetingEnd': "Reminder created—try other commands",
'guidePhoneStart': "Tell Lily: “Call Mom”",
'guidePhoneInput': "Call Mom",
'guidePhoneSend': "Lily will make the call",
'guidePhoneEnd': "Call placed—try other commands",
'guideSmsStart': "Tell Lily: “Send Dad a text: I’ll be home for dinner”",
'guideSmsInput': "Text Dad: I’ll be home for dinner",
'guideSmsSend': "Lily will send the message",
'guideSmsEnd': "Message sent—voice control is convenient",
'Dile a Lily: “Crea un recordatorio de reunión para mañana a las 15:00”',
'guideMeetingInput':
'Crea un recordatorio de reunión para mañana a las 15:00',
'guideMeetingSend': 'Lily creará un recordatorio de reunión',
'guideMeetingEnd': 'Recordatorio creado—prueba otros comandos',
'guidePhoneStart': 'Dile a Lily: “Llama a mamá”',
'guidePhoneInput': 'Llama a mamá',
'guidePhoneSend': 'Lily realizará la llamada',
'guidePhoneEnd': 'Llamada realizada—prueba otros comandos',
'guideSmsStart': 'Dile a Lily: “Envía un mensaje a papá: Llego a cenar”',
'guideSmsInput': 'Mensaje a papá: Llego a cenar',
'guideSmsSend': 'Lily enviará el mensaje',
'guideSmsEnd': 'Mensaje enviado—el control por voz es muy cómodo',
// Meeting模块 - 首页与导航
"startMeeting": "Iniciar reunión",
@ -506,9 +516,10 @@ const Map<String, String> esES = {
'aiPenNotFound': 'Dispositivo AI PEN no encontrado',
'connectedToAiPen': 'Conectado al dispositivo AI PEN',
'checkingNewFiles': 'Comprobando archivos nuevos para importar',
'startingWifi': 'Starting Wi-Fi...',
'newFilesDetected': 'New files detected, preparing to import',
'noNewFiles': 'No new files to import',
'startingWifi': 'Iniciando Wi‑Fi...',
'newFilesDetected':
'Se han detectado nuevos archivos, preparando la importación',
'noNewFiles': 'No hay archivos nuevos para importar',
'connectingWifi': 'Conectando a la red Wi‑Fi...',
'wifiConnected': 'Conexión Wi‑Fi realizada correctamente',
'downloadingFiles': 'Descargando archivos...',
@ -523,7 +534,7 @@ const Map<String, String> esES = {
'liveRecording': 'Grabación en vivo',
'audioVideoRecording': 'Grabación de audio/vídeo',
'callRecording': 'Grabación de llamadas',
'importAudio': 'Import Audio',
'importAudio': 'Importar audio',
'meetingRecordExitTitle': 'Salir de la grabación', // 退出录音
'meetingRecordExitMessage': 'Elige qué hacer antes de salir', // 请选择离开后的操作
'meetingRecordEndAndSave': 'Finalizar y guardar', // 结束录音并保存
@ -546,25 +557,25 @@ const Map<String, String> esES = {
'Para mostrar la barra flotante de grabación (temporizador/pausa/reanudar) durante la grabación, necesitamos el permiso de superposición.', // 为了在录音过程中显示录音悬浮条(计时/暂停/继续),需要悬浮窗权限。
'overlayPermissionOpenSettingsFailed':
'No se pueden abrir los ajustes del sistema automáticamente. Abre Ajustes y habilita manualmente el permiso de superposición.', // 无法自动跳转到系统设置,请手动前往系统设置开启悬浮窗权限。
'transcribing': 'Transcribing...',
'summarizing': 'Summarizing...',
'analyzing': 'Analyzing...',
'summaryComplete': 'Summary Complete',
'generate': 'Generate',
'overview': 'Overview',
'transcription': 'Transcription',
'summary': 'Summary',
'mindMap': 'Mind Map',
'editSpeaker': 'Edit',
'nameSpeaker': 'Name Speaker',
'transcribing': 'Transcribiendo...',
'summarizing': 'Resumiendo...',
'analyzing': 'Analizando...',
'summaryComplete': 'Resumen completado',
'generate': 'Generar',
'overview': 'Vista general',
'transcription': 'Transcripción',
'summary': 'Resumen',
'mindMap': 'Mapa mental',
'editSpeaker': 'Editar',
'nameSpeaker': 'Nombrar al orador',
// Meeting模块 - 转写与总结
'reSummarize': 'Re-summarize',
'dateAndTime': 'Date & Time',
'location': 'Location',
'attendees': 'Attendees',
'inputLocation': 'Enter Location',
'inputAttendees': 'Enter Attendees',
'reSummarize': 'Volver a resumir',
'dateAndTime': 'Fecha y hora',
'location': 'Ubicación',
'attendees': 'Asistentes',
'inputLocation': 'Introducir ubicación',
'inputAttendees': 'Introducir asistentes',
// Meeting模块 - 模版与生成
'selectSummaryTemplate': 'Seleccionar plantilla de resumen',
@ -637,12 +648,12 @@ const Map<String, String> esES = {
'applyToAllParagraphs': 'Aplicar a todos los párrafos de este orador',
'summaryWillBeUpdatedAutomatically':
'El resumen se actualizará automáticamente según la plantilla',
'nameCurrentSpeaker': 'Name Current Speaker',
'recentlyUsedNames': 'Recently Used Names',
'applyToCurrentParagraph': 'Apply to Current Paragraph',
'totalTime': 'Total Time',
'nameCurrentSpeaker': 'Nombrar al orador actual',
'recentlyUsedNames': 'Nombres usados recientemente',
'applyToCurrentParagraph': 'Aplicar al párrafo actual',
'totalTime': 'Tiempo total',
'summarySpeakerWillBeUpdatedAutomatically':
'Summary speakers will be updated automatically',
'Los oradores del resumen se actualizarán automáticamente',
// Meeting模块 - 云端同步
'syncToCloud': 'Sincronizar en la nube',
@ -709,39 +720,40 @@ const Map<String, String> esES = {
'upgradeMembership': 'Actualiza tu membresía',
'yearlySubscription': 'Suscripción anual',
'monthlySubscription': 'Suscripción mensual',
'proMonthly': 'Pro ¥60/month',
'proMonthly': 'Pro ¥60/mes',
'proMonthlyDesc':
'7-day free trial, then ¥800/year. Cancel anytime before trial ends.',
'sevenDayTrial': '7-day Free Trial',
'unlimitedMonthly': 'Unlimited ¥120/month',
'Prueba gratuita de 7 días, luego ¥800/año. Cancela en cualquier momento antes de que finalice la prueba.',
'sevenDayTrial': 'Prueba gratuita de 7 días',
'unlimitedMonthly': 'Ilimitado ¥120/mes',
'unlimitedMonthlyDesc':
'7-day free trial, then ¥1600/year. Cancel anytime before trial ends.',
'trialNote': '*Free trial for 7 days, then charged annually, cancel anytime.',
'Prueba gratuita de 7 días, luego ¥1600/año. Cancela en cualquier momento antes de que finalice la prueba.',
'trialNote':
'*Prueba gratuita durante 7 días; luego se cobra anualmente. Cancela en cualquier momento.',
'proPrice': 'Pro ¥120',
'proMonthlyPrice': '¥120/month',
'proMonthlyPrice': '¥120/mes',
'subscribe': 'Suscribirse',
'unlimitedPrice': 'Unlimited ¥198',
'unlimitedMonthlyPrice': '¥198/month',
'unlimitedPrice': 'Ilimitado ¥198',
'unlimitedMonthlyPrice': '¥198/mes',
'upgradeNote':
'*Tras actualizar la suscripción, los días restantes de la suscripción original se calcularán automáticamente y se reembolsarán a tu cuenta de pago',
'buyMoreTranscriptionTime': 'Comprar más tiempo de transcripción',
'sixThousandMinutes': '6,000 minutes',
'threeThousandMinutes': '3,000 minutes',
'sixHundredMinutes': '600 minutes',
'oneHundredTwentyMinutes': '120 minutes',
'validityPeriod': '*Validity Period: ',
'sixThousandMinutes': '6.000 minutos',
'threeThousandMinutes': '3.000 minutos',
'sixHundredMinutes': '600 minutos',
'oneHundredTwentyMinutes': '120 minutos',
'validityPeriod': '*Período de validez: ',
'validityNote':
'Se activa inmediatamente tras la compra, válido durante 2 años después de la activación.',
'purchase': 'Comprar',
// Meeting模块 - 录音语言
'recordLanguage': 'Idioma de grabación',
'cantonese': 'Cantonese',
'sichuan': 'Sichuan Dialect',
'shanghai': 'Shanghai Dialect',
'cantonese': 'Cantonés',
'sichuan': 'Dialecto de Sichuan',
'shanghai': 'Dialecto de Shanghái',
// Meeting模块 - 错误提示
'meetingUnknownLanguage': 'Unknown Language',
'meetingUnknownLanguage': 'Idioma desconocido',
'analysisFailed': 'Análisis fallido, por favor intente más tarde',
'transcriptionFailed': 'Transcripción fallida, por favor intente más tarde',
'summaryFailed': 'Resumen fallido, por favor intente más tarde',

22
test/es_es_translations_test.dart

@ -0,0 +1,22 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:voitrans/core/translations/language/en_us.dart';
import 'package:voitrans/core/translations/language/es_es.dart';
void main() {
test('esES can be imported and accessed', () {
expect(esES, isNotEmpty);
expect(esES['continue'], isNotNull);
});
test('esES has the same translation keys as enUS', () {
expect(esES.keys.toSet(), equals(enUS.keys.toSet()));
});
test('esES has no empty values', () {
final emptyKeys = esES.entries
.where((entry) => entry.value.trim().isEmpty)
.map((entry) => entry.key)
.toList(growable: false);
expect(emptyKeys, isEmpty);
});
}
Loading…
Cancel
Save