2026-11-12 12:28:40 +08:00
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Globalization;
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
|
|
|
|
namespace Ironspore
|
|
|
|
|
{
|
|
|
|
|
public static class GameLocalization
|
|
|
|
|
{
|
2026-11-12 18:01:54 +08:00
|
|
|
public static string PreferenceKey => GameDataPaths.PreferenceKey("settings.locale");
|
2026-11-12 12:28:40 +08:00
|
|
|
public const string FallbackCode = "en";
|
|
|
|
|
public static event Action Changed;
|
|
|
|
|
public static string CurrentCode { get; private set; } = FallbackCode;
|
|
|
|
|
public static LocalizationCatalog Catalog
|
|
|
|
|
{
|
|
|
|
|
get; private set;
|
|
|
|
|
}
|
|
|
|
|
public static CultureInfo Culture => CultureInfo.GetCultureInfo(CurrentCode);
|
|
|
|
|
static readonly Dictionary<string, Dictionary<string, string>> tables = new Dictionary<string, Dictionary<string, string>>();
|
|
|
|
|
static readonly HashSet<string> missing = new HashSet<string>();
|
|
|
|
|
|
|
|
|
|
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
|
|
|
|
|
static void Reset()
|
|
|
|
|
{
|
|
|
|
|
Changed = null;
|
|
|
|
|
Catalog = null;
|
|
|
|
|
CurrentCode = FallbackCode;
|
|
|
|
|
tables.Clear();
|
|
|
|
|
missing.Clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static void Initialize(LocalizationCatalog catalog)
|
|
|
|
|
{
|
|
|
|
|
if (!catalog || catalog.locales == null)
|
|
|
|
|
throw new InvalidOperationException("Localization catalog is missing.");
|
|
|
|
|
Catalog = catalog;
|
|
|
|
|
tables.Clear();
|
|
|
|
|
missing.Clear();
|
|
|
|
|
foreach (var locale in catalog.locales)
|
|
|
|
|
tables.Add(locale.code, ReadTable(locale.table));
|
|
|
|
|
if (!tables.ContainsKey(FallbackCode))
|
|
|
|
|
throw new InvalidOperationException("English fallback table is missing.");
|
|
|
|
|
string system = CultureInfo.CurrentUICulture.Name;
|
|
|
|
|
if (Application.systemLanguage == SystemLanguage.ChineseSimplified || Application.systemLanguage == SystemLanguage.ChineseTraditional || Application.systemLanguage == SystemLanguage.Chinese)
|
|
|
|
|
system = "zh-Hans";
|
|
|
|
|
CurrentCode = ResolveInitialLocale(PlayerPrefs.GetString(PreferenceKey, ""), system, tables.Keys);
|
|
|
|
|
if (!tables.ContainsKey(CurrentCode))
|
|
|
|
|
CurrentCode = FallbackCode;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static Dictionary<string, string> ReadTable(TextAsset source)
|
|
|
|
|
{
|
|
|
|
|
if (!source)
|
|
|
|
|
throw new ArgumentNullException(nameof(source));
|
|
|
|
|
var result = new Dictionary<string, string>(StringComparer.Ordinal);
|
|
|
|
|
var table = JsonUtility.FromJson<TranslationTable>(source.text);
|
|
|
|
|
if (table?.entries == null)
|
|
|
|
|
throw new FormatException("Invalid translation table: " + source.name);
|
|
|
|
|
foreach (var entry in table.entries)
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrWhiteSpace(entry.key) || string.IsNullOrWhiteSpace(entry.value))
|
|
|
|
|
throw new FormatException("Empty translation in " + source.name);
|
|
|
|
|
result.Add(entry.key, entry.value);
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static string ResolveInitialLocale(string saved, string system, IEnumerable<string> availableCodes = null)
|
|
|
|
|
{
|
|
|
|
|
var supported = new HashSet<string>(availableCodes ?? new[] { "en", "zh-Hans" }, StringComparer.OrdinalIgnoreCase);
|
|
|
|
|
if (!string.IsNullOrEmpty(saved) && supported.Contains(saved))
|
|
|
|
|
return saved;
|
|
|
|
|
if (!string.IsNullOrEmpty(system))
|
|
|
|
|
{
|
|
|
|
|
if (system.StartsWith("zh", StringComparison.OrdinalIgnoreCase) && supported.Contains("zh-Hans"))
|
|
|
|
|
return "zh-Hans";
|
|
|
|
|
if (supported.Contains(system))
|
|
|
|
|
return system;
|
|
|
|
|
string language = system.Split('-')[0];
|
|
|
|
|
if (supported.Contains(language))
|
|
|
|
|
return language;
|
|
|
|
|
}
|
|
|
|
|
return FallbackCode;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static bool SelectLocale(string code, bool persist = true)
|
|
|
|
|
{
|
|
|
|
|
if (!tables.ContainsKey(code))
|
|
|
|
|
return false;
|
|
|
|
|
bool changed = CurrentCode != code;
|
|
|
|
|
CurrentCode = code;
|
|
|
|
|
if (persist)
|
|
|
|
|
{
|
|
|
|
|
PlayerPrefs.SetString(PreferenceKey, code);
|
|
|
|
|
PlayerPrefs.Save();
|
|
|
|
|
}
|
|
|
|
|
if (changed)
|
|
|
|
|
Changed?.Invoke();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static string Text(string key, params object[] args)
|
|
|
|
|
{
|
|
|
|
|
string template = null;
|
|
|
|
|
if (tables.TryGetValue(CurrentCode, out var selected))
|
|
|
|
|
selected.TryGetValue(key, out template);
|
|
|
|
|
if (string.IsNullOrEmpty(template) && tables.TryGetValue(FallbackCode, out var fallback))
|
|
|
|
|
fallback.TryGetValue(key, out template);
|
|
|
|
|
if (string.IsNullOrEmpty(template))
|
|
|
|
|
{
|
|
|
|
|
if (Debug.isDebugBuild && missing.Add(key))
|
|
|
|
|
Debug.LogWarning("Missing localization key: " + key);
|
|
|
|
|
return "[" + key + "]";
|
|
|
|
|
}
|
|
|
|
|
return args.Length == 0 ? template : string.Format(Culture, template, args);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static string ItemName(string id) => Text("item." + id);
|
|
|
|
|
public static LocalizedMessage Message(string key, params object[] args) => new LocalizedMessage(key, args);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Keep keys and parameters so existing notifications and reports follow locale changes.
|
|
|
|
|
public sealed class LocalizedMessage
|
|
|
|
|
{
|
|
|
|
|
readonly string key;
|
|
|
|
|
readonly object[] args;
|
|
|
|
|
public LocalizedMessage(string key, params object[] args)
|
|
|
|
|
{
|
|
|
|
|
this.key = key;
|
|
|
|
|
this.args = args;
|
|
|
|
|
}
|
|
|
|
|
public override string ToString() => GameLocalization.Text(key, args);
|
|
|
|
|
}
|
|
|
|
|
}
|