using System.Globalization; using System.Runtime.InteropServices; namespace WeaveLoader.API; /// /// Localized text value used for names, tooltips, and UI strings. /// public sealed class Text { public enum TextKind { Literal = 0, Translatable = 1 } public TextKind Kind { get; } public string Value { get; } private Text(TextKind kind, string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); Kind = kind; Value = value; } public static Text Literal(string value) => new(TextKind.Literal, value); public static Text Translatable(string key) => new(TextKind.Translatable, key); internal string Resolve() { return Kind == TextKind.Literal ? Value : Localization.Resolve(Value); } } /// /// Simple language table loader for mod translations (Java-style .lang files). /// public static class Localization { private static readonly object s_lock = new(); private static string s_locale = GetDefaultLocale(); private static string? s_loadedLocale; private static Dictionary s_entries = new(StringComparer.Ordinal); private static int s_lastGameLanguage = int.MinValue; /// When true, follow the game's current language selection. public static bool UseGameLanguage { get; set; } = true; /// Active locale used when resolving translatable keys (e.g. "en-GB"). public static string Locale { get => s_locale; set { ArgumentException.ThrowIfNullOrWhiteSpace(value); lock (s_lock) { s_locale = value; s_loadedLocale = null; } } } /// Reload language files for the current locale. public static void Reload() { lock (s_lock) { s_loadedLocale = null; } } internal static string Resolve(string key) { EnsureLoaded(); return s_entries.TryGetValue(key, out var value) ? value : key; } private static void EnsureLoaded() { lock (s_lock) { if (UseGameLanguage) { string? gameLocale = TryGetGameLocale(); if (!string.IsNullOrWhiteSpace(gameLocale) && !string.Equals(gameLocale, s_locale, StringComparison.OrdinalIgnoreCase)) { s_locale = gameLocale; s_loadedLocale = null; } } if (s_loadedLocale == s_locale) return; s_entries = LoadLocaleTable(s_locale); s_loadedLocale = s_locale; } } private static string GetDefaultLocale() { string? gameLocale = TryGetGameLocale(); if (!string.IsNullOrWhiteSpace(gameLocale)) return gameLocale!; var env = Environment.GetEnvironmentVariable("WEAVELOADER_LOCALE"); if (!string.IsNullOrWhiteSpace(env)) return env; try { var culture = CultureInfo.CurrentUICulture; if (!string.IsNullOrWhiteSpace(culture.Name)) return culture.Name.Replace('_', '-'); } catch { // ignore } return "en-GB"; } private static string? TryGetGameLocale() { try { int lang = NativeInterop.native_get_minecraft_language(); if (lang == s_lastGameLanguage) return null; s_lastGameLanguage = lang; return MapLanguageToLocale(lang); } catch { return null; } } private static string? MapLanguageToLocale(int lang) { // 0 = default (system); return null so we fall back to system culture / env. return lang switch { 1 => "en-GB", 2 => "ja-JP", 3 => "de-DE", 4 => "fr-FR", 5 => "es-ES", 6 => "it-IT", 7 => "ko-KR", 8 => "zh-TW", 9 => "pt-PT", 10 => "pt-BR", 11 => "ru-RU", 12 => "nl-NL", 13 => "fi-FI", 14 => "sv-SE", 15 => "da-DK", 16 => "no-NO", 17 => "pl-PL", 18 => "tr-TR", 19 => "es-MX", 20 => "el-GR", _ => null }; } private static Dictionary LoadLocaleTable(string locale) { var entries = new Dictionary(StringComparer.Ordinal); var locales = BuildLocaleFallbacks(locale); foreach (var loc in locales) LoadLocaleFiles(entries, loc); return entries; } private static List BuildLocaleFallbacks(string locale) { var list = new List(); void Add(string value) { if (string.IsNullOrWhiteSpace(value)) return; if (!list.Contains(value, StringComparer.OrdinalIgnoreCase)) list.Add(value); } Add(locale); Add(locale.Replace('_', '-')); Add(locale.Replace('-', '_')); Add("en-GB"); Add("en-US"); return list; } private static void LoadLocaleFiles(Dictionary entries, string locale) { foreach (var modsPath in GetModsRoots()) { foreach (var modDir in Directory.EnumerateDirectories(modsPath)) { var assetsDir = Path.Combine(modDir, "assets"); if (!Directory.Exists(assetsDir)) continue; foreach (var nsDir in Directory.EnumerateDirectories(assetsDir)) { var langFile = Path.Combine(nsDir, "lang", $"{locale}.lang"); if (!File.Exists(langFile)) continue; ParseLangFile(langFile, entries); } } } } private static void ParseLangFile(string path, Dictionary entries) { foreach (var rawLine in File.ReadLines(path)) { var line = rawLine.Trim(); if (line.Length == 0 || line.StartsWith('#')) continue; int idx = line.IndexOf('='); if (idx <= 0 || idx >= line.Length - 1) continue; string key = line[..idx].Trim(); string value = line[(idx + 1)..].Trim(); if (key.Length == 0) continue; entries[key] = value; } } private static List GetModsRoots() { var roots = new List(); void AddCandidate(string? path) { if (string.IsNullOrWhiteSpace(path)) return; string full; try { full = Path.GetFullPath(path); } catch { return; } if (!Directory.Exists(full)) return; if (!roots.Contains(full, StringComparer.OrdinalIgnoreCase)) roots.Add(full); } AddCandidate(GetNativeModsPath()); var baseDir = AppContext.BaseDirectory; AddCandidate(Path.Combine(baseDir, "mods")); AddCandidate(Path.Combine(baseDir, "..", "mods")); AddCandidate(Path.Combine(baseDir, "..", "..", "mods")); var cwd = Directory.GetCurrentDirectory(); AddCandidate(Path.Combine(cwd, "mods")); AddCandidate(Path.Combine(cwd, "..", "mods")); return roots; } private static string? GetNativeModsPath() { try { var ptr = NativeInterop.native_get_mods_path(); if (ptr == nint.Zero) return null; return Marshal.PtrToStringAnsi(ptr); } catch { return null; } } }