chore: Improve translation script and translate vi, es, et (#3555)

* improve translation script

* update translation script, more translates for es, et, vi
This commit is contained in:
Wilson 2025-09-18 23:22:02 +10:00 committed by GitHub
parent c1c4152633
commit 5d28cea789
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 9592 additions and 39 deletions

View file

@ -1,6 +1,6 @@
{
"@@locale": "es",
"@@last_modified": "2025-05-23 14:51:23.780564",
"@@last_modified": "2025-07-24 13:19:34.706481",
"about": "Acerca de",
"@about": {
"type": "String",
@ -5917,5 +5917,416 @@
"@announcements": {
"type": "String",
"placeholders": {}
},
"customReaction": "Reacción personalizada",
"chatCapacitySetTooLow": "La capacidad de chat debe ser al menos {count}.",
"spaceCapacitySetTooLow": "La capacidad del espacio debe ser al menos {count}.",
"grammarCopyVERBFORMshrt": "Corto",
"constructUseCollected": "Recopilado en chat",
"modeLabel": "Tipo de actividad",
"makeYourOwnActivity": "Crea tu propia actividad",
"deleteChatDesc": "¿Estás seguro de que deseas eliminar este chat? Se eliminará para todos los participantes y todos los mensajes dentro del chat ya no estarán disponibles para práctica o análisis de aprendizaje.",
"deleteSpaceDesc": "El espacio y cualquier chat y/o subespacio seleccionado se eliminarán para todos los participantes y todos los mensajes dentro del chat ya no estarán disponibles para práctica o análisis de aprendizaje. Esta acción no se puede deshacer.",
"maxFifty": "Máximo 50",
"activities": "Actividades",
"access": "Acceso",
"addSubspace": "Agregar subespacio",
"botSettings": "Configuración del bot",
"activitySuggestionTimeoutMessage": "Estamos trabajando arduamente para generar actividades para ti, por favor vuelve en un minuto",
"accessSettingsWarning": "¡Ups! Parece que no tienes permiso para configurar las reglas de acceso de esta sala. Debes revisarlas para asegurarte de que sean lo que necesitas y hablar con un administrador de la sala si necesitas cambiarlas",
"howSpaceCanBeFound": "Cómo se puede encontrar este espacio",
"private": "Privado",
"cannotBeFoundInSearch": "No se puede encontrar en la búsqueda",
"public": "Público",
"visibleToCommunity": "Visible para la comunidad más amplia de Pangea Chat a través de \"Encuentra a tu gente\"",
"howSpaceCanBeJoined": "Cómo se puede unirse a este espacio",
"canBeFoundVia": "Se puede encontrar a través de:",
"canBeFoundViaInvitation": "• invitación",
"canBeFoundViaCodeOrLink": "• código o enlace",
"canBeFoundViaKnock": "• solicitud para unirse y aprobación del administrador",
"createYourSpace": "Crea tu espacio",
"youHaveLeveledUp": "¡Has subido de nivel!",
"sendActivities": "Enviar actividades",
"getStartedBotChatDesc": "¡Chatear con IA es un excelente lugar para comenzar y las herramientas de lectura, escritura, escucha y habla de Pangea facilitan todo!",
"getStartedCommunitiesDesc": "¡Aprender con una comunidad es donde brilla Pangea Chat!\nPuedes unirte a tu clase, encontrar una escuela o incluso crear la tuya propia.",
"getStartedFriendsDesc": "¿Tienes un amigo que quiere aprender contigo?",
"getStartedBotChatComplete": "¡Bien hecho! Estás chateando con el bot!",
"getStartedCommunitiesComplete": "¡Genial, te has unido a un espacio!",
"getStartedComplete": "¡Has completado esta sección!\nSigue explorando nuestras increíbles funciones chateando con amigos.",
"getStartedFriendsComplete": "¡Woohoo! ¡Tienes amigos! 😉",
"getStartedBotChatButton": "¡Comienza a chatear!",
"getStartedFriendsButton": "Chatear con un amigo",
"groupChat": "Chat grupal",
"directMessage": "Mensaje directo",
"newDirectMessage": "Nuevo mensaje directo",
"speakingExercisesTooltip": "Hablar",
"noChatsFoundHereYet": "Aún no se han encontrado chats aquí",
"duration": "Duración",
"transcriptionFailed": "Error al transcribir el audio",
"aUserIsKnocking": "Un usuario está solicitando unirse a tu espacio",
"usersAreKnocking": "{users} usuarios están solicitando unirse a tu espacio",
"failedToFetchTranscription": "Error al obtener la transcripción",
"deleteEmptySpaceDesc": "El espacio será eliminado para todos los participantes. Esta acción no se puede deshacer.",
"regenerate": "Regenerar",
"mySavedActivities": "Mis actividades guardadas",
"noSavedActivities": "No hay actividades guardadas",
"saveActivity": "Guardar esta actividad",
"yourSavedActivities": "Actividades guardadas",
"failedToPlayVideo": "Error al reproducir el video",
"done": "Hecho",
"inThisSpace": "En este espacio",
"myContacts": "Mis contactos",
"inviteAllInSpace": "Invitar a todos en este espacio",
"spaceParticipantsHaveBeenInvitedToTheChat": "Todos los participantes del espacio han sido invitados al chat",
"numKnocking": "{count} tocando",
"numInvited": "{count} invitado",
"saved": "Guardado",
"reset": "Restablecer",
"errorGenerateActivityMessage": "Error al generar la actividad",
"errorRegenerateActivityMessage": "Error al regenerar la actividad",
"errorFetchingActivitiesMessage": "Error al obtener actividades",
"errorFetchingDefinition": "Error al obtener la definición",
"errorProcessAnalytics": "Error al procesar análisis",
"errorDownloading": "Error en la descarga",
"errorFetchingLevelSummary": "Error al obtener el resumen del nivel",
"errorLoadingSpaceChildren": "Error al cargar chats dentro de este espacio",
"unexpectedError": "Error inesperado.",
"pleaseReload": "Por favor, recarga e intenta de nuevo.",
"translationError": "Error de traducción",
"errorFetchingTranslation": "Error al obtener la traducción",
"errorFetchingActivity": "Error al obtener la actividad",
"check": "Verificar",
"unableToFindRoom": "No se encontró chat o espacio con ese código. Por favor, intenta de nuevo.",
"@customReaction": {
"type": "String",
"placeholders": {}
},
"@chatCapacitySetTooLow": {
"type": "int",
"placeholders": {
"count": {
"type": "int"
}
}
},
"@spaceCapacitySetTooLow": {
"type": "int",
"placeholders": {
"count": {
"type": "int"
}
}
},
"@grammarCopyVERBFORMshrt": {
"type": "String",
"placeholders": {}
},
"@constructUseCollected": {
"type": "String",
"placeholders": {}
},
"@modeLabel": {
"type": "String",
"placeholders": {}
},
"@deleteChatDesc": {
"type": "String",
"placeholders": {}
},
"@deleteSpaceDesc": {
"type": "String",
"placeholders": {}
},
"@maxFifty": {
"type": "String",
"placeholders": {}
},
"@activities": {
"type": "String",
"placeholders": {}
},
"@access": {
"type": "String",
"placeholders": {}
},
"@addSubspace": {
"type": "String",
"placeholders": {}
},
"@botSettings": {
"type": "String",
"placeholders": {}
},
"@activitySuggestionTimeoutMessage": {
"type": "String",
"placeholders": {}
},
"@accessSettingsWarning": {
"type": "String",
"placeholders": {}
},
"@howSpaceCanBeFound": {
"type": "String",
"placeholders": {}
},
"@private": {
"type": "String",
"placeholders": {}
},
"@cannotBeFoundInSearch": {
"type": "String",
"placeholders": {}
},
"@public": {
"type": "String",
"placeholders": {}
},
"@visibleToCommunity": {
"type": "String",
"placeholders": {}
},
"@howSpaceCanBeJoined": {
"type": "String",
"placeholders": {}
},
"@canBeFoundVia": {
"type": "String",
"placeholders": {}
},
"@canBeFoundViaInvitation": {
"type": "String",
"placeholders": {}
},
"@canBeFoundViaCodeOrLink": {
"type": "String",
"placeholders": {}
},
"@canBeFoundViaKnock": {
"type": "String",
"placeholders": {}
},
"@createYourSpace": {
"type": "String",
"placeholders": {}
},
"@youHaveLeveledUp": {
"type": "String",
"placeholders": {}
},
"@sendActivities": {
"type": "String",
"placeholders": {}
},
"@getStartedBotChatDesc": {
"type": "String",
"placeholders": {}
},
"@getStartedCommunitiesDesc": {
"type": "String",
"placeholders": {}
},
"@getStartedFriendsDesc": {
"type": "String",
"placeholders": {}
},
"@getStartedBotChatComplete": {
"type": "String",
"placeholders": {}
},
"@getStartedCommunitiesComplete": {
"type": "String",
"placeholders": {}
},
"@getStartedComplete": {
"type": "String",
"placeholders": {}
},
"@getStartedFriendsComplete": {
"type": "String",
"placeholders": {}
},
"@getStartedBotChatButton": {
"type": "String",
"placeholders": {}
},
"@getStartedFriendsButton": {
"type": "String",
"placeholders": {}
},
"@groupChat": {
"type": "String",
"placeholders": {}
},
"@directMessage": {
"type": "String",
"placeholders": {}
},
"@newDirectMessage": {
"type": "String",
"placeholders": {}
},
"@speakingExercisesTooltip": {
"type": "String",
"placeholders": {}
},
"@noChatsFoundHereYet": {
"type": "String",
"placeholders": {}
},
"@duration": {
"type": "String",
"placeholders": {}
},
"@transcriptionFailed": {
"type": "String",
"placeholders": {}
},
"@aUserIsKnocking": {
"type": "String",
"placeholders": {}
},
"@usersAreKnocking": {
"type": "int",
"placeholders": {
"users": {
"type": "int"
}
}
},
"@failedToFetchTranscription": {
"type": "String",
"placeholders": {}
},
"@deleteEmptySpaceDesc": {
"type": "String",
"placeholders": {}
},
"@regenerate": {
"type": "String",
"placeholders": {}
},
"@mySavedActivities": {
"type": "String",
"placeholders": {}
},
"@noSavedActivities": {
"type": "String",
"placeholders": {}
},
"@saveActivity": {
"type": "String",
"placeholders": {}
},
"@yourSavedActivities": {
"type": "String",
"placeholders": {}
},
"@failedToPlayVideo": {
"type": "String",
"placeholders": {}
},
"@done": {
"type": "String",
"placeholders": {}
},
"@inThisSpace": {
"type": "String",
"placeholders": {}
},
"@myContacts": {
"type": "String",
"placeholders": {}
},
"@inviteAllInSpace": {
"type": "String",
"placeholders": {}
},
"@spaceParticipantsHaveBeenInvitedToTheChat": {
"type": "String",
"placeholders": {}
},
"@numKnocking": {
"type": "String",
"placeholders": {
"count": {
"type": "int"
}
}
},
"@numInvited": {
"type": "String",
"placeholders": {
"count": {
"type": "int"
}
}
},
"@saved": {
"type": "String",
"placeholders": {}
},
"@reset": {
"type": "String",
"placeholders": {}
},
"@errorGenerateActivityMessage": {
"type": "String",
"placeholders": {}
},
"@errorRegenerateActivityMessage": {
"type": "String",
"placeholders": {}
},
"@errorFetchingActivitiesMessage": {
"type": "String",
"placeholders": {}
},
"@errorFetchingDefinition": {
"type": "String",
"placeholders": {}
},
"@errorProcessAnalytics": {
"type": "String",
"placeholders": {}
},
"@errorDownloading": {
"type": "String",
"placeholders": {}
},
"@errorFetchingLevelSummary": {
"type": "String",
"placeholders": {}
},
"@errorLoadingSpaceChildren": {
"type": "String",
"placeholders": {}
},
"@unexpectedError": {
"type": "String",
"placeholders": {}
},
"@pleaseReload": {
"type": "String",
"placeholders": {}
},
"@translationError": {
"type": "String",
"placeholders": {}
},
"@errorFetchingTranslation": {
"type": "String",
"placeholders": {}
},
"@errorFetchingActivity": {
"type": "String",
"placeholders": {}
},
"@check": {
"type": "String",
"placeholders": {}
},
"@unableToFindRoom": {
"type": "String",
"placeholders": {}
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2025-07-10 15:55:50.516947",
"@@last_modified": "2025-07-24 12:57:05.290437",
"about": "Giới thiệu",
"@about": {
"type": "String",
@ -4501,5 +4501,103 @@
"@saved": {
"type": "String",
"placeholders": {}
},
"chatCapacitySetTooLow": "Dung lượng trò chuyện phải ít nhất là {count}.",
"spaceCapacitySetTooLow": "Dung lượng không gian phải ít nhất là {count}.",
"reset": "Đặt lại",
"errorGenerateActivityMessage": "Không thể tạo hoạt động",
"errorRegenerateActivityMessage": "Không thể tái tạo hoạt động",
"errorFetchingActivitiesMessage": "Không thể lấy hoạt động",
"errorFetchingDefinition": "Không thể lấy định nghĩa",
"errorProcessAnalytics": "Không thể xử lý phân tích",
"errorDownloading": "Tải xuống thất bại",
"errorFetchingLevelSummary": "Không thể lấy tóm tắt cấp độ",
"errorLoadingSpaceChildren": "Không thể tải trò chuyện trong không gian này",
"unexpectedError": "Lỗi không mong đợi.",
"pleaseReload": "Vui lòng tải lại và thử lại.",
"translationError": "Lỗi dịch thuật",
"errorFetchingTranslation": "Không thể lấy bản dịch",
"errorFetchingActivity": "Không thể lấy hoạt động",
"check": "Kiểm tra",
"unableToFindRoom": "Không tìm thấy trò chuyện hoặc không gian nào với mã đó. Vui lòng thử lại.",
"@chatCapacitySetTooLow": {
"type": "int",
"placeholders": {
"count": {
"type": "int"
}
}
},
"@spaceCapacitySetTooLow": {
"type": "int",
"placeholders": {
"count": {
"type": "int"
}
}
},
"@reset": {
"type": "String",
"placeholders": {}
},
"@errorGenerateActivityMessage": {
"type": "String",
"placeholders": {}
},
"@errorRegenerateActivityMessage": {
"type": "String",
"placeholders": {}
},
"@errorFetchingActivitiesMessage": {
"type": "String",
"placeholders": {}
},
"@errorFetchingDefinition": {
"type": "String",
"placeholders": {}
},
"@errorProcessAnalytics": {
"type": "String",
"placeholders": {}
},
"@errorDownloading": {
"type": "String",
"placeholders": {}
},
"@errorFetchingLevelSummary": {
"type": "String",
"placeholders": {}
},
"@errorLoadingSpaceChildren": {
"type": "String",
"placeholders": {}
},
"@unexpectedError": {
"type": "String",
"placeholders": {}
},
"@pleaseReload": {
"type": "String",
"placeholders": {}
},
"@translationError": {
"type": "String",
"placeholders": {}
},
"@errorFetchingTranslation": {
"type": "String",
"placeholders": {}
},
"@errorFetchingActivity": {
"type": "String",
"placeholders": {}
},
"@check": {
"type": "String",
"placeholders": {}
},
"@unableToFindRoom": {
"type": "String",
"placeholders": {}
}
}

1196
scripts/languages.json Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,23 +1,62 @@
"""
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.
- 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
```
- Ensure vi language translations are up-to-date. This script uses en->vi translations as an example on how to translate so it is necessary. If not, you can run:
```
python scripts/translate.py --lang vi --lang-display-name "Vietnamese" --mode append
```
3 modes:
- append mode (default): translate only the missing translation keys
- upsert mode (not implemented): translate everything (all keys from English)
- update mode (not implemented): specify keys to translate and update their metadata
Usage:
python scripts/translate.py
"""
import argparse
import json
import random
from collections import OrderedDict
from datetime import datetime
from pathlib import Path
from typing import Any
from openai import OpenAI
l10n_dir = Path(__file__).parent.parent / "lib" / "l10n"
def load_needed_translations() -> dict[str, list[str]]:
import json
from pathlib import Path
def load_all_keys() -> list[str]:
"""
Load all translation keys from intl_en.arb file.
"""
path_to_en_translations = l10n_dir / "intl_en.arb"
if not path_to_en_translations.exists():
raise FileNotFoundError(
f"File not found: {path_to_en_translations}. Please run `flutter gen-l10n` to generate the file."
)
with open(path_to_en_translations, encoding="utf-8") as f:
translations = json.loads(f.read())
return [key for key in translations.keys() if not key.startswith("@")]
def load_needed_translations() -> dict[str, list[str]]:
path_to_needed_translations = (
Path(__file__).parent.parent / "needed-translations.txt"
)
@ -25,31 +64,30 @@ def load_needed_translations() -> dict[str, list[str]]:
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:
with open(path_to_needed_translations, encoding="utf-8") as f:
needed_translations = json.loads(f.read())
supported_langs = load_supported_languages()
all_keys = load_all_keys()
for lang_code, _ in supported_langs:
if lang_code not in needed_translations:
needed_translations[lang_code] = all_keys
return needed_translations
def load_translations(lang_code: str) -> dict[str, str]:
import json
path_to_translations = l10n_dir / 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())
translations = {}
else:
with open(path_to_translations, encoding="utf-8") 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
path_to_translations = l10n_dir / f"intl_{lang_code}.arb"
@ -58,7 +96,7 @@ def save_translations(lang_code: str, translations: dict[str, str]) -> None:
# Load existing data to preserve order if exists.
if path_to_translations.exists():
with open(path_to_translations, "r") as f:
with open(path_to_translations, "r", encoding="utf-8") as f:
try:
existing_data = json.load(f, object_pairs_hook=OrderedDict)
except json.JSONDecodeError:
@ -73,7 +111,7 @@ def save_translations(lang_code: str, translations: dict[str, str]) -> None:
else:
existing_data[key] = value # new key appended at the end
with open(path_to_translations, "w") as f:
with open(path_to_translations, "w", encoding="utf-8") as f:
f.write(json.dumps(existing_data, indent=2, ensure_ascii=False))
@ -173,14 +211,10 @@ def reconcile_metadata(
save_translations(lang_code, translations)
def translate(lang_code: str, lang_display_name: str) -> None:
def append_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, [])
@ -274,7 +308,7 @@ def translate(lang_code: str, lang_display_name: str) -> None:
"content": prompt,
},
],
model="gpt-4o-mini",
model="gpt-4.1-nano",
temperature=0.0,
)
response = chat_completion.choices[0].message.content
@ -292,15 +326,91 @@ def translate(lang_code: str, lang_display_name: str) -> None:
reconcile_metadata(lang_code, needed_translations, english_translations_dict)
"""Example usage:
python scripts/translate.py
def load_supported_languages() -> list[tuple[str, str]]:
"""
Load the supported languages from the languages.json file.
"""
with open("scripts/languages.json", "r", encoding="utf-8") as f:
raw_languages = json.load(f)
languages: list[tuple[str, str]] = []
for lang in raw_languages:
assert isinstance(lang, dict), "Each language entry must be a dictionary."
language_code = lang.get("language_code", None)
language_name = lang.get("language_name", None)
assert (
language_code and language_name
), f"Each language must have a 'language_code' and 'language_name'. Found: {lang}"
languages.append((language_code, language_name))
return languages
"""
python scripts/translate.py --lang vi --lang-display-name "Vietnamese" --mode append
python scripts/translate.py --translate-all --mode append
"""
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): "
parser = argparse.ArgumentParser(description="Translate app strings.")
parser.add_argument(
"--lang",
type=str,
help="Language code to translate to (e.g. 'vi' for Vietnamese, 'en' for English).",
)
translate(
lang_code=lang_code,
lang_display_name=lang_display_name,
parser.add_argument(
"--lang-display-name",
type=str,
help="Display name of the language (e.g. 'Vietnamese', 'English').",
)
parser.add_argument(
"--mode",
type=str,
choices=["append", "upsert", "update"],
default="append",
help="Mode of translation: 'append' to translate only missing keys, 'upsert' to translate all keys, 'update' to specify keys to translate and update their metadata.",
)
parser.add_argument(
"--translate-all",
action="store_true",
help="Translate all keys (overrides the mode).",
)
args = parser.parse_args()
translate_all = args.translate_all
lang_code = args.lang
lang_display_name = args.lang_display_name
mode = args.mode
if not translate_all:
assert (
args.lang
), "Language code is required if translate all is not set. Use --lang to specify the language code."
assert (
args.lang_display_name
), "Language display name is required if translate all is not set. Use --lang-display-name to specify the language display name."
if mode == "append":
if not translate_all:
append_translate(
lang_code=lang_code,
lang_display_name=lang_display_name,
)
else:
languages = load_supported_languages()
for i, (lang_code, lang_display_name) in enumerate(languages):
print(f"Translating {i + 1}/{len(languages)}: {lang_display_name}")
append_translate(
lang_code=lang_code,
lang_display_name=lang_display_name,
)
else:
raise NotImplementedError(
f"Mode '{mode}' is not implemented yet. Please use 'append' mode for now."
)