Localización
OpenMon incluye un sistema de localización personalizado respaldado por Google Sheets. Las traducciones se descargan como archivos TSV, se parsean en activos ScriptableLanguage y se sirven en tiempo de ejecución a través de la clase Localizer. El sistema vive en OpenMon.Foundation.Localization con posprocesadores específicos del juego en OpenMon.MCE.Localization.PostProcessors.
Descripción general de la arquitectura
| Clase | Propósito |
|---|---|
Localizer | Servicio central de runtime. Carga paquetes de idioma, resuelve claves, aplica posprocesadores |
ILocalizer | Interfaz para el localizador, inyectada vía Zenject |
ScriptableLanguage | ScriptableObject que contiene todos los pares clave-valor para un idioma |
LocalizerSettings | Configuración de desarrollador: directorio de paquetes de idioma, URLs de Google Sheet, posprocesadores |
LocalizerConfiguration | Configuración de jugador: idioma seleccionado persistido (predeterminado: "English") |
GoogleSheetLoader | Herramienta de editor que descarga hojas y genera activos ScriptableLanguage |
LocalizedTextMeshPro | Componente UI que actualiza automáticamente un TMP_Text cuando cambia el idioma |
LocalizationFontSwitcher | Intercambia fuentes TMP por idioma (para soporte CJK) |
LanguageFontMap | ScriptableObject que mapea nombres de idioma a sobreescrituras de TMP_FontAsset |
LocalizedTextPostProcessor | Base abstracta para posprocesar texto resuelto |
ReplaceSubstring | Posprocesador incorporado: buscar y reemplazar simple |
InjectPlayerName | Posprocesador MCE: reemplaza {PlayerName} con el nombre del personaje del jugador |
InjectDexValues | Posprocesador MCE: reemplaza {Dex.CaughtSpecies} con el conteo de capturados |
Integración con Google Sheets
Las traducciones se crean en Google Sheets con esta disposición:
| Clave | English | Spanish | Japanese |
|---|---|---|---|
Menu/Play | Play | Jugar | ... |
Menu/Options | Options | Opciones | ... |
Monsters/001/Name | Sproutyl | Brotino | ... |
Cada columna después de la columna de clave representa un idioma. El encabezado de la columna se convierte en el nombre del activo ScriptableLanguage.
Importación desde el editor
La utilidad de editor GoogleSheetLoader descarga cada URL de hoja desde LocalizerSettings.GoogleSheetsDownloadUrls, la convierte al formato de exportación TSV y parsea las filas en activos ScriptableLanguage guardados en Assets/Resources/{LanguagePackDirectory}/.
El flujo:
- Para cada URL, convertir la URL
/editde Google Sheets a/export?format=tsv - Descargar el archivo TSV a
Temp/ - Parsear los encabezados para descubrir los nombres de idioma
- Crear o actualizar un activo
ScriptableLanguagepor idioma - Poblar
Language[key] = valuepara cada fila - Guardar activos y refrescar el AssetDatabase
Se soportan múltiples hojas. Si dos hojas definen la misma clave, gana la URL posterior (las URLs más abajo en la lista sobrescriben a las de arriba).
Redescarga en tiempo de ejecución
El Localizer puede redescargar traducciones en runtime mediante ReDownloadLanguagesFromGoogleSheet(). Esto crea instancias temporales de ScriptableLanguage en memoria (no guardadas en disco) y recarga el sistema de idioma. Útil para probar traducciones sin reimportar en el editor.
ScriptableLanguage
Cada idioma es un ScriptableLanguage ScriptableObject que contiene un SerializableDictionary<string, string> de todos los pares clave-valor. El activo se almacena en Resources/Languages/ (configurable mediante LocalizerSettings.LanguagePackDirectory).
// ScriptableLanguage resuelve claves con fallback a la clave original
string value = englishPack["Menu/Play"]; // "Play"
string missing = englishPack["Nonexistent"]; // "Nonexistent" (clave original como fallback)
Cuando no se encuentra una clave, el activo devuelve la misma clave. Esto hace que las traducciones faltantes sean inmediatamente visibles en el juego como cadenas de clave original.
Localizer
El Localizer es el servicio principal de runtime, vinculado como ILocalizer mediante Zenject. Carga todos los activos ScriptableLanguage desde el directorio Resources al inicializarse.
Resolución de texto
// Sintaxis de indexador (la más común)
string text = localizer["Menu/Play"]; // "Play"
// Con parámetros de sustitución
// Si el valor es "You caught {0}!" y "Monsters/025/Name" resuelve a "Thundermane"
string text = localizer["Messages/Caught", true, "Monsters/025/Name"];
// Resultado: "You caught Thundermane!"
// Parámetros como cadenas literales (no claves de localización)
string text = localizer.GetText("Messages/Caught", false, "Thundermane");
Los marcadores de posición {0}, {1}, etc. en los valores de traducción son reemplazados por la cadena modificadora correspondiente. Cuando modifiersAreLocalizableKeys es true (predeterminado), cada modificador se resuelve a través del localizador antes de la sustitución.
Cambio de idioma
// Obtener idiomas disponibles
List<string> languages = localizer.GetAllLanguageIds(); // ["English", "Spanish", "Japanese"]
// Obtener idioma actual
string current = localizer.GetCurrentLanguage(); // "English"
// Cambiar idioma (persistido automáticamente en la configuración)
localizer.SetLanguage("Spanish");
// Cambiar por índice
localizer.SetLanguage(1);
Cuando cambia el idioma, la selección se persiste mediante IConfigurationManager para que sobreviva a los reinicios de la aplicación. Todos los suscriptores al evento de cambio de idioma son notificados.
Eventos de cambio de idioma
// Suscribirse con nombre de idioma
localizer.SubscribeToLanguageChange((string newLang) =>
{
Debug.Log("Language changed to: " + newLang);
});
// Suscribirse sin parámetro (solo una notificación)
localizer.SubscribeToLanguageChange(() => RefreshUI());
// Cancelar suscripción
localizer.UnsubscribeFromLanguageChange(myCallback);
Convenciones de claves de localización
Las claves siguen un formato de ruta jerárquica:
| Dominio | Patrón | Ejemplo |
|---|---|---|
| UI / Menús | Menu/{Screen}/{Element} | Menu/Battle/Run |
| Monstruos | Monsters/{DexNumber}/Name | Monsters/001/Name |
| Descripciones de monstruos | Monsters/{DexNumber}/Description | Monsters/001/Description |
| Movimientos | Moves/{MoveName}/Name | Moves/Tackle/Name |
| Objetos | Items/{ItemName}/Name | Items/Potion/Name |
| Misiones | Quests/{QuestName} | Quests/FindTheProfessor |
| Objetivos de misión | Quests/{QuestName}/Objectives/{N} | Quests/FindTheProfessor/Objectives/0 |
| Diálogos | Dialog/{SceneName}/{NPC}/{Line} | Dialog/Route1/OldMan/0 |
Posprocesadores de texto
Después de que una clave se resuelve y los modificadores se sustituyen, el texto pasa por un pipeline de instancias de LocalizedTextPostProcessor configuradas en LocalizerSettings.TextPostProcessors. Cada procesador puede modificar la cadena in-situ.
Incorporado: ReplaceSubstring
Un procesador simple de buscar y reemplazar. Se crea mediante OpenMon > Localization > TextPostProcessors > ReplaceSubstring. Configure los campos Original y Replacement en el inspector. Útil para sustituciones globales como reemplazar \n con saltos de línea reales.
MCE: InjectPlayerName
Reemplaza {PlayerName} en cualquier cadena localizada con el LocalizableName del personaje del jugador. Se crea mediante MCE > Localization > TextPostProcessors > InjectPlayerName.
// Valor de traducción: "Welcome, {PlayerName}! Ready to explore?"
// Después del posprocesado: "Welcome, Red! Ready to explore?"
MCE: InjectDexValues
Reemplaza {Dex.CaughtSpecies} con el número de especies que el jugador ha capturado. Se crea mediante MCE > Localization > TextPostProcessors > InjectDexValues.
Posprocesadores personalizados
Extienda LocalizedTextPostProcessor para crear el suyo propio:
[CreateAssetMenu(menuName = "MyGame/Localization/MyPostProcessor")]
public class MyPostProcessor : LocalizedTextPostProcessor
{
public override bool PostProcessText(ref string text, params object[] extraParams)
{
text = text.Replace("{MyToken}", GetMyValue());
return true;
}
}
Agregue el activo a LocalizerSettings.TextPostProcessors para que se incluya en el pipeline.
UI: LocalizedTextMeshPro
El componente LocalizedTextMeshPro vincula un TMP_Text a una clave de localización. Se actualiza automáticamente cuando cambia el idioma.
// Establecer desde código
localizedText.SetValue("Menu/Play");
// Con modificadores
localizedText.SetValue("Messages/Caught", true, "Monsters/025/Name");
Agregue este componente a cualquier GameObject con un TMP_Text. Establezca el campo LocalizationKey en el inspector. Cuando SetOnEnable es true (predeterminado), el texto se resuelve inmediatamente al habilitarse.
Soporte de fuentes CJK
Para idiomas como japonés que requieren glifos diferentes, use el componente LocalizationFontSwitcher junto con un activo LanguageFontMap.
Configuración
- Cree un activo LanguageFontMap mediante OpenMon > Localization > Language Font Map
- Agregue entradas que mapeen nombres de idioma a referencias de TMP_FontAsset:
"Japanese"->NotoSansJP-Regular SDF- Opcionalmente, establezca una variante negrita por idioma
- Agregue
LocalizationFontSwitchera su Canvas raíz - Asigne el
LanguageFontMapal campoFontMap - Establezca
SearchScopeaChildren(predeterminado) oScene
Cuando cambia el idioma, el conmutador itera todos los componentes TMP_Text en el ámbito configurado e intercambia su fuente. Almacena en caché la fuente original para que al volver a un idioma no mapeado se restaure la predeterminada.
Configuración de inyección de dependencias
El LocalizationInstaller (crear mediante OpenMon > Installers > Localization) vincula:
LocalizerSettingscomo singleton (inyectado enLocalizer)ILocalizeraLocalizercomo singleton lazy- Todos los
TextPostProcessorsconfigurados se encolan para inyección
SceneContext
+-- LocalizationInstaller (referencia al activo LocalizerSettings)
Agregar un nuevo idioma
- Agregue una nueva columna a su Google Sheet con el nombre del idioma como encabezado (ej.,
French) - Complete las traducciones para todas las claves
- Vuelva a ejecutar la importación de Google Sheet en el editor
- Aparecerá un nuevo activo
French.assetenResources/Languages/ - Si el idioma requiere una fuente diferente, agregue una entrada a su
LanguageFontMap
No se requieren cambios de código. El Localizer descubre todos los activos ScriptableLanguage en el directorio configurado al iniciar.