Use 1 locales file instead of individual files for each langauge. This makes it easier to keep track of what is missing. The PR will automatically fix missing locales and throw an error if anything is incorrect, by running the emulator. That way the person adding a new locale or new language can just run the emulator once to populate all the fields, so they can easily begin translating.
205 lines
6.8 KiB
C#
205 lines
6.8 KiB
C#
using Ryujinx.Ava.UI.ViewModels;
|
|
using Ryujinx.Common;
|
|
using Ryujinx.Common.Logging;
|
|
using Ryujinx.Common.Utilities;
|
|
using Ryujinx.UI.Common.Configuration;
|
|
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text.Encodings.Web;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
using System.Text.Unicode;
|
|
|
|
namespace Ryujinx.Ava.Common.Locale
|
|
{
|
|
class LocaleManager : BaseModel
|
|
{
|
|
private const string DefaultLanguageCode = "en_US";
|
|
|
|
private readonly Dictionary<LocaleKeys, string> _localeStrings;
|
|
private Dictionary<LocaleKeys, string> _localeDefaultStrings;
|
|
private readonly ConcurrentDictionary<LocaleKeys, object[]> _dynamicValues;
|
|
private string _localeLanguageCode;
|
|
|
|
public static LocaleManager Instance { get; } = new();
|
|
public event Action LocaleChanged;
|
|
|
|
public LocaleManager()
|
|
{
|
|
_localeStrings = new Dictionary<LocaleKeys, string>();
|
|
_localeDefaultStrings = new Dictionary<LocaleKeys, string>();
|
|
_dynamicValues = new ConcurrentDictionary<LocaleKeys, object[]>();
|
|
|
|
Load();
|
|
}
|
|
|
|
private void Load()
|
|
{
|
|
var localeLanguageCode = !string.IsNullOrEmpty(ConfigurationState.Instance.UI.LanguageCode.Value) ?
|
|
ConfigurationState.Instance.UI.LanguageCode.Value : CultureInfo.CurrentCulture.Name.Replace('-', '_');
|
|
|
|
// Load en_US as default, if the target language translation is missing or incomplete.
|
|
LoadDefaultLanguage();
|
|
LoadLanguage(localeLanguageCode);
|
|
|
|
// Save whatever we ended up with.
|
|
if (Program.PreviewerDetached)
|
|
{
|
|
ConfigurationState.Instance.UI.LanguageCode.Value = _localeLanguageCode;
|
|
|
|
ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath);
|
|
}
|
|
}
|
|
|
|
public string this[LocaleKeys key]
|
|
{
|
|
get
|
|
{
|
|
// Check if the locale contains the key.
|
|
if (_localeStrings.TryGetValue(key, out string value))
|
|
{
|
|
// Check if the localized string needs to be formatted.
|
|
if (_dynamicValues.TryGetValue(key, out var dynamicValue))
|
|
try
|
|
{
|
|
return string.Format(value, dynamicValue);
|
|
}
|
|
catch
|
|
{
|
|
// If formatting failed use the default text instead.
|
|
if (_localeDefaultStrings.TryGetValue(key, out value))
|
|
try
|
|
{
|
|
return string.Format(value, dynamicValue);
|
|
}
|
|
catch
|
|
{
|
|
// If formatting the default text failed return the key.
|
|
return key.ToString();
|
|
}
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
// If the locale doesn't contain the key return the default one.
|
|
return _localeDefaultStrings.TryGetValue(key, out string defaultValue)
|
|
? defaultValue
|
|
: key.ToString(); // If the locale text doesn't exist return the key.
|
|
}
|
|
set
|
|
{
|
|
_localeStrings[key] = value;
|
|
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
|
|
public bool IsRTL() =>
|
|
_localeLanguageCode switch
|
|
{
|
|
"ar_SA" or "he_IL" => true,
|
|
_ => false
|
|
};
|
|
|
|
public static string FormatDynamicValue(LocaleKeys key, params object[] values)
|
|
=> Instance.UpdateAndGetDynamicValue(key, values);
|
|
|
|
public string UpdateAndGetDynamicValue(LocaleKeys key, params object[] values)
|
|
{
|
|
_dynamicValues[key] = values;
|
|
|
|
OnPropertyChanged("Item");
|
|
|
|
return this[key];
|
|
}
|
|
|
|
private void LoadDefaultLanguage()
|
|
{
|
|
_localeDefaultStrings = LoadJsonLanguage(DefaultLanguageCode);
|
|
}
|
|
|
|
public void LoadLanguage(string languageCode)
|
|
{
|
|
var locale = LoadJsonLanguage(languageCode);
|
|
|
|
if (locale == null)
|
|
{
|
|
_localeLanguageCode = DefaultLanguageCode;
|
|
locale = _localeDefaultStrings;
|
|
}
|
|
else
|
|
{
|
|
_localeLanguageCode = languageCode;
|
|
}
|
|
|
|
foreach ((LocaleKeys key, string val) in locale)
|
|
{
|
|
_localeStrings[key] = val;
|
|
}
|
|
|
|
OnPropertyChanged("Item");
|
|
|
|
LocaleChanged?.Invoke();
|
|
}
|
|
|
|
private static Dictionary<LocaleKeys, string> LoadJsonLanguage(string languageCode)
|
|
{
|
|
var localeStrings = new Dictionary<LocaleKeys, string>();
|
|
string fileData = EmbeddedResources.ReadAllText($"Ryujinx/Assets/locales.json");
|
|
|
|
if (fileData == null)
|
|
{
|
|
// We were unable to find file for that language code.
|
|
return null;
|
|
}
|
|
|
|
LocalesJson json = JsonHelper.Deserialize(fileData, LocalesJsonContext.Default.LocalesJson);
|
|
|
|
foreach (LocalesEntry locale in json.Locales)
|
|
{
|
|
if (locale.Translations.Count != json.Languages.Count)
|
|
{
|
|
Logger.Error?.Print(LogClass.UI, $"Locale key {{{locale.ID}}} is missing languages!");
|
|
throw new Exception("Missing locale data!");
|
|
}
|
|
|
|
if (Enum.TryParse<LocaleKeys>(locale.ID, out var localeKey))
|
|
{
|
|
if (locale.Translations.TryGetValue(languageCode, out string val) && val != "")
|
|
{
|
|
localeStrings[localeKey] = val;
|
|
}
|
|
else
|
|
{
|
|
locale.Translations.TryGetValue("en_US", out val);
|
|
localeStrings[localeKey] = val;
|
|
}
|
|
}
|
|
}
|
|
|
|
return localeStrings;
|
|
}
|
|
}
|
|
|
|
public struct LocalesJson
|
|
{
|
|
public List<string> Languages { get; set; }
|
|
public List<LocalesEntry> Locales { get; set; }
|
|
}
|
|
|
|
public struct LocalesEntry
|
|
{
|
|
public string ID { get; set; }
|
|
public Dictionary<string, string> Translations { get; set; }
|
|
}
|
|
|
|
[JsonSourceGenerationOptions(WriteIndented = true)]
|
|
[JsonSerializable(typeof(LocalesJson))]
|
|
internal partial class LocalesJsonContext : JsonSerializerContext { }
|
|
}
|