Copies for all (#1809)

* initial script

* copies for all!

* revert intl files

* mistaken en to vi translation

* improve translation

* add vi translation, fix trnaalste script to respect existing metdata

* revert translation files

* fix translation to only add more without changing too much existing translations

* revert en, es, and vi for further testing

* remove sorting metakeys

* generated

* build: translations

---------

Co-authored-by: WilsonLe <leanhminh2907@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
pull/1688/head
ggurdin 8 months ago committed by GitHub
parent cf1a420415
commit b7ae77ebd5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

1
.gitignore vendored

@ -75,3 +75,4 @@ scripts/.credentials
olm
needed-translations.txt
.venv

@ -1,6 +1,6 @@
{
"@@locale": "es",
"@@last_modified": "2021-08-14 12:41:10.097243",
"@@last_modified": "2025-03-05 13:07:43.272672",
"about": "Acerca de",
"@about": {
"type": "String",
@ -5155,5 +5155,313 @@
"invitePeopleSpaceSubtitle": "Invitar a usuarios o administradores a este espacio",
"noCapacityLimit": "Sin límite de capacidad",
"downloadGroupText": "Descargar texto del grupo",
"enableAutocorrectWarning": "¡Atención! Es necesario añadir el teclado del idioma de destino"
"enableAutocorrectWarning": "¡Atención! Es necesario añadir el teclado del idioma de destino",
"writeAMessageLangCodes": "Escribe en {l1} o {l2}...",
"@writeAMessageLangCodes": {
"type": "String",
"placeholders": {
"l1": {
"type": "String"
},
"l2": {
"type": "String"
}
}
},
"botConfigNoPermissionTitle": "Sin permiso",
"botConfigNoPermissionMessage": "Contacta al administrador de la sala para cambiar la configuración del bot",
"changeContent": "¡Oh no! La IA puede facilitar experiencias de aprendizaje personalizadas pero... también alucina. ¿Qué debería ser?",
"grammarCopyVERBFORMaux": "Auxiliar",
"levelShort": "NIV {level}",
"@levelShort": {
"type": "String",
"placeholders": {
"level": {
"type": "int"
}
}
},
"spaceDescription": "Descripción del espacio",
"addSpaceDescription": "Agregar una descripción del espacio",
"notificationsOn": "Notificaciones activadas",
"notificationsOff": "Notificaciones desactivadas",
"spaceCanBeFoundViaSearch": "El espacio se puede encontrar a través de la búsqueda",
"chatCanBeFoundViaSearch": "El chat se puede encontrar a través de la búsqueda",
"requireCodeToJoin": "Requiere código para unirse",
"canFindInSearch": "Se puede encontrar en la búsqueda",
"addSubspaceWarning": "Una vez que agregues esto, no aparecerá en los resultados de búsqueda pública, y será visible para todos los miembros del espacio principal.",
"nestedSpaceError": "Los espacios no deben ser añadidos como hijos de otros espacios",
"addChatToSpace": "Agregar chat",
"createChatAndInviteUsers": "Crear chat e invitar usuarios",
"updatedNewSpaceDescription": "Los espacios te permiten consolidar tus chats y construir comunidades privadas o públicas.",
"joinWithCode": "Unirse con código",
"enterCodeToJoin": "Ingrese el código para unirse",
"mandatoryUpdateRequired": "Actualización obligatoria requerida",
"mandatoryUpdateRequiredDesc": "Se requiere una nueva versión de la aplicación para continuar. Actualice ahora para proceder.",
"updateAvailableDesc": "Una nueva versión de la aplicación está disponible. ¡Actualice ahora para obtener las últimas funciones y mejoras!",
"updateLater": "Más tarde",
"constructUseWaDesc": "Usado sin ayuda",
"constructUseGaDesc": "Error gramatical",
"constructUseUnkDesc": "Desconocido",
"constructUseCorITDesc": "Correcto en la traducción",
"constructUseIgnITDesc": "Ignorado en la traducción",
"constructUseIncITDesc": "Incorrecto en la traducción",
"constructUseIgnIGCDesc": "Ignorado en la corrección gramatical",
"constructUseCorIGCDesc": "Correcto en la corrección gramatical",
"constructUseIncIGCDesc": "Incorrecto en la corrección gramatical",
"constructUseCorPADesc": "Correcto en la actividad de significado de palabras",
"constructUseIgnPADesc": "Ignorado en la actividad de significado de palabras",
"constructUseIncPADesc": "Incorrecto en la actividad de significado de palabras",
"constructUseCorWLDesc": "Correcto en la actividad de escucha de palabras",
"constructUseIncWLDesc": "Incorrecto en la actividad de escucha de palabras",
"constructUseIngWLDesc": "Ignorado en la actividad de escucha de palabras",
"constructUseCorHWLDesc": "Correcto en la actividad de palabra oculta",
"constructUseIncHWLDesc": "Incorrecto en la actividad de palabra oculta",
"constructUseIgnHWLDesc": "Ignorado en la actividad de palabra oculta",
"constructUseCorLDesc": "Correcto en la actividad de lema",
"constructUseIncLDesc": "Incorrecto en la actividad de lema",
"constructUseIgnLDesc": "Ignorado en la actividad de lema",
"constructUseCorMDesc": "Correcto en la actividad de gramática",
"constructUseIncMDesc": "Incorrecto en la actividad de gramática",
"constructUseIgnMDesc": "Ignorado en la actividad de gramática",
"constructUseEmojiDesc": "Correcto en la actividad de emoji",
"constructUseNanDesc": "No aplicable",
"xpIntoLevel": "{currentXP} / {maxXP} XP",
"@xpIntoLevel": {
"type": "String",
"placeholders": {
"currentXP": {
"type": "int"
},
"maxXP": {
"type": "int"
}
}
},
"signInWithUsername": "Iniciar sesión con nombre de usuario y contraseña",
"registrationEmailMessage": "Por favor verifica tu correo electrónico con un enlace enviado allí. En algunos casos, el correo puede tardar hasta 5 minutos en llegar. También verifica tu carpeta de spam.",
"enableTTSToolName": "Texto a voz habilitado",
"enableTTSToolDescription": "Permitir que la aplicación genere salida de texto a voz para partes del texto en tu idioma objetivo.",
"couldNotFindTTS": "No pudimos encontrar un motor de texto a voz para tu idioma objetivo actual.",
"ttsInstructionsHyperlink": "Haz clic aquí para ver las instrucciones para descargar una nueva voz en tu dispositivo.",
"currentVersion": "Versión actual",
"latestVersion": "Última versión",
"createAnAccount": "Crear una cuenta",
"signIn": "Iniciar sesión",
"signUpWithEmail": "Regístrate con Email",
"signUpWithGoogle": "Regístrate con Google",
"signUpWithApple": "Regístrate con Apple",
"yourUsername": "Tu nombre de usuario",
"yourEmail": "Tu correo electrónico",
"pleaseEnterAnEmail": "Por favor, introduce una dirección de correo electrónico",
"signInWithGoogle": "Iniciar sesión con Google",
"signInWithApple": "Iniciar sesión con Apple",
"chooseYourAvatar": "Elige tu avatar",
"iWantToLearn": "Quiero aprender",
"pleaseAgreeToTOS": "Por favor, acepta los Términos y Condiciones",
"pleaseEnterEmail": "Por favor, introduce una dirección de correo electrónico válida.",
"pleaseSelectALanguage": "Por favor, selecciona un idioma",
"myBaseLanguage": "Mi idioma base",
"clickWordsInstructions": "Haz clic en una palabra o en los botones de abajo para aprender más",
"chooseBestDefinition": "¿Qué significa esta palabra?",
"meaningSectionHeader": "Significado:",
"formSectionHeader": "Formas utilizadas en los chats:",
"noEmojiSelectedTooltip": "No se ha seleccionado ningún emoji",
"writingExercisesTooltip": "Actividades de escritura",
"listeningExercisesTooltip": "Actividades de escucha",
"readingExercisesTooltip": "Actividades de lectura",
"meaningNotFound": "No se pudo encontrar el significado.",
"formsNotFound": "No se pudieron encontrar las formas.",
"chooseBaseForm": "Elige la forma base",
"notTheCodeError": "¡Lo siento, ese no es el código!",
"totalXP": "XP total",
"numLemmas": "Número total de lemas",
"listOfLemmas": "Lista de lemas",
"numLemmasUsedCorrectly": "Número de lemas utilizados correctamente al menos una vez",
"listLemmasUsedCorrectly": "Lista de lemas utilizados correctamente al menos una vez",
"numLemmasUsedIncorrectly": "Número de lemas utilizados correctamente 0 veces",
"listLemmasUsedIncorrectly": "Número de lemas utilizados correctamente 0 veces",
"numLemmasSmallXP": "Número de lemas con 0 - 30 XP",
"numLemmasMediumXP": "Número de lemas con 31 - 200 XP",
"numLemmasLargeXP": "Número de lemas con > 200 XP",
"listLemmasSmallXP": "Lista de lemas con 0 - 30 XP",
"listLemmasMediumXP": "Lista de lemas con 31 - 200 XP",
"listLemmasLargeXP": "Lista de lemas con > 200 XP",
"numGrammarConcepts": "Número de conceptos gramaticales",
"listGrammarConcepts": "Conceptos gramaticales",
"listGrammarConceptsUsedCorrectly": "Conceptos gramaticales utilizados correctamente en mensajes originales al menos el 80% del tiempo",
"listGrammarConceptsUsedIncorrectly": "Conceptos gramaticales utilizados correctamente en mensajes originales menos del 80% del tiempo",
"listGrammarConceptsUseCorrectlySystemGenerated": "Conceptos gramaticales elegidos correctamente de sugerencias generadas por el sistema al menos el 80% del tiempo",
"listGrammarConceptsUseIncorrectlySystemGenerated": "Conceptos gramaticales elegidos correctamente de sugerencias generadas por el sistema menos del 80% del tiempo",
"listGrammarConceptsSmallXP": "Conceptos gramaticales con 0-50 xp",
"listGrammarConceptsMediumXP": "Conceptos gramaticales con 51-200 xp",
"listGrammarConceptsLargeXP": "Conceptos gramaticales 201-500 xp",
"listGrammarConceptsHugeXP": "Conceptos gramaticales >500 xp",
"numMessagesSent": "Número de mensajes enviados",
"numWordsTyped": "Número de palabras escritas en mensajes originales",
"numCorrectChoices": "Número de palabras correctas elegidas de sugerencias generadas por el sistema",
"numIncorrectChoices": "Número de palabras incorrectas elegidas de sugerencias generadas por el sistema",
"downloadSpaceAnalytics": "Descargar análisis de espacio",
"commaSeparatedFile": "CSV",
"excelFile": "Excel",
"fileType": "Tipo de archivo",
"download": "Descargar",
"analyticsNotAvailable": "Análisis de usuarios no disponible",
"downloading": "Descargando...",
"failedFetchUserAnalytics": "Error al descargar el análisis de usuarios",
"downloadComplete": "¡Descarga completa!",
"whatIsTheMorphTag": "¿Cuál es la {morphologicalFeature} de '{wordForm}'?",
"@whatIsTheMorphTag": {
"type": "String",
"placeholders": {
"morphologicalFeature": {
"type": "String"
},
"wordForm": {
"type": "String"
}
}
},
"dataAvailable": "Disponibilidad de datos",
"lemmasNeverUsedCorrectly": "Número de lemas utilizados correctamente 0 veces",
"available": "Disponible",
"accessingMemberAnalytics": "Accediendo al análisis de miembros...",
"pangeaBotIsFallible": "¡El Pangea Bot también comete errores!",
"whatIsMeaning": "¿Qué significa '{lemma}'?",
"@whatIsMeaning": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
},
"pickAnEmoji": "¿Cuál es tu emoji favorito para '{lemma}'?",
"@pickAnEmoji": {
"type": "String",
"placeholders": {
"lemma": {
"type": "String"
}
}
},
"lemmaMeaningInstructionsBody": "Arriba está el significado del lema. Haz doble clic para editar.",
"doubleClickToEdit": "Haz doble clic para editar.",
"removeFeature": "Eliminar {feature}",
"@removeFeature": {
"type": "String",
"placeholders": {
"feature": {
"type": "String"
}
}
},
"notInClass": "¡No está en una clase!",
"noClassCode": "¡Sin código de clase!",
"chooseCorrectLabel": "Elige la etiqueta correcta.",
"levelPopupTitle": "¡Felicidades por alcanzar\nel Nivel {level}!",
"@levelPopupTitle": {
"type": "String",
"placeholders": {
"level": {
"type": "int"
}
}
},
"activityPlannerTitle": "Planificador de Actividades",
"topicLabel": "Tema",
"topicPlaceholder": "Elige un tema...",
"modeLabel": "Modo",
"modePlaceholder": "Elige un modo...",
"learningObjectiveLabel": "Objetivo de Aprendizaje",
"learningObjectivePlaceholder": "Elige un objetivo de aprendizaje...",
"mediaLabel": "Medios que los aprendices deben compartir",
"languageOfInstructionsLabel": "Idioma de las instrucciones de la actividad",
"targetLanguageLabel": "Idioma objetivo",
"cefrLevelLabel": "Nivel CEFR",
"generateActivitiesButton": "Generar Actividades",
"launchActivityButton": "Iniciar Actividad",
"image": "Imagen",
"video": "Video",
"nan": "No aplicable",
"activityPlannerOverviewInstructionsBody": "¡Elige un tema, modo, objetivo de aprendizaje y genera una actividad para el chat!",
"completeActivitiesToUnlock": "Completa las actividades de palabras resaltadas para desbloquear",
"myBookmarkedActivities": "Mis Actividades Marcadas",
"noBookmarkedActivities": "No hay actividades marcadas",
"activityTitle": "Título de la Actividad",
"addVocabulary": "Agregar vocabulario",
"instructions": "Instrucciones",
"bookmark": "Marcar esta actividad",
"numberOfLearners": "Número de aprendices",
"mustBeInteger": "Debe ser un número entero, por ejemplo, 1, 2, 3, ...",
"noLemmasFound": "No hay vocabulario con más de {xp} XP. ¡Sigue practicando!",
"@noLemmasFound": {
"type": "String",
"placeholders": {
"xp": {
"type": "int"
}
}
},
"constructUsePvmDesc": "Producido en mensaje de voz",
"lockedMorphFeature": "Esperando ser desbloqueado",
"leaveSpaceDescription": "Al salir del espacio, dejarás todos los chats dentro de él. Otros usuarios verán que has salido del espacio.",
"whatIsLemma": "¿Qué es el lema?",
"constructUseCorMmDesc": "Significado correcto del mensaje",
"constructUseIncMmDesc": "Significado incorrecto del mensaje",
"constructUseIgnMmDesc": "Significado del mensaje ignorado",
"clickForMeaningActivity": "Haz clic aquí para un Desafío de Significado",
"meaning": "Significado",
"chatWith": "Grupo con {displayname}",
"@chatWith": {
"type": "String",
"placeholders": {
"displayname": {
"type": "String"
}
}
},
"slightlyOffensive": "Ligeramente ofensivo",
"clickOnEmailLink": "Por favor, haz clic en el enlace del correo electrónico y luego procede. En raras ocasiones, el correo electrónico puede ser enviado a spam o tardar hasta 5 minutos en llegar.",
"whoIsAllowedToJoinThisChat": "Quién está permitido unirse a este chat",
"dontForgetPassword": "¡No olvides tu contraseña!",
"enableAutocorrectToolName": "Habilitar autocorrección del dispositivo",
"enableAutocorrectDescription": "Si tu dispositivo soporta el idioma que estás aprendiendo, puedes habilitar la autocorrección del dispositivo para corregir errores comunes mientras escribes.",
"ttsDisbledTitle": "Texto a voz deshabilitado",
"ttsDisabledBody": "Puedes habilitar texto a voz en la configuración de aprendizaje",
"noSpaceDescriptionYet": "No se ha creado ninguna descripción de espacio aún.",
"tooLargeToSend": "Este mensaje es demasiado grande para enviar",
"exitWithoutSaving": "¿Estás seguro de que quieres salir sin guardar?",
"enableAutocorrectPopupTitle": "Agrega el teclado de tu idioma objetivo yendo a:",
"enableAutocorrectPopupSteps": " • Configuración\n • General\n • Teclado\n • Teclados\n • Agregar nuevo teclado",
"enableAutocorrectPopupDescription": "Una vez que se seleccione el idioma, puedes hacer clic en el pequeño ícono de globo en la esquina inferior izquierda de tu teclado para activar el teclado recién instalado.",
"downloadGboardTitle": "Descarga Gboard desde Google Play Store para habilitar la autocorrección y otras funciones del teclado:",
"downloadGboardSteps": " • Descargar Gboard\n • Abrir la aplicación\n • Idiomas\n • Agregar teclado\n • Seleccionar idioma\n • Seleccionar tipo de teclado\n • Listo",
"downloadGboardDescription": "Una vez que se seleccione el idioma, puedes hacer clic en el pequeño ícono de globo en la esquina inferior izquierda de tu teclado para activar el teclado recién instalado.",
"displayName": "Nombre para mostrar",
"leaveRoomDescription": "Estás a punto de salir de este chat. Otros usuarios verán que has salido del chat.",
"confirmUserId": "Por favor, confirma tu nombre de usuario de Pangea Chat para poder eliminar tu cuenta.",
"startingToday": "A partir de hoy",
"oneWeekFreeTrial": "Una semana de prueba gratuita",
"paidSubscriptionStarts": "Comenzando {startDate}",
"@paidSubscriptionStarts": {
"type": "String",
"placeholders": {
"startDate": {
"type": "String"
}
}
},
"cancelInSubscriptionSettings": "• Cancela en cualquier momento en la configuración de suscripción",
"cancelToAvoidCharges": "• Cancela antes de {trialEnds} para evitar cargos",
"@cancelToAvoidCharges": {
"type": "String",
"placeholders": {
"trialEnds": {
"type": "String"
}
}
},
"downloadGboard": "Descargar Gboard",
"autocorrectNotAvailable": "Desafortunadamente, tu plataforma no es compatible actualmente con esta función. ¡Mantente atento a futuros desarrollos!"
}

@ -1,5 +1,5 @@
{
"@@last_modified": "2025-02-27 09:51:45.710355",
"@@last_modified": "2025-03-05 13:01:52.197829",
"about": "Giới thiệu",
"@about": {
"type": "String",
@ -3709,5 +3709,42 @@
"enableAutocorrectToolName": "Bật tự động sửa",
"enableAutocorrectDescription": "Sử dụng tính năng tự động sửa tích hợp của bàn phím khi gõ tin nhắn",
"ttsDisbledTitle": "Tính năng chuyển văn bản thành giọng nói đã bị tắt",
"ttsDisabledBody": "Bạn có thể bật tính năng chuyển văn bản thành giọng nói trong cài đặt học tập của bạn"
"ttsDisabledBody": "Bạn có thể bật tính năng chuyển văn bản thành giọng nói trong cài đặt học tập của bạn",
"leaveSpaceDescription": "Bằng cách rời khỏi không gian, bạn sẽ rời tất cả các cuộc trò chuyện bên trong nó. Những người dùng khác sẽ thấy rằng bạn đã rời khỏi không gian.",
"noSpaceDescriptionYet": "Chưa tạo mô tả không gian nào.",
"tooLargeToSend": "Tin nhắn này quá lớn để gửi",
"exitWithoutSaving": "Bạn có chắc chắn muốn rời đi mà không lưu không?",
"enableAutocorrectPopupTitle": "Thêm bàn phím ngôn ngữ mục tiêu của bạn bằng cách vào:",
"enableAutocorrectPopupSteps": " • Cài đặt\n • Chung\n • Bàn phím\n • Bàn phím\n • Thêm bàn phím mới",
"enableAutocorrectPopupDescription": "Khi ngôn ngữ được chọn, bạn có thể nhấp vào biểu tượng quả địa cầu nhỏ ở góc dưới bên trái của bàn phím để kích hoạt bàn phím mới được cài đặt.",
"downloadGboardTitle": "Tải Gboard từ Google Play Store để kích hoạt tính năng tự động sửa và các tính năng bàn phím khác:",
"downloadGboardSteps": " • Tải Gboard\n • Mở ứng dụng\n • Ngôn ngữ\n • Thêm bàn phím\n • Chọn ngôn ngữ\n • Chọn loại bàn phím\n • Xong",
"downloadGboardDescription": "Khi ngôn ngữ được chọn, bạn có thể nhấp vào biểu tượng quả địa cầu nhỏ ở góc dưới bên trái của bàn phím để kích hoạt bàn phím mới được cài đặt.",
"enableAutocorrectWarning": "Cảnh báo! Cần thêm bàn phím ngôn ngữ mục tiêu của bạn",
"displayName": "Tên hiển thị",
"leaveRoomDescription": "Bạn sắp rời khỏi cuộc trò chuyện này. Những người dùng khác sẽ thấy rằng bạn đã rời khỏi cuộc trò chuyện.",
"confirmUserId": "Vui lòng xác nhận tên người dùng Pangea Chat của bạn để xóa tài khoản.",
"startingToday": "Bắt đầu từ hôm nay",
"oneWeekFreeTrial": "Thử nghiệm miễn phí một tuần",
"paidSubscriptionStarts": "Bắt đầu từ {startDate}",
"@paidSubscriptionStarts": {
"type": "String",
"placeholders": {
"startDate": {
"type": "String"
}
}
},
"cancelInSubscriptionSettings": "• Hủy bất cứ lúc nào trong cài đặt đăng ký",
"cancelToAvoidCharges": "• Hủy trước {trialEnds} để tránh bị tính phí",
"@cancelToAvoidCharges": {
"type": "String",
"placeholders": {
"trialEnds": {
"type": "String"
}
}
},
"downloadGboard": "Tải Gboard",
"autocorrectNotAvailable": "Rất tiếc, nền tảng của bạn hiện không được hỗ trợ cho tính năng này. Hãy theo dõi để biết thêm thông tin phát triển!"
}

@ -0,0 +1,276 @@
"""
Prerequiresite:
- Ensure you have an up-to-date `needed-translations.txt` file should you wish to translate only the missing translation keys. To generate an updated `needed-translations.txt` file, run `flutter gen-l10n`
- Ensure you have python `openai` package installed. If not, run `pip install openai`.
- Ensure you have an OpenAI API key set in your environment variable `OPENAI_API_KEY`. If not, you can set it by running `export OPENAI_API_KEY=your-api-key` on MacOS/Linux.
Usage:
python scripts/translate.py
"""
def load_needed_translations() -> dict[str, list[str]]:
import json
from pathlib import Path
path_to_needed_translations = (
Path(__file__).parent.parent / "needed-translations.txt"
)
if not path_to_needed_translations.exists():
raise FileNotFoundError(
f"File not found: {path_to_needed_translations}. Please run `flutter gen-l10n` to generate the file."
)
with open(path_to_needed_translations) as f:
needed_translations = json.loads(f.read())
return needed_translations
def load_translations(lang_code: str) -> dict[str, str]:
import json
from pathlib import Path
path_to_translations = (
Path(__file__).parent.parent / "assets" / "l10n" / f"intl_{lang_code}.arb"
)
if not path_to_translations.exists():
raise FileNotFoundError(
f"File not found: {path_to_translations}. Please run `flutter gen-l10n` to generate the file."
)
with open(path_to_translations) as f:
translations = json.loads(f.read())
return translations
def save_translations(lang_code: str, translations: dict[str, str]) -> None:
import json
from collections import OrderedDict
from datetime import datetime
from pathlib import Path
path_to_translations = (
Path(__file__).parent.parent / "assets" / "l10n" / f"intl_{lang_code}.arb"
)
translations["@@locale"] = lang_code
translations["@@last_modified"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")
# Load existing data to preserve order if exists.
if path_to_translations.exists():
with open(path_to_translations, "r") as f:
try:
existing_data = json.load(f, object_pairs_hook=OrderedDict)
except json.JSONDecodeError:
existing_data = OrderedDict()
else:
existing_data = OrderedDict()
# Update existing keys and append new keys (preserving existing order).
for key, value in translations.items():
if key in existing_data:
existing_data[key] = value # update value; order remains unchanged
else:
existing_data[key] = value # new key appended at the end
with open(path_to_translations, "w") as f:
f.write(json.dumps(existing_data, indent=2, ensure_ascii=False))
def reconcile_metadata(lang_code: str, translation_keys: list[str]) -> None:
"""
For each translation key, update its metadata (the key prefixed with '@') by merging
any existing metadata with computed metadata. For basic translations, if no metadata exists,
add it; otherwise, leave it as is.
"""
translations = load_translations(lang_code)
for key in translation_keys:
translation = translations[key]
meta_key = f"@{key}"
existing_meta = translations.get(meta_key, {})
assert isinstance(translation, str)
# Case 1: Basic translations, no placeholders.
if "{" not in translation:
if not existing_meta:
translations[meta_key] = {"type": "text", "placeholders": {}}
# if metadata exists, leave it as is.
# Case 2: Translations with placeholders (no pluralization).
elif (
"{" in translation
and "plural," not in translation
and "other{" not in translation
):
# Compute placeholders.
computed_placeholders = {}
for placeholder in translation.split("{")[1:]:
placeholder_name = placeholder.split("}")[0]
computed_placeholders[placeholder_name] = {}
if existing_meta:
# Merge computed placeholders into existing metadata.
existing_meta.setdefault("type", "text")
existing_meta["placeholders"] = computed_placeholders
translations[meta_key] = existing_meta
else:
translations[meta_key] = {
"type": "text",
"placeholders": computed_placeholders,
}
# Case 3: Translations with pluralization.
elif (
"{" in translation and "plural," in translation and "other{" in translation
):
# Extract placeholders appearing before the plural part.
prefix = translation.split("plural,")[0].split("{")[1]
placeholders_list = [
p.strip() for p in prefix.split(",") if p.strip() != ""
]
computed_placeholders = {ph: {} for ph in placeholders_list}
if existing_meta:
existing_meta.setdefault("type", "text")
existing_meta["placeholders"] = computed_placeholders
translations[meta_key] = existing_meta
else:
translations[meta_key] = {
"type": "text",
"placeholders": computed_placeholders,
}
save_translations(lang_code, translations)
def translate(lang_code: str, lang_display_name: str) -> None:
"""
Translate the needed translations from English to the target language.
"""
import json
import random
from openai import OpenAI
needed_translations = load_needed_translations()
needed_translations = needed_translations.get(lang_code, [])
english_translations_dict = load_translations("en")
vietnamese_translations_dict = load_translations("vi")
# there are 3 types of translation keys: basic, with placeholders, with pluralization. Read more: TRANSLATORS_GUIDE.md
basic_translation_keys = [
k
for k in english_translations_dict.keys()
if not k.startswith("@") and not english_translations_dict[k].startswith("{")
]
example_basic_translation_keys = (
random.sample(basic_translation_keys, 2)
if len(basic_translation_keys) > 2
else basic_translation_keys
)
placeholder_translation_keys = [
k
for k in english_translations_dict.keys()
if not k.startswith("@")
and "{" in english_translations_dict[k]
and "plural," not in english_translations_dict[k]
and "other{" not in english_translations_dict[k]
]
example_placeholder_translation_keys = (
random.sample(placeholder_translation_keys, 2)
if len(placeholder_translation_keys) > 2
else placeholder_translation_keys
)
plural_translation_keys = [
k
for k in english_translations_dict.keys()
if not k.startswith("@")
and "{" in english_translations_dict[k]
and "plural," in english_translations_dict[k]
and "other{" in english_translations_dict[k]
]
example_plural_translation_keys = (
random.sample(plural_translation_keys, 2)
if len(plural_translation_keys) > 2
else plural_translation_keys
)
# build example translations
example_english_translations = {}
for key in example_basic_translation_keys:
example_english_translations[key] = english_translations_dict[key]
for key in example_placeholder_translation_keys:
example_english_translations[key] = english_translations_dict[key]
for key in example_plural_translation_keys:
example_english_translations[key] = english_translations_dict[key]
example_vietnamese_translations = {}
for key in example_basic_translation_keys:
example_vietnamese_translations[key] = vietnamese_translations_dict[key]
for key in example_placeholder_translation_keys:
example_vietnamese_translations[key] = vietnamese_translations_dict[key]
for key in example_plural_translation_keys:
example_vietnamese_translations[key] = vietnamese_translations_dict[key]
new_translations = {}
progress = 0
for i in range(0, len(needed_translations), 20):
chunk = needed_translations[i : i + 20]
translation_requests = {}
for key in chunk:
translation_requests[key] = english_translations_dict[key]
prompt = f"""
Please translate the following text from English to {lang_display_name}.
Example:
req: {json.dumps(example_english_translations, indent=2)}
res: {json.dumps(example_vietnamese_translations, indent=2)}
========================
req: {json.dumps(translation_requests, indent=2)}
res:
"""
client = OpenAI()
chat_completion = client.chat.completions.create(
messages=[
{
"role": "system",
"content": "You are a translator that will only response to translation requests in json format without any additional information.",
},
{
"role": "user",
"content": prompt,
},
],
model="gpt-4o-mini",
temperature=0.0,
)
response = chat_completion.choices[0].message.content
_new_translations = json.loads(response)
new_translations.update(_new_translations)
print(f"Translated {progress + len(chunk)}/{len(needed_translations)}")
progress += len(chunk)
# save translations
current_translations = load_translations(lang_code)
current_translations.update(new_translations)
save_translations(lang_code, current_translations)
# reconcile metadata
reconcile_metadata(lang_code, needed_translations)
"""Example usage:
python scripts/translate.py
"""
if __name__ == "__main__":
lang_code = input("Enter the language code (e.g. vi, en): ").strip()
lang_display_name = input(
"Enter the language display name (e.g. Vietnamese, English): "
)
translate(
lang_code=lang_code,
lang_display_name=lang_display_name,
)
Loading…
Cancel
Save