feat: establish mech extraction prototype with bilingual UI
Track Unity source, scenes, editable assets, licenses, tools and QA evidence. Exclude generated caches, Windows builds and raw runtime logs.
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Ironspore
|
||||
{
|
||||
public static class GameLocalization
|
||||
{
|
||||
public const string PreferenceKey = "settings.locale";
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user