diff --git a/I18N_PLACEHOLDER_REMEDIATION.md b/I18N_PLACEHOLDER_REMEDIATION.md new file mode 100644 index 000000000..0af2dcf24 --- /dev/null +++ b/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/.dart +dart format lib/core/translations/language/.dart +flutter test test/_translations_test.dart +``` + +Recommended additional checks: + +```bash +rg "[‘’]" lib/core/translations/language/.dart +``` + +## Test Convention + +For each locale file, add a `test/_translations_test.dart` test that asserts: +- The map can be imported and accessed. +- Its key set matches `enUS`. +- It contains no empty string values. + diff --git a/lib/core/translations/language/es_es.dart b/lib/core/translations/language/es_es.dart index ea8369768..777d4f997 100644 --- a/lib/core/translations/language/es_es.dart +++ b/lib/core/translations/language/es_es.dart @@ -238,36 +238,36 @@ const Map 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 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 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 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 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 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 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 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', diff --git a/test/es_es_translations_test.dart b/test/es_es_translations_test.dart new file mode 100644 index 000000000..aab8d47ed --- /dev/null +++ b/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); + }); +}