跳到主要内容

本地化

OpenMon 配备了一个由 Google Sheets 支持的自定义本地化系统。翻译内容以 TSV 文件形式下载,解析为 ScriptableLanguage 资源,并在运行时通过 Localizer 类提供服务。该系统位于 OpenMon.Foundation.Localization,游戏特定的后处理器位于 OpenMon.MCE.Localization.PostProcessors。

架构概述​

类用途
Localizer核心运行时服务。加载语言包、解析键、应用后处理器
ILocalizer本地化器的接口,通过 Zenject 注入
ScriptableLanguage保存单一语言所有键值对的 ScriptableObject
LocalizerSettings开发者配置:语言包目录、Google Sheet URL、后处理器
LocalizerConfiguration玩家配置:持久化选择的语言(默认:"English")
GoogleSheetLoader编辑器工具,用于下载工作表并生成 ScriptableLanguage 资源
LocalizedTextMeshPro语言更改时自动更新 TMP_Text 的 UI 组件
LocalizationFontSwitcher按语言切换 TMP 字体(用于 CJK 支持)
LanguageFontMap将语言名称映射到 TMP_FontAsset 覆盖的 ScriptableObject
LocalizedTextPostProcessor用于后处理已解析文本的抽象基类
ReplaceSubstring内置后处理器:简单的查找和替换
InjectPlayerNameMCE 后处理器:将 {PlayerName} 替换为玩家角色的名称
InjectDexValuesMCE 后处理器:将 {Dex.CaughtSpecies} 替换为捕获计数

Google Sheets 集成​

翻译在 Google Sheets 中以以下布局编写:

KeyEnglishSpanishJapanese
Menu/PlayPlayJugar...
Menu/OptionsOptionsOpciones...
Monsters/001/NameSproutylBrotino...

键列之后的每一列代表一种语言。列标题成为 ScriptableLanguage 资源的名称。

编辑器导入​

GoogleSheetLoader 编辑器工具从 LocalizerSettings.GoogleSheetsDownloadUrls 下载每个工作表的 URL,将其转换为 TSV 导出格式,并将行解析为 ScriptableLanguage 资源,保存到 Assets/Resources/{LanguagePackDirectory}/。

流程:

  1. 对于每个 URL,将 Google Sheets 的 /edit URL 转换为 /export?format=tsv
  2. 将 TSV 文件下载到 Temp/
  3. 解析表头以发现语言名称
  4. 为每种语言创建或更新 ScriptableLanguage 资源
  5. 为每行填充 Language[key] = value
  6. 保存资源并刷新 AssetDatabase

支持多个工作表。如果两个工作表定义了相同的键,则后面的 URL 获胜(列表中较低的 URL 覆盖较高的 URL)。

运行时重新下载​

Localizer 可以通过 ReDownloadLanguagesFromGoogleSheet() 在运行时重新下载翻译。这会在内存中创建临时的 ScriptableLanguage 实例(不保存到磁盘)并重新加载语言系统。适用于在不重新导入编辑器的情况下测试翻译。

ScriptableLanguage​

每种语言是一个包含 SerializableDictionary<string, string> 所有键值对的 ScriptableLanguage ScriptableObject。该资源存储在 Resources/Languages/ 中(可通过 LocalizerSettings.LanguagePackDirectory 配置)。

// ScriptableLanguage 解析键,未找到时回退到原始键
string value = englishPack["Menu/Play"]; // "Play"
string missing = englishPack["Nonexistent"]; // "Nonexistent"(原始键作为回退)

当找不到键时,资源返回键本身。这使得缺失的翻译在游戏中作为原始键字符串立即可见。

Localizer​

Localizer 是主要的运行时服务,通过 Zenject 绑定为 ILocalizer。它在初始化时从 Resources 目录加载所有 ScriptableLanguage 资源。

解析文本​

// 索引器语法(最常用)
string text = localizer["Menu/Play"]; // "Play"

// 带替换参数
// 如果值为 "You caught {0}!" 且 "Monsters/025/Name" 解析为 "Thundermane"
string text = localizer["Messages/Caught", true, "Monsters/025/Name"];
// 结果:"You caught Thundermane!"

// 参数作为字面字符串(非本地化键)
string text = localizer.GetText("Messages/Caught", false, "Thundermane");

翻译值中的 {0}、{1} 等占位符会被相应的修饰字符串替换。当 modifiersAreLocalizableKeys 为 true(默认值)时,每个修饰符在替换前会先通过本地化器解析。

语言切换​

// 获取可用语言
List<string> languages = localizer.GetAllLanguageIds(); // ["English", "Spanish", "Japanese"]

// 获取当前语言
string current = localizer.GetCurrentLanguage(); // "English"

// 切换语言(自动持久化到配置)
localizer.SetLanguage("Spanish");

