mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-15 23:31:07 +00:00
[GUI] now languages are embedded in a single assembly
This commit is contained in:
@@ -48,7 +48,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<TargetPath>LICENSE.txt</TargetPath>
|
<TargetPath>LICENSE.txt</TargetPath>
|
||||||
<Visible>False</Visible>
|
<Visible>False</Visible>
|
||||||
</Content>
|
</Content>
|
||||||
<Content Include="..\SharpEmu.GUI\Languages\*.json">
|
<Content Include="..\SharpEmu.GUI\Languages\*.json" Condition="'$(Configuration)' != 'Release'">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
<TargetPath>Languages\%(Filename)%(Extension)</TargetPath>
|
<TargetPath>Languages\%(Filename)%(Extension)</TargetPath>
|
||||||
<Visible>False</Visible>
|
<Visible>False</Visible>
|
||||||
|
|||||||
@@ -6,10 +6,11 @@ using System.Text.Json;
|
|||||||
namespace SharpEmu.GUI;
|
namespace SharpEmu.GUI;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Loads UI strings for the launcher from Languages/<code>.json next to the
|
/// Loads UI strings for the launcher. Every language ships embedded in the
|
||||||
/// executable. The files are plain, pretty-printed JSON (a flat key/value
|
/// assembly (see SharpEmu.GUI.csproj) so a release build is fully
|
||||||
/// map) so they stay easy to open and translate by hand; a missing or
|
/// self-contained; an optional Languages/<code>.json file next to the
|
||||||
/// unreadable key falls back to the key name itself.
|
/// executable overrides the embedded copy for that code, so a translation
|
||||||
|
/// fix or a brand-new language never needs a rebuild.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class Localization
|
public sealed class Localization
|
||||||
{
|
{
|
||||||
@@ -17,13 +18,16 @@ public sealed class Localization
|
|||||||
|
|
||||||
public sealed record LanguageInfo(string Code, string NativeName);
|
public sealed record LanguageInfo(string Code, string NativeName);
|
||||||
|
|
||||||
|
private const string EmbeddedResourcePrefix = "Languages.";
|
||||||
|
private const string EmbeddedResourceSuffix = ".json";
|
||||||
|
|
||||||
private Dictionary<string, string> _strings = new();
|
private Dictionary<string, string> _strings = new();
|
||||||
|
|
||||||
private Localization()
|
private Localization()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Directory holding the *.json language files; user-editable.</summary>
|
/// <summary>Directory holding optional *.json language overrides, next to the executable.</summary>
|
||||||
public static string LanguagesDirectory => Path.Combine(AppContext.BaseDirectory, "Languages");
|
public static string LanguagesDirectory => Path.Combine(AppContext.BaseDirectory, "Languages");
|
||||||
|
|
||||||
public string CurrentCode { get; private set; } = "en";
|
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);
|
public string Format(string key, params object?[] args) => string.Format(Get(key), args);
|
||||||
|
|
||||||
/// <summary>Languages discovered under <see cref="LanguagesDirectory"/>, sorted by code.</summary>
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
public List<LanguageInfo> DiscoverLanguages()
|
public List<LanguageInfo> DiscoverLanguages()
|
||||||
{
|
{
|
||||||
var languages = new List<LanguageInfo>();
|
var languages = new Dictionary<string, LanguageInfo>(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
|
try
|
||||||
{
|
{
|
||||||
foreach (var file in Directory.EnumerateFiles(LanguagesDirectory, "*.json"))
|
foreach (var file in Directory.EnumerateFiles(LanguagesDirectory, "*.json"))
|
||||||
{
|
{
|
||||||
var code = Path.GetFileNameWithoutExtension(file);
|
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)
|
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));
|
var result = languages.Values.ToList();
|
||||||
return languages;
|
result.Sort((a, b) => string.CompareOrdinal(a.Code, b.Code));
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string? ReadLanguageName(string path)
|
/// <summary>Loads a language by code (e.g. "en"): a loose override file first, then the embedded copy.</summary>
|
||||||
|
public void Load(string code)
|
||||||
|
{
|
||||||
|
if (!TryLoadLooseFile(code) && !TryLoadEmbedded(code) &&
|
||||||
|
!string.Equals(code, "en", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
_ = TryLoadLooseFile("en") || TryLoadEmbedded("en");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<string> 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
|
try
|
||||||
{
|
{
|
||||||
using var document = JsonDocument.Parse(File.ReadAllText(path));
|
using var document = JsonDocument.Parse(stream);
|
||||||
if (document.RootElement.TryGetProperty("_languageName", out var name) &&
|
if (document.RootElement.TryGetProperty("_languageName", out var name) &&
|
||||||
name.ValueKind == JsonValueKind.String)
|
name.ValueKind == JsonValueKind.String)
|
||||||
{
|
{
|
||||||
@@ -66,44 +111,54 @@ public sealed class Localization
|
|||||||
}
|
}
|
||||||
catch (Exception)
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Loads a language by file code (e.g. "en"), falling back to English.</summary>
|
private bool TryLoadLooseFile(string code)
|
||||||
public void Load(string code)
|
|
||||||
{
|
|
||||||
if (!TryLoadFile(code) && !string.Equals(code, "en", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
TryLoadFile("en");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryLoadFile(string code)
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var path = Path.Combine(LanguagesDirectory, $"{code}.json");
|
var path = Path.Combine(LanguagesDirectory, $"{code}.json");
|
||||||
if (!File.Exists(path))
|
return File.Exists(path) && TryLoad(code, File.ReadAllText(path));
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var loaded = JsonSerializer.Deserialize<Dictionary<string, string>>(File.ReadAllText(path));
|
|
||||||
if (loaded is null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_strings = loaded;
|
|
||||||
CurrentCode = code;
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
return false;
|
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<Dictionary<string, string>>(json);
|
||||||
|
if (loaded is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_strings = loaded;
|
||||||
|
CurrentCode = code;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,14 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<AvaloniaResource Include="..\..\assets\images\SharpEmu.ico" Link="Assets/SharpEmu.ico" />
|
<AvaloniaResource Include="..\..\assets\images\SharpEmu.ico" Link="Assets/SharpEmu.ico" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<EmbeddedResource Include="Languages\*.json">
|
||||||
|
<LogicalName>Languages.%(Filename)%(Extension)</LogicalName>
|
||||||
|
</EmbeddedResource>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- The controller readers (DualSense raw HID + Xbox XInput) are shared
|
<!-- The controller readers (DualSense raw HID + Xbox XInput) are shared
|
||||||
with the emulator's pad HLE. They are dependency-free, so they are
|
with the emulator's pad HLE. They are dependency-free, so they are
|
||||||
compiled in directly rather than pulling a reference to all of
|
compiled in directly rather than pulling a reference to all of
|
||||||
|
|||||||
Reference in New Issue
Block a user