mirror of
https://github.com/MaSzyna-EU07/StarterNG.git
synced 2026-10-29 02:57:29 +01:00
207 lines
6.6 KiB
C#
207 lines
6.6 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.ComponentModel;
|
|
using System.IO;
|
|
using System.Xml;
|
|
using StarterNG.Application.Abstractions;
|
|
|
|
namespace StarterNG.Services;
|
|
|
|
/// <summary>
|
|
/// Loads the translation table from startercfg/lang/*.xml and exposes it as
|
|
/// <see cref="ILocalizedStrings"/>. Stays a change-notifying class because the
|
|
/// XAML binds to it directly as a static source.
|
|
/// </summary>
|
|
public class LocalizationService : INotifyPropertyChanged, ILocalizedStrings
|
|
{
|
|
public static string LangDirectory =>
|
|
Path.Combine(AppContext.BaseDirectory, "startercfg", "lang");
|
|
|
|
private readonly IDiagnosticsLog _log;
|
|
|
|
private Dictionary<string, string> _strings = new();
|
|
|
|
public LocalizationService(IDiagnosticsLog log)
|
|
{
|
|
_log = log;
|
|
}
|
|
|
|
public event PropertyChangedEventHandler? PropertyChanged;
|
|
|
|
public event Action? LanguageChanged;
|
|
|
|
public string CurrentLanguage { get; private set; } = "English";
|
|
|
|
public string CurrentLangCode { get; private set; } = "en";
|
|
|
|
private readonly HashSet<string> _reportedMisses = new(StringComparer.Ordinal);
|
|
|
|
/// <summary>
|
|
/// Keys with this suffix hold the optional one-line explanation shown under a
|
|
/// setting. They are only authored in en/pl, so a miss yields an empty string
|
|
/// (which collapses the description line) instead of the raw key, and is not
|
|
/// reported as a translation gap.
|
|
/// </summary>
|
|
private const string OptionalSuffix = "Desc";
|
|
|
|
public string this[string key]
|
|
{
|
|
get
|
|
{
|
|
if (_strings.TryGetValue(key, out var value))
|
|
return value;
|
|
|
|
if (key.EndsWith(OptionalSuffix, StringComparison.Ordinal))
|
|
return string.Empty;
|
|
|
|
lock (_reportedMisses)
|
|
if (_reportedMisses.Add(key))
|
|
_log.Log($"lang/{CurrentLangCode}: missing key \"{key}\"");
|
|
|
|
return key;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Looks up a key that may legitimately be absent, without logging a miss.
|
|
/// Returns an empty string when the current language does not define it.
|
|
/// </summary>
|
|
public string Opt(string key) =>
|
|
_strings.TryGetValue(key, out var value) ? value : string.Empty;
|
|
|
|
public readonly record struct LanguageInfo(string Code, string Name, string Path);
|
|
|
|
public static IReadOnlyList<LanguageInfo> AvailableLanguages(IDiagnosticsLog? log = null)
|
|
{
|
|
var list = new List<LanguageInfo>();
|
|
if (!Directory.Exists(LangDirectory))
|
|
return list;
|
|
|
|
foreach (var path in Directory.EnumerateFiles(LangDirectory, "*.xml"))
|
|
{
|
|
try
|
|
{
|
|
var (code, name) = ReadHeader(path);
|
|
if (!string.IsNullOrEmpty(code))
|
|
list.Add(new LanguageInfo(code, string.IsNullOrEmpty(name) ? code : name, path));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log?.Log($"lang/{Path.GetFileName(path)}", ex);
|
|
}
|
|
}
|
|
return list;
|
|
}
|
|
|
|
public string Load(string codeOrName)
|
|
{
|
|
var languages = AvailableLanguages(_log);
|
|
|
|
LanguageInfo? match = null;
|
|
foreach (var lang in languages)
|
|
{
|
|
if (string.Equals(lang.Code, codeOrName, StringComparison.OrdinalIgnoreCase) ||
|
|
string.Equals(lang.Name, codeOrName, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
match = lang;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (match is null)
|
|
{
|
|
foreach (var lang in languages)
|
|
if (string.Equals(lang.Code, "en", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
match = lang;
|
|
break;
|
|
}
|
|
if (match is null && languages.Count > 0)
|
|
match = languages[0];
|
|
}
|
|
|
|
if (match is { } info)
|
|
LoadFile(info.Path, info.Code, info.Name);
|
|
else
|
|
{
|
|
|
|
_strings = new Dictionary<string, string>();
|
|
CurrentLangCode = "en";
|
|
CurrentLanguage = "English";
|
|
}
|
|
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Item[]"));
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(string.Empty));
|
|
LanguageChanged?.Invoke();
|
|
return CurrentLanguage;
|
|
}
|
|
|
|
private void LoadFile(string path, string code, string name)
|
|
{
|
|
var dict = new Dictionary<string, string>(StringComparer.Ordinal);
|
|
|
|
// A translation which has not caught up with new settings shows them in English
|
|
// rather than as their raw keys. Descriptions stay out: a missing one collapses.
|
|
string english = Path.Combine(Path.GetDirectoryName(path) ?? LangDirectory, "en.xml");
|
|
if (!string.Equals(code, "en", StringComparison.OrdinalIgnoreCase) && File.Exists(english))
|
|
{
|
|
try
|
|
{
|
|
ReadStrings(english, dict);
|
|
foreach (var key in new List<string>(dict.Keys))
|
|
if (key.EndsWith(OptionalSuffix, StringComparison.Ordinal))
|
|
dict.Remove(key);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_log.Log($"lang/{Path.GetFileName(english)}", ex);
|
|
}
|
|
}
|
|
|
|
ReadStrings(path, dict);
|
|
|
|
_strings = dict;
|
|
lock (_reportedMisses)
|
|
_reportedMisses.Clear();
|
|
CurrentLangCode = code;
|
|
CurrentLanguage = name;
|
|
}
|
|
|
|
private static void ReadStrings(string path, Dictionary<string, string> dict)
|
|
{
|
|
var settings = new XmlReaderSettings { IgnoreComments = true, IgnoreWhitespace = true };
|
|
using var reader = XmlReader.Create(path, settings);
|
|
|
|
while (!reader.EOF)
|
|
{
|
|
if (reader.NodeType == XmlNodeType.Element && reader.Name == "String")
|
|
{
|
|
var key = reader.GetAttribute("key");
|
|
var value = reader.ReadElementContentAsString();
|
|
if (!string.IsNullOrEmpty(key))
|
|
dict[key] = value;
|
|
}
|
|
else
|
|
{
|
|
reader.Read();
|
|
}
|
|
}
|
|
}
|
|
|
|
private static (string code, string name) ReadHeader(string path)
|
|
{
|
|
var settings = new XmlReaderSettings { IgnoreComments = true, IgnoreWhitespace = true };
|
|
using var reader = XmlReader.Create(path, settings);
|
|
|
|
while (reader.Read())
|
|
{
|
|
if (reader.NodeType == XmlNodeType.Element && reader.Name == "Language")
|
|
{
|
|
return (reader.GetAttribute("code") ?? "",
|
|
reader.GetAttribute("name") ?? "");
|
|
}
|
|
}
|
|
return ("", "");
|
|
}
|
|
}
|