From 6c8c98ed7d369f76a387b6543447cffb5a7b06c2 Mon Sep 17 00:00:00 2001 From: ParantezTech Date: Tue, 14 Jul 2026 02:07:31 +0300 Subject: [PATCH] [GUI] now languages are embedded in a single assembly --- src/SharpEmu.CLI/SharpEmu.CLI.csproj | 2 +- src/SharpEmu.GUI/Localization.cs | 131 +++++++++++++++++++-------- src/SharpEmu.GUI/SharpEmu.GUI.csproj | 8 ++ 3 files changed, 102 insertions(+), 39 deletions(-) diff --git a/src/SharpEmu.CLI/SharpEmu.CLI.csproj b/src/SharpEmu.CLI/SharpEmu.CLI.csproj index 06d18f3..8cd33a5 100644 --- a/src/SharpEmu.CLI/SharpEmu.CLI.csproj +++ b/src/SharpEmu.CLI/SharpEmu.CLI.csproj @@ -48,7 +48,7 @@ SPDX-License-Identifier: GPL-2.0-or-later LICENSE.txt False - + PreserveNewest Languages\%(Filename)%(Extension) False diff --git a/src/SharpEmu.GUI/Localization.cs b/src/SharpEmu.GUI/Localization.cs index eb81850..d83f737 100644 --- a/src/SharpEmu.GUI/Localization.cs +++ b/src/SharpEmu.GUI/Localization.cs @@ -6,10 +6,11 @@ using System.Text.Json; namespace SharpEmu.GUI; /// -/// Loads UI strings for the launcher from Languages/<code>.json next to the -/// executable. The files are plain, pretty-printed JSON (a flat key/value -/// map) so they stay easy to open and translate by hand; a missing or -/// unreadable key falls back to the key name itself. +/// Loads UI strings for the launcher. Every language ships embedded in the +/// assembly (see SharpEmu.GUI.csproj) so a release build is fully +/// self-contained; an optional Languages/<code>.json file next to the +/// executable overrides the embedded copy for that code, so a translation +/// fix or a brand-new language never needs a rebuild. /// public sealed class Localization { @@ -17,13 +18,16 @@ public sealed class Localization public sealed record LanguageInfo(string Code, string NativeName); + private const string EmbeddedResourcePrefix = "Languages."; + private const string EmbeddedResourceSuffix = ".json"; + private Dictionary _strings = new(); private Localization() { } - /// Directory holding the *.json language files; user-editable. + /// Directory holding optional *.json language overrides, next to the executable. public static string LanguagesDirectory => Path.Combine(AppContext.BaseDirectory, "Languages"); public string CurrentCode { get; private set; } = "en"; @@ -32,32 +36,73 @@ public sealed class Localization public string Format(string key, params object?[] args) => string.Format(Get(key), args); - /// Languages discovered under , sorted by code. + /// + /// Languages available either embedded in the binary or as a loose + /// override file, sorted by code. A loose file's declared name wins when + /// the same code exists in both places. + /// public List DiscoverLanguages() { - var languages = new List(); + var languages = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var code in EmbeddedLanguageCodes()) + { + using var stream = OpenEmbeddedLanguageStream(code); + if (stream is not null) + { + languages[code] = new LanguageInfo(code, ReadLanguageName(stream) ?? code); + } + } + try { foreach (var file in Directory.EnumerateFiles(LanguagesDirectory, "*.json")) { var code = Path.GetFileNameWithoutExtension(file); - languages.Add(new LanguageInfo(code, ReadLanguageName(file) ?? code)); + using var stream = File.OpenRead(file); + languages[code] = new LanguageInfo(code, ReadLanguageName(stream) ?? code); } } catch (Exception) { - // Missing Languages directory: no languages to offer. + // No loose Languages directory: the embedded languages still stand. } - languages.Sort((a, b) => string.CompareOrdinal(a.Code, b.Code)); - return languages; + var result = languages.Values.ToList(); + result.Sort((a, b) => string.CompareOrdinal(a.Code, b.Code)); + return result; } - private static string? ReadLanguageName(string path) + /// Loads a language by code (e.g. "en"): a loose override file first, then the embedded copy. + public void Load(string code) + { + if (!TryLoadLooseFile(code) && !TryLoadEmbedded(code) && + !string.Equals(code, "en", StringComparison.OrdinalIgnoreCase)) + { + _ = TryLoadLooseFile("en") || TryLoadEmbedded("en"); + } + } + + private static IEnumerable EmbeddedLanguageCodes() + { + foreach (var name in typeof(Localization).Assembly.GetManifestResourceNames()) + { + if (name.StartsWith(EmbeddedResourcePrefix, StringComparison.Ordinal) && + name.EndsWith(EmbeddedResourceSuffix, StringComparison.Ordinal)) + { + yield return name[EmbeddedResourcePrefix.Length..^EmbeddedResourceSuffix.Length]; + } + } + } + + private static Stream? OpenEmbeddedLanguageStream(string code) => + typeof(Localization).Assembly.GetManifestResourceStream($"{EmbeddedResourcePrefix}{code}{EmbeddedResourceSuffix}"); + + private static string? ReadLanguageName(Stream stream) { try { - using var document = JsonDocument.Parse(File.ReadAllText(path)); + using var document = JsonDocument.Parse(stream); if (document.RootElement.TryGetProperty("_languageName", out var name) && name.ValueKind == JsonValueKind.String) { @@ -66,44 +111,54 @@ public sealed class Localization } catch (Exception) { - // Malformed file: fall back to the file's code as its display name. + // Malformed file: fall back to the code as its own display name. } return null; } - /// Loads a language by file code (e.g. "en"), falling back to English. - public void Load(string code) - { - if (!TryLoadFile(code) && !string.Equals(code, "en", StringComparison.OrdinalIgnoreCase)) - { - TryLoadFile("en"); - } - } - - private bool TryLoadFile(string code) + private bool TryLoadLooseFile(string code) { try { var path = Path.Combine(LanguagesDirectory, $"{code}.json"); - if (!File.Exists(path)) - { - return false; - } - - var loaded = JsonSerializer.Deserialize>(File.ReadAllText(path)); - if (loaded is null) - { - return false; - } - - _strings = loaded; - CurrentCode = code; - return true; + return File.Exists(path) && TryLoad(code, File.ReadAllText(path)); } catch (Exception) { return false; } } + + private bool TryLoadEmbedded(string code) + { + try + { + using var stream = OpenEmbeddedLanguageStream(code); + if (stream is null) + { + return false; + } + + using var reader = new StreamReader(stream); + return TryLoad(code, reader.ReadToEnd()); + } + catch (Exception) + { + return false; + } + } + + private bool TryLoad(string code, string json) + { + var loaded = JsonSerializer.Deserialize>(json); + if (loaded is null) + { + return false; + } + + _strings = loaded; + CurrentCode = code; + return true; + } } diff --git a/src/SharpEmu.GUI/SharpEmu.GUI.csproj b/src/SharpEmu.GUI/SharpEmu.GUI.csproj index ffeaa28..c16f714 100644 --- a/src/SharpEmu.GUI/SharpEmu.GUI.csproj +++ b/src/SharpEmu.GUI/SharpEmu.GUI.csproj @@ -30,6 +30,14 @@ SPDX-License-Identifier: GPL-2.0-or-later + + + Languages.%(Filename)%(Extension) + + + + +