/************************************************************************* * ModernUO * * Copyright 2019-2026 - ModernUO Development Team * * Email: hi@modernuo.com * * File: Localization.cs * * * * This program is free software: you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3 of the License, or * * (at your option) any later version. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * *************************************************************************/ using System; using System.Buffers.Binary; using System.Collections.Generic; using System.IO; using System.Runtime.CompilerServices; using System.Text; using Server.Buffers; namespace Server; public static class Localization { private const bool _loadLocalizationOnStartup = false; public const string FallbackLanguage = "enu"; private static Dictionary _fallbackEntries; private static readonly Dictionary> _localizations = new(); public static void Configure() { if (_loadLocalizationOnStartup) { foreach (var file in Core.FindDataFileByPattern("cliloc.*")) { var fi = new FileInfo(file); LoadClilocs(fi.Extension.ToLowerInvariant(), file); } } } public static void Add(string lang, int number, string text) { lang = lang?.ToLower() ?? FallbackLanguage; var entry = new LocalizationEntry(lang, number, text); if (!_localizations.TryGetValue(lang, out var entries)) { entries = new Dictionary(); _localizations[lang] = entries; if (lang == FallbackLanguage) { _fallbackEntries ??= entries; } } entries.Add(number, entry); } public static bool Remove(string lang, int number) { lang = lang?.ToLower() ?? FallbackLanguage; if (!_localizations.TryGetValue(lang, out var entries) || !entries.Remove(number)) { return false; } if (entries.Count == 0) { _localizations.Remove(lang); } return true; } public static void Clear() { _localizations.Clear(); _fallbackEntries = null; } public static Dictionary LoadClilocs(string lang) => LoadClilocs(lang, Core.FindDataFile($"cliloc.{lang}", false)); private static Dictionary LoadClilocs(string lang, string file) { lang = lang?.ToLower() ?? FallbackLanguage; var entries = _localizations[lang] = new Dictionary(); if (lang == FallbackLanguage) { _fallbackEntries = entries; } if (File.Exists(file)) { using var fs = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read); Span header = stackalloc byte[6]; _ = fs.Read(header); byte[] data; BufferReader br; if (BinaryPrimitives.ReadInt32LittleEndian(header) != 2 || BinaryPrimitives.ReadInt16LittleEndian(header[4..]) != 1) { // Skip header fs.Position = 4; data = BwtDecompress.Decompress(fs, (int)fs.Length - 4); br = new BufferReader(data); var header2 = br.ReadInt(); // Header 2 var header1 = br.ReadShort(); // Header 1 if (header2 != 2 || header1 != 1) { throw new Exception($"Invalid cliloc header in {file}"); } } else { data = GC.AllocateUninitializedArray((int)fs.Length - 6); _ = fs.Read(data); br = new BufferReader(data); } byte[] buffer = null; while (br.Position < data.Length) { var number = br.ReadInt(); var flag = br.ReadByte(); // Original, Custom, Modified var length = br.ReadShort(); if (buffer == null || buffer.Length < length) { buffer = GC.AllocateUninitializedArray(length); } br.Read(buffer.AsSpan(0, length)); var text = Encoding.UTF8.GetString(buffer.AsSpan(0, length)); entries[number] = new LocalizationEntry(lang, number, text); } } return entries; } /// /// Returns the original text for a localization entry. /// /// Localization number /// Language in ISO 639‑2 format /// Original text for the localization entry public static string GetText(int number, string lang = FallbackLanguage) => TryGetLocalization(lang, number, out var entry) ? entry.Text : null; /// /// Gets a localization entry using the . /// /// Localization number /// Localization entry retrieved /// True if the entry exists, otherwise false. public static bool TryGetLocalization(int number, out LocalizationEntry entry) => TryGetLocalization(FallbackLanguage, number, out entry); /// /// Gets a localization entry. /// /// Language in ISO 639-2 format /// Localization number /// Localization entry retrieved /// True if the entry exists, otherwise false. public static bool TryGetLocalization(string lang, int number, out LocalizationEntry entry) { lang = lang?.ToLower() ?? FallbackLanguage; if (lang != FallbackLanguage) { if (!_localizations.TryGetValue(lang, out var entries)) { entries = LoadClilocs(lang); } if (entries.TryGetValue(number, out entry)) { return true; } } _fallbackEntries ??= LoadClilocs(FallbackLanguage); return _fallbackEntries.TryGetValue(number, out entry); } /// /// Creates a formatted string of the localization entry using the specified language. /// Uses string interpolation under the hood. This method is preferably relative to the object array method signature. /// Example: /// Localization.Format(1073841, "jpn", $"{totalItems}{maxItems}{totalWeight}"); /// /// Language in ISO 639-2 format /// Localization number /// interpolated string handler used by the compiler as a string builder during compilation /// A copy of the localization text where the placeholder arguments have been replaced with string representations of the provided interpolation arguments public static PooledArraySpanFormattable Format( int number, string lang, [InterpolatedStringHandlerArgument("number", "lang")] ref LocalizationEntry.LocalizationInterpolationHandler handler ) { var chars = handler.ToPooledArray(out var length); handler = default; // Defensive clear return new PooledArraySpanFormattable(chars, length); } /// /// Creates a formatted string of the localization entry using the . /// Uses string interpolation under the hood. This method is preferably relative to the object array method signature. /// Example: /// Localization.Format(1073841, $"{totalItems}{maxItems}{totalWeight}"); /// /// Localization number /// interpolated string handler used by the compiler as a string builder during compilation /// A copy of the localization text where the placeholder arguments have been replaced with string representations of the provided interpolation arguments public static PooledArraySpanFormattable Format( int number, [InterpolatedStringHandlerArgument("number")] ref LocalizationEntry.LocalizationInterpolationHandler handler ) { var chars = handler.ToPooledArray(out var length); handler = default; // Defensive clear return new PooledArraySpanFormattable(chars, length); } }