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