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)
+
+
+
+
+