// 按索引切换
localizer.SetLanguage(1);

语言更改时,选择会通过 IConfigurationManager 持久化,以便在应用重启后保持。所有语言更改事件的订阅者都会收到通知。

语言更改事件​

// 使用语言名称订阅
localizer.SubscribeToLanguageChange((string newLang) =>
{
Debug.Log("Language changed to: " + newLang);
});

// 无参数订阅(仅通知)
localizer.SubscribeToLanguageChange(() => RefreshUI());

// 取消订阅
localizer.UnsubscribeFromLanguageChange(myCallback);

本地化键约定​

键遵循分层路径格式:

领域模式示例
UI / 菜单Menu/{Screen}/{Element}Menu/Battle/Run
怪物Monsters/{DexNumber}/NameMonsters/001/Name
怪物描述Monsters/{DexNumber}/DescriptionMonsters/001/Description
招式Moves/{MoveName}/NameMoves/Tackle/Name
道具Items/{ItemName}/NameItems/Potion/Name
任务Quests/{QuestName}Quests/FindTheProfessor
任务目标Quests/{QuestName}/Objectives/{N}Quests/FindTheProfessor/Objectives/0
对话Dialog/{SceneName}/{NPC}/{Line}Dialog/Route1/OldMan/0

文本后处理器​

键被解析且修饰符被替换后,文本会通过 LocalizerSettings.TextPostProcessors 中配置的 LocalizedTextPostProcessor 实例管道。每个处理器可以原地修改字符串。

内置:ReplaceSubstring​

一个简单的查找和替换处理器。通过 OpenMon > Localization > TextPostProcessors > ReplaceSubstring 创建。在检视面板中配置 Original 和 Replacement 字段。适用于全局替换,例如将 \n 替换为实际换行符。

MCE:InjectPlayerName​

将任何本地化字符串中的 {PlayerName} 替换为玩家角色的 LocalizableName。通过 MCE > Localization > TextPostProcessors > InjectPlayerName 创建。

// 翻译值:"Welcome, {PlayerName}! Ready to explore?"
// 后处理后:"Welcome, Red! Ready to explore?"

MCE:InjectDexValues​

将 {Dex.CaughtSpecies} 替换为玩家已捕获的物种数量。通过 MCE > Localization > TextPostProcessors > InjectDexValues 创建。

自定义后处理器​

扩展 LocalizedTextPostProcessor 来创建自己的处理器:

[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;
}
}

将资源添加到 LocalizerSettings.TextPostProcessors 以使其包含在管道中。

UI:LocalizedTextMeshPro​

LocalizedTextMeshPro 组件将 TMP_Text 绑定到本地化键。它在语言更改时自动更新。

// 从代码设置
localizedText.SetValue("Menu/Play");

// 带修饰符
localizedText.SetValue("Messages/Caught", true, "Monsters/025/Name");

将此组件添加到任何带有 TMP_Text 的 GameObject 上。在检视面板中设置 LocalizationKey 字段。当 SetOnEnable 为 true(默认值)时,文本在启用时立即解析。

CJK 字体支持​

对于需要不同字形的语言(如日语),请将 LocalizationFontSwitcher 组件与 LanguageFontMap 资源一起使用。

设置​

  1. 通过 OpenMon > Localization > Language Font Map 创建一个 LanguageFontMap 资源
  2. 添加将语言名称映射到 TMP_FontAsset 引用的条目:
    • "Japanese" -> NotoSansJP-Regular SDF
    • 可选择为每种语言设置粗体变体
  3. 将 LocalizationFontSwitcher 添加到根 Canvas
  4. 将 LanguageFontMap 分配给 FontMap 字段
  5. 将 SearchScope 设置为 Children(默认)或 Scene

语言更改时,切换器会遍历配置范围内的所有 TMP_Text 组件并交换它们的字体。它会缓存原始字体,以便切换回未映射的语言时恢复默认字体。

依赖注入设置​

LocalizationInstaller(通过 OpenMon > Installers > Localization 创建)绑定:

  • LocalizerSettings 为单例(注入到 Localizer)
  • ILocalizer 到 Localizer 为惰性单例
  • 所有配置的 TextPostProcessors 排队等待注入
SceneContext
+-- LocalizationInstaller(引用 LocalizerSettings 资源)

添加新语言​

  1. 在 Google Sheet 中添加一个新列,以语言名称作为标题(例如 French)
  2. 填写所有键的翻译
  3. 在编辑器中重新运行 Google Sheet 导入
  4. 新的 French.asset 将出现在 Resources/Languages/ 中
  5. 如果该语言需要不同的字体,请在 LanguageFontMap 中添加一个条目

无需更改代码。Localizer 在启动时会自动发现配置目录中的所有 ScriptableLanguage 资源。