/*************************************************************************
* 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);
}
}