I2 Localization 多语言资源拆分方案
📋 目录
I2 Localization 官方架构原理
核心数据结构
I2 Localization 插件使用以下核心数据结构管理多语言:
┌─────────────────────────────────────────────────────────────────┐
│ LanguageSourceData │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ mLanguages: List<LanguageData> │ │
│ │ ├── [0] { Name: "English", Code: "en-US" } │ │
│ │ ├── [1] { Name: "简体中文", Code: "zh-CN" } │ │
│ │ └── [2] { Name: "日本語", Code: "ja-JP" } │ │
│ └─────────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ mTerms: List<TermData> │ │
│ │ ├── [0] { Term: "btn_ok", │ │
│ │ │ Languages: ["OK", "确定", "はい"] } │ │
│ │ ├── [1] { Term: "btn_cancel", │ │
│ │ │ Languages: ["Cancel", "取消", "キャンセル"] } │ │
│ │ └── ... │ │
│ └─────────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ mDictionary: Dictionary<string, TermData> │ │
│ │ 用于快速查找 Term │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
翻译查找流程
┌──────────────────────────────────────────────────────────────────┐
│ LocalizationManager.GetTranslation("btn_ok") │
└──────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ 遍历 Sources 列表(支持多个 LanguageSourceData) │
│ for (int i = 0; i < Sources.Count; i++) │
│ { │
│ if (Sources[i].TryGetTranslation(term, out translation)) │
│ return translation; │
│ } │
└──────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ LanguageSourceData.TryGetTranslation(term) │
│ ├── 1. 从 mDictionary 查找 TermData │
│ ├── 2. 获取当前语言索引 CurrentLanguageIndex │
│ └── 3. 返回 termData.Languages[CurrentLanguageIndex] │
└──────────────────────────────────────────────────────────────────┘
│
▼
返回 "确定"
TermData 结构详解
每个 TermData 存储一个 Key 对应的所有语言翻译:
csharp
public class TermData
{
public string Term; // Key 名称,如 "btn_ok"
public eTermType TermType; // 类型:Text, Texture, Audio 等
public string Description; // 描述(仅 Editor)
// 🔑 核心:所有语言的翻译存储在同一个数组中
public string[] Languages; // ["OK", "确定", "はい", ...]
public byte[] Flags; // 每个翻译的标记位
}
关键点 :Languages 数组的索引与 mLanguages 列表的索引一一对应。
语言切换原理
csharp
// 官方语言切换流程
LocalizationManager.CurrentLanguage = "简体中文";
// 内部实现:
// 1. 找到语言索引
int langIndex = Source.GetLanguageIndex("简体中文"); // 返回 1
// 2. 后续所有翻译查询都使用这个索引
string translation = termData.Languages[langIndex]; // Languages[1] = "确定"
Localize 组件的更新机制
当语言切换时,I2 通过以下机制通知所有 Localize 组件更新:
┌────────────────────────────────────────────────────────────────────┐
│ LocalizationManager.CurrentLanguage = "简体中文" │
└────────────────────────────────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────────┐
│ SetLanguageAndCode(languageName, languageCode) │
│ ├── mCurrentLanguage = languageName │
│ ├── mLanguageCode = languageCode │
│ └── LocalizeAll(Force) // 🔑 触发全局更新 │
└────────────────────────────────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────────┐
│ LocalizeAll() │
│ // 使用 Unity API 查找场景中所有 Localize 组件 │
│ Localize[] locals = Resources.FindObjectsOfTypeAll<Localize>(); │
│ foreach (var local in locals) │
│ { │
│ local.OnLocalize(Force); // 逐个通知更新 │
│ } │
│ OnLocalizeEvent?.Invoke(); // 触发全局事件(可选监听) │
└────────────────────────────────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────────┐
│ Localize.OnLocalize(Force) │
│ ├── 检查:LastLocalizedLanguage == CurrentLanguage ? │
│ │ └── 如果相同且非强制,跳过更新 │
│ ├── 获取翻译:LocalizationManager.GetTranslation(FinalTerm) │
│ └── 更新目标:mLocalizeTarget.DoLocalize(translation) │
│ └── 例如:Text.text = translation │
└────────────────────────────────────────────────────────────────────┘
核心代码(LocalizationManager_Language.cs):
csharp
public static void SetLanguageAndCode(string LanguageName, string LanguageCode, ...)
{
if (mCurrentLanguage != LanguageName || mLanguageCode != LanguageCode || Force)
{
mCurrentLanguage = LanguageName;
mLanguageCode = LanguageCode;
// 🔑 通知所有 Localize 组件更新
LocalizeAll(Force);
}
}
核心代码(LocalizationManager_Translation.cs):
csharp
public static void LocalizeAll(bool Force = false)
{
// 查找场景中所有 Localize 组件(包括未激活的)
Localize[] Locals = (Localize[])Resources.FindObjectsOfTypeAll(typeof(Localize));
for (int i = 0; i < Locals.Length; i++)
{
Locals[i].OnLocalize(Force);
}
// 触发全局事件,供其他脚本监听
if (OnLocalizeEvent != null)
OnLocalizeEvent();
}
Localize 组件的更新逻辑(Localize.cs):
csharp
public void OnLocalize(bool Force = false)
{
// 避免重复更新:如果语言没变且非强制,跳过
if (!Force && LastLocalizedLanguage == LocalizationManager.CurrentLanguage)
return;
LastLocalizedLanguage = LocalizationManager.CurrentLanguage;
// 获取新翻译
MainTranslation = LocalizationManager.GetTranslation(FinalTerm);
// 更新目标组件(如 Text、Image 等)
mLocalizeTarget.DoLocalize(this, MainTranslation, SecondaryTranslation);
}
关键设计点:
| 机制 | 说明 |
|---|---|
Resources.FindObjectsOfTypeAll |
查找所有 Localize 组件,包括未激活的 |
LastLocalizedLanguage |
缓存上次语言,避免重复更新 |
OnLocalizeEvent |
全局事件,供自定义脚本监听语言变化 |
mLocalizeTarget |
抽象目标接口,支持 Text、Image、Audio 等多种类型 |
官方方案的问题
单一 Asset 架构
官方设计将所有数据存储在一个 Asset 中:
Resources/
└── I2Languages.prefab (或 .asset)
└── LanguageSourceData
├── mLanguages: [en-US, zh-CN, ja-JP, ko-KR, ...] (25种语言)
└── mTerms: [5000个 TermData,每个包含25种翻译]
问题分析
| 问题 | 具体表现 |
|---|---|
| 包体积膨胀 | 5,000 Key × 25 语言 × 50 字节 ≈6.25 MB 但用户实际只需 1 种语言 ≈ 0.25 MB |
| 内存浪费 | 运行时所有语言数据常驻内存,实际只用 1/25 |
| 加载缓慢 | 启动时反序列化 6MB+ 数据 |
| 热更粒度粗 | 改一个语言的一个词,需更新整个 6MB 文件 |
拆分方案设计
核心思路
将 TermData.Languages[] 数组按语言拆分到独立文件:
改造前: 改造后:
┌────────────────────┐ ┌─────────────────────┐
│ I2Languages.asset │ │ I2Languages.asset │ (仅语言列表)
│ ├── mLanguages │ │ └── mLanguages │
│ │ [en-US, zh-CN] │ │ [en-US, zh-CN, ja-JP...] │
│ └── mTerms │ │ (无 mTerms) │
│ ├── btn_ok │ ├─────────────────────┤
│ │ Languages: │ │ en-US.asset │
│ │ [0]="OK" │ ──────► │ ├── mLanguages: [en-US] │
│ │ [1]="确定" │ │ └── mTerms │
│ │ [2]="はい" │ │ └── btn_ok │
│ └── ... │ │ Languages[0]="OK" │
└────────────────────┘ ├─────────────────────┤
│ zh-CN.asset │
│ ├── mLanguages: [zh-CN] │
│ └── mTerms │
│ └── btn_ok │
│ Languages[0]="确定" │
└─────────────────────┘
文件结构
AssetBundle/SplitLanguage/
├── I2Languages.asset # 索引:仅包含语言列表(mLanguages)
├── en-US.asset # 英语:包含所有 Key + 英语翻译
├── zh-CN.asset # 中文:包含所有 Key + 中文翻译
├── ja-JP.asset # 日语:包含所有 Key + 日语翻译
└── ...
核心改造点
修改 Import_CSV 方法(唯一代码改动)
官方 Import_CSV 方法签名:
csharp
// 官方版本(三个参数)
public string Import_CSV(string Category, List<string[]> CSV,
eSpreadsheetUpdateMode UpdateMode = eSpreadsheetUpdateMode.Replace)
我们添加了第四个参数 splitCode:
csharp
// 改造版本(四个参数)
public string Import_CSV(string Category, List<string[]> CSV,
eSpreadsheetUpdateMode UpdateMode = eSpreadsheetUpdateMode.Replace,
string splitCode = "") // 🔑 新增:指定只导入某种语言
新增代码(约 5 行):
csharp
// LanguageSourceData_Import_CSV.cs 第 90-94 行
// 在解析语言列时,跳过非目标语言
GoogleLanguages.UnPackCodeFromLanguageName(langToken, out LanName, out LanCode);
// ========== 新增代码 START ==========
if (!string.IsNullOrEmpty(splitCode) && LanCode != splitCode)
{
LanIndices[i] = -1; // 标记为跳过
continue;
}
// ========== 新增代码 END ==========
int LanIdx = GetLanguageIndexFromCode(LanCode);
// ... 后续逻辑不变
设计要点:
- 向后兼容:
splitCode默认为空,行为与官方版本完全一致 - 最小侵入:只添加 5 行代码,不修改任何官方逻辑
- 通过
LanIndices[i] = -1标记跳过,复用官方的跳过机制
多语言切分工具
I2UpdateTool - 核心拆分工具
提供 Unity Editor 菜单入口:Tools/I2Localize Split
csharp
public class I2SplitTool : EditorWindow
{
[MenuItem("Tools/I2Localize Split")]
private static void ShowWindow()
{
var window = GetWindow<I2SplitTool>("多语言切分工具");
window.Show();
}
}
核心拆分逻辑
csharp
/// <summary>
/// 按语言拆分多语言配置
/// </summary>
/// <param name="csvString">完整的 CSV 数据(包含所有语言)</param>
/// <param name="firstSheet">是否为第一个表(决定 Replace 还是 Merge)</param>
private void SplitLocalConfig(string csvString, bool firstSheet)
{
var outputPath = "Assets/AssetBundle/SplitLanguage";
// 1. 清空输出目录
if (Directory.Exists(outputPath))
Directory.Delete(outputPath, true);
Directory.CreateDirectory(outputPath);
// 2. 解析 CSV
List<string[]> csv = LocalizationReader.ReadCSV(csvString);
var csvLanguages = new List<string[]>() { csv[0] }; // 只取表头
// 3. 创建索引文件(仅包含语言列表,不包含 Key 和翻译)
// csvLanguages 只有表头一行:["Key", "Type", "Desc", "en-US", "zh-CN", ...]
var i2LanguageAsset = CreateInstance<LanguageSourceAsset>();
i2LanguageAsset.mSource.Import_CSV(string.Empty, csvLanguages);
AssetDatabase.CreateAsset(i2LanguageAsset,
Path.Combine(outputPath, "I2Languages.asset"));
// 4. 🔑 核心:为每种语言创建独立的 Asset
foreach (var language in _newSourceData.mLanguages)
{
if (!language.IsEnabled()) continue;
// 创建新的 LanguageSourceAsset
var tempSource = CreateInstance<LanguageSourceAsset>();
// 使用 splitCode 参数,只导入当前语言
tempSource.mSource.Import_CSV(
string.Empty,
csv,
eSpreadsheetUpdateMode.Replace,
language.Code); // 🔑 关键:传入语言代码
// 保存为独立文件:en-US.asset, zh-CN.asset, ja-JP.asset ...
var output = Path.Combine(outputPath, language.Code + ".asset");
AssetDatabase.CreateAsset(tempSource, output);
}
AssetDatabase.SaveAssets();
}
更新流程
┌────────────────────────────────────────────────────────────────┐
│ 1. 点击 "拆分多语言数据" │
└────────────────────────────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────┐
│ 2. SplitLocalConfig() 执行拆分 │
│ ├── 创建 I2Languages.asset(索引) │
│ ├── 创建 en-US.asset │
│ ├── 创建 zh-CN.asset │
│ ├── 创建 ja-JP.asset │
│ └── ... │
└────────────────────────────────────────────────────────────────┘
运行时加载
LanguageLoader - 按需加载管理
csharp
public static class LanguageLoader
{
private static LanguageSourceAsset _source;
/// <summary>
/// 初始化多语言系统
/// </summary>
public static void Init()
{
// 1. 加载索引文件(语言列表,用于语言切换时查询)
LoadLanguageIndex();
// 2. 加载当前语言的翻译数据
LoadCurrentLanguage();
}
/// <summary>
/// 加载索引文件(仅包含语言列表,用于语言名称和代码的映射)
/// </summary>
private static void LoadLanguageIndex()
{
// I2Languages.asset 仅包含 mLanguages 列表,体积极小(约 2KB)
// 作用:提供语言代码到语言名称的映射(如 "en-US" → "English")
var asset = LoadAsset<LanguageSourceAsset>(
Path.Combine("SplitLanguage", "I2Languages.asset"));
asset.mSource.Awake(); // 注册到 LocalizationManager.Sources
}
/// <summary>
/// 加载当前语言的翻译数据
/// </summary>
private static void LoadCurrentLanguage()
{
var code = GetLanguageCode(); // 如 "en-US"
// 加载对应语言的 Asset(如 en-US.asset,约 250KB)
var path = Path.Combine("SplitLanguage", code + ".asset");
_source = LoadAsset<LanguageSourceAsset>(path);
// Fallback:找不到时使用英语
if (_source == null)
{
_source = LoadAsset<LanguageSourceAsset>(
Path.Combine("SplitLanguage", "en-US.asset"));
LocalizationManager.SetLanguageAndCode("English (United States)", "en-US");
}
_source.mSource.Awake(); // 注册到 LocalizationManager.Sources
}
/// <summary>
/// 运行时切换语言
/// </summary>
public static void SwitchLanguage(string code)
{
// 1. 卸载当前语言
UnloadLanguage();
// 2. 加载新语言
var path = Path.Combine("SplitLanguage", code + ".asset");
_source = LoadAsset<LanguageSourceAsset>(path);
if (_source != null)
{
var languageName = LocalizationManager.GetLanguageFromCode(code);
LocalizationManager.SetLanguageAndCode(languageName, code);
_source.mSource.Awake();
}
}
/// <summary>
/// 卸载当前语言资源
/// </summary>
private static void UnloadLanguage()
{
if (_source != null)
{
_source.mSource.OnDestroy(); // 从 Sources 列表移除
UnloadAsset(_source);
_source = null;
}
}
}
加载时序图
游戏启动
│
▼
LanguageLoader.Init()
│
├──► LoadLanguageIndex()
│ └── 加载 I2Languages.asset (仅语言列表, ~2KB)
│ └── 注册到 LocalizationManager.Sources[0]
│ └── 作用:提供语言代码↔名称映射
│
└──► LoadCurrentLanguage()
└── 加载 en-US.asset (Key + 英语翻译, ~250KB)
└── 注册到 LocalizationManager.Sources[1]
│
▼
翻译查询
│
▼
LocalizationManager.GetTranslation("btn_ok")
│
├── Sources[0] (I2Languages) → 没有 Term 数据,跳过
└── Sources[1] (en-US) → 找到 Term,返回 "OK"
│
▼
用户切换语言 → LanguageLoader.SwitchLanguage("zh-CN")
│
├── UnloadLanguage() → 卸载 en-US.asset
└── LoadLanguage() → 加载 zh-CN.asset
│
▼
LocalizationManager.GetTranslation("btn_ok") → 返回 "确定"
收益分析
包体积对比
| 指标 | 官方方案 | 拆分方案 | 优化幅度 |
|---|---|---|---|
| 初始包大小 | 6.25 MB | ~0.25 MB | ↓ 96% |
| 运行时内存 | 6.25 MB | ~0.25 MB | ↓ 96% |
| 启动加载时间 | ~500 ms | ~30 ms | ↓ 94% |
按 5,000 Key × 25 语言 × 50 字节计算
拆分后:I2Languages.asset (~2KB) + 单语言 Asset (~250KB)
热更新对比
| 更新场景 | 官方方案 | 拆分方案 |
|---|---|---|
| 修改英语一个词 | 下载 6.25 MB | 下载 0.25 MB |
| 新增一种语言 | 下载 6.25 MB | 下载 0.25 MB |
| 修复日语翻译 | 下载 6.25 MB | 下载 0.25 MB |