From ecb9d73ca07229dc192b84fbabfae0e2d374550f Mon Sep 17 00:00:00 2001 From: sewer56lol Date: Tue, 15 Aug 2023 01:18:41 +0100 Subject: [PATCH 01/11] Added: Basic Localization Test --- src/NexusMods.App.UI/App.axaml.cs | 6 +- .../Buttons/Download/DownloadButtonView.axaml | 6 +- .../Game/GameLeftMenuDesignViewModel.cs | 1 + .../Home/HomeLeftMenuDesignViewModel.cs | 7 +- .../LeftMenu/Home/HomeLeftMenuViewModel.cs | 7 +- src/NexusMods.App.UI/NexusMods.App.UI.csproj | 12 ++++ .../Resources/Language.Designer.cs | 66 +++++++++++++++++++ .../Resources/Language.pl.resx | 23 +++++++ src/NexusMods.App.UI/Resources/Language.resx | 30 +++++++++ 9 files changed, 148 insertions(+), 10 deletions(-) create mode 100644 src/NexusMods.App.UI/Resources/Language.Designer.cs create mode 100644 src/NexusMods.App.UI/Resources/Language.pl.resx create mode 100644 src/NexusMods.App.UI/Resources/Language.resx diff --git a/src/NexusMods.App.UI/App.axaml.cs b/src/NexusMods.App.UI/App.axaml.cs index c603e2613b..c9277c38a0 100644 --- a/src/NexusMods.App.UI/App.axaml.cs +++ b/src/NexusMods.App.UI/App.axaml.cs @@ -1,8 +1,10 @@ +using System.Globalization; using Avalonia; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using NexusMods.App.UI.Resources; using NexusMods.App.UI.Windows; using ReactiveUI; using Splat; @@ -18,7 +20,7 @@ public App(IServiceProvider provider) { _provider = provider; } - + public override void Initialize() { AvaloniaXamlLoader.Load(this); @@ -26,6 +28,8 @@ public override void Initialize() public override void OnFrameworkInitializationCompleted() { + // TODO: Switch Language from Config + // Language.Culture = new CultureInfo("pl"); Locator.CurrentMutable.UnregisterCurrent(typeof(IViewLocator)); Locator.CurrentMutable.Register(() => _provider.GetRequiredService(), typeof(IViewLocator)); diff --git a/src/NexusMods.App.UI/Controls/Spine/Buttons/Download/DownloadButtonView.axaml b/src/NexusMods.App.UI/Controls/Spine/Buttons/Download/DownloadButtonView.axaml index d371ea8829..9608877930 100644 --- a/src/NexusMods.App.UI/Controls/Spine/Buttons/Download/DownloadButtonView.axaml +++ b/src/NexusMods.App.UI/Controls/Spine/Buttons/Download/DownloadButtonView.axaml @@ -1,4 +1,4 @@ - - - + + diff --git a/src/NexusMods.App.UI/LeftMenu/Game/GameLeftMenuDesignViewModel.cs b/src/NexusMods.App.UI/LeftMenu/Game/GameLeftMenuDesignViewModel.cs index 993a25a075..e26e87baed 100644 --- a/src/NexusMods.App.UI/LeftMenu/Game/GameLeftMenuDesignViewModel.cs +++ b/src/NexusMods.App.UI/LeftMenu/Game/GameLeftMenuDesignViewModel.cs @@ -2,6 +2,7 @@ using NexusMods.App.UI.Extensions; using NexusMods.App.UI.Icons; using NexusMods.App.UI.LeftMenu.Items; +using NexusMods.App.UI.Resources; using NexusMods.App.UI.RightContent; using NexusMods.DataModel.Games; using ReactiveUI.Fody.Helpers; diff --git a/src/NexusMods.App.UI/LeftMenu/Home/HomeLeftMenuDesignViewModel.cs b/src/NexusMods.App.UI/LeftMenu/Home/HomeLeftMenuDesignViewModel.cs index 8e8de22a23..e5347c0e3d 100644 --- a/src/NexusMods.App.UI/LeftMenu/Home/HomeLeftMenuDesignViewModel.cs +++ b/src/NexusMods.App.UI/LeftMenu/Home/HomeLeftMenuDesignViewModel.cs @@ -1,6 +1,7 @@ using System.Collections.ObjectModel; using NexusMods.App.UI.Icons; using NexusMods.App.UI.LeftMenu.Items; +using NexusMods.App.UI.Resources; using NexusMods.App.UI.RightContent; namespace NexusMods.App.UI.LeftMenu.Home; @@ -14,9 +15,9 @@ public HomeLeftMenuDesignViewModel() { var items = new ILeftMenuItemViewModel[] { - new IconViewModel { Name = "Newsfeed", Icon = IconType.News}, - new IconViewModel { Name = "My Games", Icon = IconType.Bookmark }, - new IconViewModel { Name = "Browse Games", Icon = IconType.Game } + new IconViewModel { Name = Language.Newsfeed, Icon = IconType.News}, + new IconViewModel { Name = Language.MyGames, Icon = IconType.Bookmark }, + new IconViewModel { Name = Language.BrowseGames, Icon = IconType.Game } }; Items = new ReadOnlyObservableCollection(new ObservableCollection(items)); } diff --git a/src/NexusMods.App.UI/LeftMenu/Home/HomeLeftMenuViewModel.cs b/src/NexusMods.App.UI/LeftMenu/Home/HomeLeftMenuViewModel.cs index b7af1439f2..af193de4a2 100644 --- a/src/NexusMods.App.UI/LeftMenu/Home/HomeLeftMenuViewModel.cs +++ b/src/NexusMods.App.UI/LeftMenu/Home/HomeLeftMenuViewModel.cs @@ -1,6 +1,7 @@ using System.Collections.ObjectModel; using NexusMods.App.UI.Icons; using NexusMods.App.UI.LeftMenu.Items; +using NexusMods.App.UI.Resources; using NexusMods.App.UI.RightContent; using NexusMods.App.UI.RightContent.MyGames; using ReactiveUI; @@ -20,17 +21,17 @@ public HomeLeftMenuViewModel(IMyGamesViewModel myGamesViewModel, IFoundGamesView { var items = new ILeftMenuItemViewModel[] { - new IconViewModel { Name = "Newsfeed", Icon = IconType.News, Activate = ReactiveCommand.Create( + new IconViewModel { Name = Language.Newsfeed, Icon = IconType.News, Activate = ReactiveCommand.Create( () => { RightContent = Initializers.IRightContent; })}, - new IconViewModel { Name = "My Games", Icon = IconType.Bookmark, Activate = ReactiveCommand.Create( + new IconViewModel { Name = Language.MyGames, Icon = IconType.Bookmark, Activate = ReactiveCommand.Create( () => { RightContent = myGamesViewModel; }) }, - new IconViewModel { Name = "Browse Games", Icon = IconType.Game, Activate = ReactiveCommand.Create( + new IconViewModel { Name = Language.BrowseGames, Icon = IconType.Game, Activate = ReactiveCommand.Create( () => { RightContent = foundGamesViewModel; diff --git a/src/NexusMods.App.UI/NexusMods.App.UI.csproj b/src/NexusMods.App.UI/NexusMods.App.UI.csproj index 7344a8cbef..b9e5057dbc 100644 --- a/src/NexusMods.App.UI/NexusMods.App.UI.csproj +++ b/src/NexusMods.App.UI/NexusMods.App.UI.csproj @@ -281,6 +281,11 @@ IMessageBoxOkCancelViewModel.cs + + True + True + Language.resx + @@ -307,4 +312,11 @@ + + + + ResXFileCodeGenerator + Language.Designer.cs + + diff --git a/src/NexusMods.App.UI/Resources/Language.Designer.cs b/src/NexusMods.App.UI/Resources/Language.Designer.cs new file mode 100644 index 0000000000..20664a40b3 --- /dev/null +++ b/src/NexusMods.App.UI/Resources/Language.Designer.cs @@ -0,0 +1,66 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace NexusMods.App.UI.Resources { + using System; + + + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [System.Diagnostics.DebuggerNonUserCodeAttribute()] + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Language { + + private static System.Resources.ResourceManager resourceMan; + + private static System.Globalization.CultureInfo resourceCulture; + + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Language() { + } + + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + internal static System.Resources.ResourceManager ResourceManager { + get { + if (object.Equals(null, resourceMan)) { + System.Resources.ResourceManager temp = new System.Resources.ResourceManager("NexusMods.App.UI.Resources.Language", typeof(Language).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + internal static System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + internal static string Newsfeed { + get { + return ResourceManager.GetString("Newsfeed", resourceCulture); + } + } + + internal static string MyGames { + get { + return ResourceManager.GetString("MyGames", resourceCulture); + } + } + + internal static string BrowseGames { + get { + return ResourceManager.GetString("BrowseGames", resourceCulture); + } + } + } +} diff --git a/src/NexusMods.App.UI/Resources/Language.pl.resx b/src/NexusMods.App.UI/Resources/Language.pl.resx new file mode 100644 index 0000000000..e97f82c371 --- /dev/null +++ b/src/NexusMods.App.UI/Resources/Language.pl.resx @@ -0,0 +1,23 @@ + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Przeglądaj Gry + + + Moje Gry + + + Newsy + + \ No newline at end of file diff --git a/src/NexusMods.App.UI/Resources/Language.resx b/src/NexusMods.App.UI/Resources/Language.resx new file mode 100644 index 0000000000..32556db9ba --- /dev/null +++ b/src/NexusMods.App.UI/Resources/Language.resx @@ -0,0 +1,30 @@ + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Newsfeed + + + My Games + + + Browse Games + + From b2f3c2613f63c5d15959cf6212cc52bdfaadf643 Mon Sep 17 00:00:00 2001 From: sewer56lol Date: Tue, 15 Aug 2023 04:00:55 +0100 Subject: [PATCH 02/11] Added: A language override feature and updated AppConfig.json --- src/NexusMods.App.UI/App.axaml.cs | 9 ++++++--- src/NexusMods.App.UI/ILauncherSettings.cs | 22 ++++++++++++++++++++++ src/NexusMods.App.UI/Services.cs | 8 +++++++- src/NexusMods.App/AppConfig.cs | 21 +++++++++++---------- src/NexusMods.App/AppConfig.json | 11 +++++++---- src/NexusMods.App/Program.cs | 1 - src/NexusMods.App/Services.cs | 4 ++-- 7 files changed, 55 insertions(+), 21 deletions(-) create mode 100644 src/NexusMods.App.UI/ILauncherSettings.cs diff --git a/src/NexusMods.App.UI/App.axaml.cs b/src/NexusMods.App.UI/App.axaml.cs index c9277c38a0..7c23dd4d8b 100644 --- a/src/NexusMods.App.UI/App.axaml.cs +++ b/src/NexusMods.App.UI/App.axaml.cs @@ -15,10 +15,12 @@ namespace NexusMods.App.UI; public class App : Application { private readonly IServiceProvider _provider; + private readonly ILauncherSettings _launcherSettings; - public App(IServiceProvider provider) + public App(IServiceProvider provider, ILauncherSettings launcherSettings) { _provider = provider; + _launcherSettings = launcherSettings; } public override void Initialize() @@ -28,8 +30,9 @@ public override void Initialize() public override void OnFrameworkInitializationCompleted() { - // TODO: Switch Language from Config - // Language.Culture = new CultureInfo("pl"); + if (!string.IsNullOrEmpty(_launcherSettings.LocaleOverride)) + Language.Culture = new CultureInfo(_launcherSettings.LocaleOverride); + Locator.CurrentMutable.UnregisterCurrent(typeof(IViewLocator)); Locator.CurrentMutable.Register(() => _provider.GetRequiredService(), typeof(IViewLocator)); diff --git a/src/NexusMods.App.UI/ILauncherSettings.cs b/src/NexusMods.App.UI/ILauncherSettings.cs new file mode 100644 index 0000000000..59e5e2ab93 --- /dev/null +++ b/src/NexusMods.App.UI/ILauncherSettings.cs @@ -0,0 +1,22 @@ +using JetBrains.Annotations; + +namespace NexusMods.App.UI; + +[PublicAPI] +public interface ILauncherSettings +{ + /// + /// Overrides the current locale of the application at startup. + /// + /// If this value is empty, the locale will not be overwritten. + public string LocaleOverride { get; set; } +} + +[PublicAPI] +public class LauncherSettings : ILauncherSettings +{ + public string LocaleOverride { get; set; } = string.Empty; + + // ReSharper disable once EmptyConstructor + public LauncherSettings() { } +} diff --git a/src/NexusMods.App.UI/Services.cs b/src/NexusMods.App.UI/Services.cs index 80244dce6d..f45e04c8c7 100644 --- a/src/NexusMods.App.UI/Services.cs +++ b/src/NexusMods.App.UI/Services.cs @@ -34,6 +34,7 @@ using NexusMods.App.UI.RightContent.MyGames; using NexusMods.App.UI.Routing; using NexusMods.App.UI.Windows; +using NexusMods.Common; using ReactiveUI; using DownloadGameNameView = NexusMods.App.UI.RightContent.DownloadGrid.Columns.DownloadGameName.DownloadGameNameView; using DownloadNameView = NexusMods.App.UI.RightContent.LoadoutGrid.Columns.DownloadName.DownloadNameView; @@ -51,8 +52,13 @@ namespace NexusMods.App.UI; public static class Services { // ReSharper disable once InconsistentNaming - public static IServiceCollection AddUI(this IServiceCollection c) + public static IServiceCollection AddUI(this IServiceCollection c, ILauncherSettings? settings) { + if (settings == null) + c.AddSingleton(); + else + c.AddSingleton(settings); + return c.AddTransient() // Services diff --git a/src/NexusMods.App/AppConfig.cs b/src/NexusMods.App/AppConfig.cs index fb3d212c91..c1c8a960ea 100644 --- a/src/NexusMods.App/AppConfig.cs +++ b/src/NexusMods.App/AppConfig.cs @@ -1,8 +1,10 @@ -using NexusMods.DataModel; +using NexusMods.App.UI; +using NexusMods.DataModel; using NexusMods.FileExtractor; using NexusMods.Networking.HttpDownloader; using NexusMods.Paths; using NexusMods.Paths.Utilities; +// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global namespace NexusMods.App; @@ -11,14 +13,12 @@ namespace NexusMods.App; /// public class AppConfig { - private readonly IFileSystem _fileSystem; - public AppConfig() { - _fileSystem = FileSystem.Shared; - DataModelSettings = new DataModelSettings(_fileSystem); - FileExtractorSettings = new FileExtractorSettings(_fileSystem); - LoggingSettings = new LoggingSettings(_fileSystem); + var fileSystem = FileSystem.Shared; + DataModelSettings = new DataModelSettings(fileSystem); + FileExtractorSettings = new FileExtractorSettings(fileSystem); + LoggingSettings = new LoggingSettings(fileSystem); } /* Default Value Rules: @@ -47,6 +47,7 @@ Individual settings objects must implement a `Sanitize` function to ensure these public FileExtractorSettings FileExtractorSettings { get; set; } public HttpDownloaderSettings HttpDownloaderSettings { get; set; } = new(); public LoggingSettings LoggingSettings { get; set; } + public LauncherSettings LauncherSettings { get; set; } = new(); /// /// Sanitizes the config; e.g. @@ -81,7 +82,7 @@ public class LoggingSettings : ILoggingSettings { private const string LogFileName = "nexusmods.app.current.log"; private const string LogFileNameTemplate = "nexusmods.app.{##}.log"; - + /// public ConfigurationPath FilePath { get; set; } @@ -90,7 +91,7 @@ public class LoggingSettings : ILoggingSettings /// public int MaxArchivedFiles { get; set; } - + /// /// Default constructor for serialization. /// @@ -99,7 +100,7 @@ public LoggingSettings() : this(FileSystem.Shared) {} /// /// Creates the default logger with logs stored in the entry directory. /// - /// The base directory to use. + /// The FileSystem implementation to use. public LoggingSettings(IFileSystem fileSystem) { var baseFolder = fileSystem.GetKnownPath(KnownPath.EntryDirectory); diff --git a/src/NexusMods.App/AppConfig.json b/src/NexusMods.App/AppConfig.json index 2fa29d1f8a..c8cb51917f 100644 --- a/src/NexusMods.App/AppConfig.json +++ b/src/NexusMods.App/AppConfig.json @@ -1,19 +1,19 @@ { "DataModelSettings": { + "UseInMemoryDataModel": false, "DataStoreFilePath": "{EntryFolder}/DataModel.sqlite", "IpcDataStoreFilePath": "{EntryFolder}/DataModel_IPC.sqlite", "ArchiveLocations": [ "{EntryFolder}/Archives" ], - "MaxHashingJobs": -1, - "LoadoutDeploymentJobs": -1, - "MaxHashingThroughputBytesPerSecond": -1 + "MaxHashingJobs": 24, + "LoadoutDeploymentJobs": 24, + "MaxHashingThroughputBytesPerSecond": 0 }, "FileExtractorSettings": { "TempFolderLocation": "{EntryFolder}/Temp" }, "HttpDownloaderSettings": { - "ChunkCount": 4, "WriteQueueLength": 16, "MinCancelAge": 500, "CancelSpeedFraction": 0.66 @@ -22,5 +22,8 @@ "FilePath": "{EntryFolder}/Logs/nexusmods.app.current.log", "ArchiveFilePath": "{EntryFolder}/Logs/nexusmods.app.{##}.log", "MaxArchivedFiles": 10 + }, + "LauncherSettings": { + "LocaleOverride": "" } } diff --git a/src/NexusMods.App/Program.cs b/src/NexusMods.App/Program.cs index d0e94b2a33..96d8f1d64c 100644 --- a/src/NexusMods.App/Program.cs +++ b/src/NexusMods.App/Program.cs @@ -79,7 +79,6 @@ public static IHost BuildHost() // Note: suppressed because invalid config will throw. config = JsonSerializer.Deserialize(configJson)!; config.Sanitize(); - services.AddSingleton(config); services.AddApp(config).Validate(); }) .ConfigureLogging((_, builder) => AddLogging(builder, config.LoggingSettings)) diff --git a/src/NexusMods.App/Services.cs b/src/NexusMods.App/Services.cs index 4b53bd23b3..da94bc7472 100644 --- a/src/NexusMods.App/Services.cs +++ b/src/NexusMods.App/Services.cs @@ -33,7 +33,7 @@ public static IServiceCollection AddRenderers(this IServiceCollection services) services.AddScoped(); return services; } - + public static IServiceCollection AddListeners(this IServiceCollection services) { services.AddSingleton(); @@ -47,7 +47,7 @@ public static IServiceCollection AddApp(this IServiceCollection services, services.AddCLI() .AddFileSystem() - .AddUI() + .AddUI(config.LauncherSettings) .AddFileExtractors(config.FileExtractorSettings) .AddDataModel(config.DataModelSettings) .AddBethesdaGameStudios() From 4ec9231bee0d6895436a2bf45662caae062c18c2 Mon Sep 17 00:00:00 2001 From: sewer56lol Date: Tue, 15 Aug 2023 20:30:40 +0100 Subject: [PATCH 03/11] Added: Documents for Localization & Translation --- docs/LocalizationAndTranslation.md | 95 ++++++++++++++++ ...7-localization-and-internationalisation.md | 105 ++++++++++++++++++ .../images/0007_unlocalised_strings.png | Bin 0 -> 62557 bytes docs/images/add_resx_file.png | Bin 0 -> 39442 bytes src/NexusMods.App.UI/NexusMods.App.UI.csproj | 2 +- src/NexusMods.App.UI/Resources/Language.resx | 2 +- 6 files changed, 202 insertions(+), 2 deletions(-) create mode 100644 docs/LocalizationAndTranslation.md create mode 100644 docs/decisions/backend/0007-localization-and-internationalisation.md create mode 100644 docs/decisions/backend/images/0007_unlocalised_strings.png create mode 100644 docs/images/add_resx_file.png diff --git a/docs/LocalizationAndTranslation.md b/docs/LocalizationAndTranslation.md new file mode 100644 index 0000000000..be698b311e --- /dev/null +++ b/docs/LocalizationAndTranslation.md @@ -0,0 +1,95 @@ +# Localization and Translation in the Nexus App + +For more details, see [0007: Localization and Internationalisation](./decisions/backend/0007-localization-and-internationalisation.md). + +In the Nexus App we use the industry standard `Resource` files for porting the App to different languages and cultures. + +This involves creating a relevant `.resx` file in every project for the base language, and additional `.resx` files for any additional language you want to support. + +Working with these files requires a supported IDE such as Rider or Visual Studio + +## Creating a New Language File + +- Create a folder called `Resources`, if it does not already exist. +- Create a Resource file (`Add` -> `Resources`), name it `Language.resx`. + +``` +Resources/Language.resx +``` + +Doubleclicking this file in a supported IDE will bring you to the `Resource Editor`, which will allow you to edit the resources file. + +### Additional Steps + +For elements which may be referenced from the UI (Avalonia), an additional step is required. + +When you create a Resource file from Within Rider, it will be set to `internal` by default. This is okay for code, but is not okay for XAML; as XAML needs those elements public. + +To make these elements public, open the project's `.csproj` file; find the `.resx` file and change `ResXFileCodeGenerator` to `PublicResXFileCodeGenerator` such that you will see: + +``` + + PublicResXFileCodeGenerator + Language.Designer.cs + +``` + +You will then need to perform an action that would trigger a rebuild of `Language.Designer.cs`. This could be temporarily adding or renaming a key. These files are (unfortunately), not autogenerated as part of MSBuild. + +## Adding a new Language + +This should be supported as a button in your IDE (in Rider it is top left of 'Localization Manager', above the project listing). + +If it is not natively supported, create a resource file using the naming convention: + +``` +Language.{culture}.resx +``` + +Manually. For example `Language.de.resx`. Both shorthands e.g. `de` (German) and full localized cultures e.g. `de-de` (German, Germany) are supported. + +## Using Localized Strings + +### Reference Text from C# + +The items are exposed as static fields of the `Language` class; i.e. + +```csharp +Language.MyGames +``` + +Is a valid way to reference string behind the code. +In some cases, you might have to use `string.Format` to inject parameters into the text: + +``` +// Resources.Hello is "Hello {0}" +string.Format(Resources.Hello, user) +``` + +### Reference from XAML + +[Official Docs Here](https://docs.avaloniaui.net/docs/next/guides/implementation-guides/localizing) + +Use the `x:Static` markup extension to reference a static element. + +```xaml + +``` + +If formatting is required, please do so in the code behind for the element to be consistent with the existing Reactive based code. +``` + +### Overriding the Language at Boot Time + +You can change the locale in `AppConfig.json` file. +This file is in the `NexusMods.App` project at time of writing and is copied to build directory. + +### Switching a Language + +Language can be switched at runtime with the following code: + +``` +Language.Culture = new CultureInfo(/* locale */); +``` + +We run this code at startup in `OnFrameworkInitializationCompleted` to set the language at startup. \ No newline at end of file diff --git a/docs/decisions/backend/0007-localization-and-internationalisation.md b/docs/decisions/backend/0007-localization-and-internationalisation.md new file mode 100644 index 0000000000..bbf7242c13 --- /dev/null +++ b/docs/decisions/backend/0007-localization-and-internationalisation.md @@ -0,0 +1,105 @@ +``` +status: accepted +date: {2023-08-15 when the decision was last updated} +deciders: App Team @ 2023-07-19 Meeting +``` + +# Internationalisation and Localisation + +## Context and Problem Statement + +Internationalisation and Localisation, referred to as `i18n` and `l10n` onwards, describe the process of adapting our code to be locale agnostic. + +Currently, our UI contains hard-coded strings, meaning the values are constant and part of our code: + +![Strings](./images/0007_unlocalised_strings.png) + +Although hardcoding strings makes designing and implementing user interfaces straightforward, it also means that we force English as a UI language. + +### Additional Details + +`i18n` and `l10n` also encompass formatting rules for various constructs like numbers, date and time, currencies, as well as text layouts. English is left-to-right and top-to-bottom horizontally, but other languages read from right-to-left and some even vertically. + +Text can also sometimes greatly vary in length. If a button has a text of length `10` in English, it might only contain two characters in Japanese or `20` in German. If the UI isn't designed or able to adapt to drastic changes in text, it will lead to text cutoff, undesired wrapping behavior or make the text completely illegible. + +Another issue are fonts. Most fonts used on the web only support a limited amount of characters, typically based on Roman alphabet. CJK fonts (**C**hinese, **J**apanese, and **K**orean), like [Noto by Google](https://fonts.google.com/noto), are special fonts that contain a vast amount of characters from all parts of the world. + +These are usually not required by websites, since browsers have fallback fonts, however, for applications that render text directly, such as ours, rendering an unsupported character might result in white boxes: https://github.com/jellyfin/jellyfin/issues/5008 + +![](https://user-images.githubusercontent.com/39822140/104345555-d5045c00-5541-11eb-953d-f3943a6c63ee.png) + +## Decision Drivers + +* Support Common Languages (EN, SP, CN) etc. +* Make Modding Easier (TM) [for non-native speakers]. +* Editing translations for people must be easy. + +During discussion, we decided that we only intend to support Left To Right languages (LTR), which makes it a non-driver in decision making. + +This is down to the design overhead involved; as adding RTL support would require entirely new UI layouts/designs. + +## Considered Options + +* DIY Solution involving Loading ResourceDictionary per language. +* .resx files. +* Custom code-behind. + +## Decision Outcome + +Chosen option: `.resx files`. + +### Consequences + +* Good, because the solution is compile time safe. +* Good, because translators can use existing translation services (like [Weblate](https://docs.weblate.org/en/latest/formats/resx.html)). +* Bad, because translators cannot see the changes within the UI [without installing dev tools & recompiling]. +* Bad, because `.resx` files can introduce minimal stutter as changing locale requires a new DLL load every load. + +## Pros and Cons of the Options + +### DIY Solution + +This solution involves loading language translation through Avalonia ResourceDictionaries via `.axaml` files. + +* Good, because translators can see new translations immediately. +* Good, because making translations requires no specialised tooling or recompilation (just a text editor). +* Good, because translation can be effortlessly changed on the fly. +* Bad, because this solution is not compile time safe. + +### RESX files + +We use resource `.resx` files, comprised of 1 default file, then 1 language per locale. + +* Good, because this is familiar to many developers [industry standard], making it easier for external contributors. +* Good, because there is a lot of external 3rd party tooling to allow editing resource files for non-technical users. +* Good, because the solution is compile time safe. +* Bad, because translators cannot see their changes inside actual app when translating. + +### Custom Code-Behind + +A custom implementation that involves dynamically fetching string values which are read from either a file or embedded resource. + +* Neutral, because this is basically doing the `.resx` solution, but with some manually created tooling. +* Bad because no convenient user tooling exists for this one. + +## Common Drawbacks + +In cases where we originally used string interpolation, such as + +```csharp +$"Hello {user}" +``` + +This code would have to be changed to: + +```csharp +// using resources (or equivalent) +// Resources.Hello is "Hello {0}" +string.Format(Resources.Hello, user); +``` + +The issue here is that `Hello {0}` injects a parameter into the `string.Format` function. When people PR/submit translations, we need to ensure that people correctly insert the format placeholders `{0}`, `{1}`, etc. + +## More Information + +- We will need to seek alternative fonts down the road for CJK support; since our design system with Roboto and Montserrat do not support such fonts. \ No newline at end of file diff --git a/docs/decisions/backend/images/0007_unlocalised_strings.png b/docs/decisions/backend/images/0007_unlocalised_strings.png new file mode 100644 index 0000000000000000000000000000000000000000..356ad8de0cf2bb473c4a208e9aa35649b9500c1e GIT binary patch literal 62557 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 KqEg)W=l=nBg)|@l literal 0 HcmV?d00001 diff --git a/docs/images/add_resx_file.png b/docs/images/add_resx_file.png new file mode 100644 index 0000000000000000000000000000000000000000..f8231b2b3d904720a3c35bf51bc737d8e5206908 GIT binary patch literal 39442 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 YkM=w(a^sBHItC!{boFyt=akR{040q=)c^nh literal 0 HcmV?d00001 diff --git a/src/NexusMods.App.UI/NexusMods.App.UI.csproj b/src/NexusMods.App.UI/NexusMods.App.UI.csproj index b9e5057dbc..77c59bb64a 100644 --- a/src/NexusMods.App.UI/NexusMods.App.UI.csproj +++ b/src/NexusMods.App.UI/NexusMods.App.UI.csproj @@ -315,7 +315,7 @@ - ResXFileCodeGenerator + PublicResXFileCodeGenerator Language.Designer.cs diff --git a/src/NexusMods.App.UI/Resources/Language.resx b/src/NexusMods.App.UI/Resources/Language.resx index 32556db9ba..6b99f1581d 100644 --- a/src/NexusMods.App.UI/Resources/Language.resx +++ b/src/NexusMods.App.UI/Resources/Language.resx @@ -3,7 +3,7 @@ - + From 78ca6fcd5569ddd3503b4e67d628a53b81343325 Mon Sep 17 00:00:00 2001 From: sewer56lol Date: Thu, 17 Aug 2023 11:21:10 +0100 Subject: [PATCH 04/11] Added: Basic Localization Plumbing --- src/NexusMods.App.UI/App.axaml.cs | 3 +- .../Localization/Localized.cs | 39 ++++++++++++++ .../Localization/Localizer.cs | 54 +++++++++++++++++++ .../NexusMods.UI.Tests.csproj | 4 ++ 4 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 src/NexusMods.App.UI/Localization/Localized.cs create mode 100644 src/NexusMods.App.UI/Localization/Localizer.cs diff --git a/src/NexusMods.App.UI/App.axaml.cs b/src/NexusMods.App.UI/App.axaml.cs index 7c23dd4d8b..4f0783789d 100644 --- a/src/NexusMods.App.UI/App.axaml.cs +++ b/src/NexusMods.App.UI/App.axaml.cs @@ -4,6 +4,7 @@ using Avalonia.Markup.Xaml; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using NexusMods.App.UI.Localization; using NexusMods.App.UI.Resources; using NexusMods.App.UI.Windows; using ReactiveUI; @@ -31,7 +32,7 @@ public override void Initialize() public override void OnFrameworkInitializationCompleted() { if (!string.IsNullOrEmpty(_launcherSettings.LocaleOverride)) - Language.Culture = new CultureInfo(_launcherSettings.LocaleOverride); + Localizer.Instance.LoadLanguage(_launcherSettings.LocaleOverride); Locator.CurrentMutable.UnregisterCurrent(typeof(IViewLocator)); Locator.CurrentMutable.Register(() => _provider.GetRequiredService(), typeof(IViewLocator)); diff --git a/src/NexusMods.App.UI/Localization/Localized.cs b/src/NexusMods.App.UI/Localization/Localized.cs new file mode 100644 index 0000000000..40885fe7d4 --- /dev/null +++ b/src/NexusMods.App.UI/Localization/Localized.cs @@ -0,0 +1,39 @@ +using Avalonia.Data; +using Avalonia.Markup.Xaml; +using Avalonia.Markup.Xaml.MarkupExtensions; +using Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings; + +namespace NexusMods.App.UI.Localization; + +/// +/// A XAML Markup Extension that provides localized strings. +/// It can be used in XAML like this: +/// `` +/// +/// Where `HelloWorld` is the key of the string to be localized inside the `.resx` file. +/// +public class LocalizedExtension : MarkupExtension +{ + /// + /// The key in the .resx file to be localized. + /// + public string Key { get; set; } + + public LocalizedExtension(string key) => Key = key; + + /// + public override object ProvideValue(IServiceProvider serviceProvider) + { + // Tip: This binds the [] method of the Localizer class. + + // Note: CompiledBindingExtension would be nicer to use, for performance reasons; but the things + // needed to manually create one, are internal :( + var binding = new ReflectionBindingExtension($"[{Key}]") + { + Mode = BindingMode.OneWay, + Source = Localizer.Instance, + }; + + return binding.ProvideValue(serviceProvider); + } +} diff --git a/src/NexusMods.App.UI/Localization/Localizer.cs b/src/NexusMods.App.UI/Localization/Localizer.cs new file mode 100644 index 0000000000..27e861a057 --- /dev/null +++ b/src/NexusMods.App.UI/Localization/Localizer.cs @@ -0,0 +1,54 @@ +using System.ComponentModel; +using System.Globalization; +using System.Text; +using Avalonia; +using Avalonia.Markup.Xaml.MarkupExtensions; +using NexusMods.App.UI.Resources; + +namespace NexusMods.App.UI.Localization; + +/// +/// Utility class that provides access +/// +public class Localizer : INotifyPropertyChanged // <= INotifyPropertyChanged is required, so we can't be static +{ + /// + /// Singleton instance of the + /// + public static Localizer Instance { get; set; } = new(); + + public event PropertyChangedEventHandler? PropertyChanged; + + // These are 'conventional' names used by the UI framework to indicate that an indexer + // property has been changed. When we create a binding with $"[{Key}]" syntax, the UI + // framework binds against the this[string key] property. + + // Historically in XAML frameworks, this can be internally represented as 'Item' or 'Item[]'; + // when I had a look through the Avalonia source for creating bindings; it seems to be 'Item' for + // array members; which I believe this would fall under. But just in case, I included both. + + private const string IndexerName = "Item"; + private const string IndexerArrayName = "Item[]"; + + /// + /// Loads a language with the specified locale, resetting the + /// + /// The new locale to apply. + public void LoadLanguage(string locale) + { + var item = new CultureInfo(locale); + Language.Culture = item; + Invalidate(); + } + + /// + /// Retrieves a string associated with current language, by key. + /// + public string this[string key] => Language.ResourceManager.GetString(key)!; + + private void Invalidate() + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(IndexerName)); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(IndexerArrayName)); + } +} diff --git a/tests/NexusMods.UI.Tests/NexusMods.UI.Tests.csproj b/tests/NexusMods.UI.Tests/NexusMods.UI.Tests.csproj index 068dd14870..acf9865634 100644 --- a/tests/NexusMods.UI.Tests/NexusMods.UI.Tests.csproj +++ b/tests/NexusMods.UI.Tests/NexusMods.UI.Tests.csproj @@ -21,4 +21,8 @@ PreserveNewest + + + + From 7627904897804a0a13a7cb4fd5bb1920f99f8de1 Mon Sep 17 00:00:00 2001 From: sewer56lol Date: Thu, 17 Aug 2023 11:49:57 +0100 Subject: [PATCH 05/11] Added: Basic tests for localization primitives. --- .../{Localized.cs => LocalizedExtension.cs} | 1 - .../Localization/Localizer.cs | 5 +- .../Localization/BasicLocalizedTests.cs | 48 +++++++++++++++++++ .../Localization/BasicLocalizerTests.cs | 17 +++++++ .../NexusMods.UI.Tests.csproj | 4 -- 5 files changed, 67 insertions(+), 8 deletions(-) rename src/NexusMods.App.UI/Localization/{Localized.cs => LocalizedExtension.cs} (95%) create mode 100644 tests/NexusMods.UI.Tests/Localization/BasicLocalizedTests.cs create mode 100644 tests/NexusMods.UI.Tests/Localization/BasicLocalizerTests.cs diff --git a/src/NexusMods.App.UI/Localization/Localized.cs b/src/NexusMods.App.UI/Localization/LocalizedExtension.cs similarity index 95% rename from src/NexusMods.App.UI/Localization/Localized.cs rename to src/NexusMods.App.UI/Localization/LocalizedExtension.cs index 40885fe7d4..e459344dbd 100644 --- a/src/NexusMods.App.UI/Localization/Localized.cs +++ b/src/NexusMods.App.UI/Localization/LocalizedExtension.cs @@ -1,7 +1,6 @@ using Avalonia.Data; using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml.MarkupExtensions; -using Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings; namespace NexusMods.App.UI.Localization; diff --git a/src/NexusMods.App.UI/Localization/Localizer.cs b/src/NexusMods.App.UI/Localization/Localizer.cs index 27e861a057..f03885e556 100644 --- a/src/NexusMods.App.UI/Localization/Localizer.cs +++ b/src/NexusMods.App.UI/Localization/Localizer.cs @@ -36,15 +36,14 @@ public class Localizer : INotifyPropertyChanged // <= INotifyPropertyChanged is /// The new locale to apply. public void LoadLanguage(string locale) { - var item = new CultureInfo(locale); - Language.Culture = item; + Language.Culture = new CultureInfo(locale); Invalidate(); } /// /// Retrieves a string associated with current language, by key. /// - public string this[string key] => Language.ResourceManager.GetString(key)!; + public string this[string key] => Language.ResourceManager.GetString(key, Language.Culture)!; private void Invalidate() { diff --git a/tests/NexusMods.UI.Tests/Localization/BasicLocalizedTests.cs b/tests/NexusMods.UI.Tests/Localization/BasicLocalizedTests.cs new file mode 100644 index 0000000000..83b5b98c6c --- /dev/null +++ b/tests/NexusMods.UI.Tests/Localization/BasicLocalizedTests.cs @@ -0,0 +1,48 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Data; +using FluentAssertions; +using NexusMods.App.UI.Localization; + +namespace NexusMods.UI.Tests.Localization; + +public class BasicLocalizedTests +{ + private readonly IServiceProvider _provider; + public BasicLocalizedTests(IServiceProvider provider) + { + _provider = provider; + } + + [Fact] + public void ProvideValue_NewStringIsReturnedInCodeBehind() + { + var localized = new LocalizedExtension("MyGames"); + + var originalString = GetStringFromBinding((Binding) localized.ProvideValue(_provider)); + Localizer.Instance.LoadLanguage("pl"); + var newString = GetStringFromBinding((Binding) localized.ProvideValue(_provider)); + + newString.Should().NotBe(originalString); + } + + private string GetStringFromBinding(Binding binding) + { + // Good info: https://github.com/AvaloniaUI/Avalonia/discussions/11401 + var dummy = new TextBlock(); + var instanced = binding.Initiate(dummy, TextBlock.TextProperty); + + // If Observable is null, then it probably was OneWayToSource binding, + // and it's not possible to read. + var result = ""; + if (instanced?.Source is { } observable) + { + observable.Subscribe(s => + { + result = (string)s!; + }).Dispose(); + } + + return result; + } +} diff --git a/tests/NexusMods.UI.Tests/Localization/BasicLocalizerTests.cs b/tests/NexusMods.UI.Tests/Localization/BasicLocalizerTests.cs new file mode 100644 index 0000000000..9590bfbc72 --- /dev/null +++ b/tests/NexusMods.UI.Tests/Localization/BasicLocalizerTests.cs @@ -0,0 +1,17 @@ +using FluentAssertions; +using NexusMods.App.UI.Localization; + +namespace NexusMods.UI.Tests.Localization; + +public class BasicLocalizerTests +{ + [Fact] + public void WhenLanguageChanges_NewStringIsReturnedInCodeBehind() + { + var originalString = Localizer.Instance["MyGames"]; + Localizer.Instance.LoadLanguage("pl"); + var newString = Localizer.Instance["MyGames"]; + + newString.Should().NotBe(originalString); + } +} diff --git a/tests/NexusMods.UI.Tests/NexusMods.UI.Tests.csproj b/tests/NexusMods.UI.Tests/NexusMods.UI.Tests.csproj index acf9865634..068dd14870 100644 --- a/tests/NexusMods.UI.Tests/NexusMods.UI.Tests.csproj +++ b/tests/NexusMods.UI.Tests/NexusMods.UI.Tests.csproj @@ -21,8 +21,4 @@ PreserveNewest - - - - From 409cd59a68e5880ab7e6fa57a332289cf23f7355 Mon Sep 17 00:00:00 2001 From: sewer56lol Date: Thu, 17 Aug 2023 11:59:33 +0100 Subject: [PATCH 06/11] Now Reflection-free! <3 --- .../Localization/LocalizedExtension.cs | 16 +++++++++++++++- .../Localization/BasicLocalizedTests.cs | 9 ++++----- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/NexusMods.App.UI/Localization/LocalizedExtension.cs b/src/NexusMods.App.UI/Localization/LocalizedExtension.cs index e459344dbd..37be2a4bc6 100644 --- a/src/NexusMods.App.UI/Localization/LocalizedExtension.cs +++ b/src/NexusMods.App.UI/Localization/LocalizedExtension.cs @@ -1,6 +1,8 @@ using Avalonia.Data; +using Avalonia.Data.Core; using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml.MarkupExtensions; +using Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings; namespace NexusMods.App.UI.Localization; @@ -24,10 +26,22 @@ public class LocalizedExtension : MarkupExtension public override object ProvideValue(IServiceProvider serviceProvider) { // Tip: This binds the [] method of the Localizer class. + // Build binding for '$"[{Key}]"'. // Note: CompiledBindingExtension would be nicer to use, for performance reasons; but the things // needed to manually create one, are internal :( - var binding = new ReflectionBindingExtension($"[{Key}]") + + var x = new CompiledBindingPathBuilder(); + x = x.SetRawSource(Localizer.Instance); + x = x.Property( + new ClrPropertyInfo( + "Item", + obj => ((Localizer)obj)[Key], + null, + typeof(string)), + PropertyInfoAccessorFactory.CreateInpcPropertyAccessor); + + var binding = new CompiledBindingExtension(x.Build()) { Mode = BindingMode.OneWay, Source = Localizer.Instance, diff --git a/tests/NexusMods.UI.Tests/Localization/BasicLocalizedTests.cs b/tests/NexusMods.UI.Tests/Localization/BasicLocalizedTests.cs index 83b5b98c6c..ca0732f059 100644 --- a/tests/NexusMods.UI.Tests/Localization/BasicLocalizedTests.cs +++ b/tests/NexusMods.UI.Tests/Localization/BasicLocalizedTests.cs @@ -1,6 +1,5 @@ -using Avalonia; using Avalonia.Controls; -using Avalonia.Data; +using Avalonia.Markup.Xaml.MarkupExtensions; using FluentAssertions; using NexusMods.App.UI.Localization; @@ -19,14 +18,14 @@ public void ProvideValue_NewStringIsReturnedInCodeBehind() { var localized = new LocalizedExtension("MyGames"); - var originalString = GetStringFromBinding((Binding) localized.ProvideValue(_provider)); + var originalString = GetStringFromBinding((CompiledBindingExtension) localized.ProvideValue(_provider)); Localizer.Instance.LoadLanguage("pl"); - var newString = GetStringFromBinding((Binding) localized.ProvideValue(_provider)); + var newString = GetStringFromBinding((CompiledBindingExtension) localized.ProvideValue(_provider)); newString.Should().NotBe(originalString); } - private string GetStringFromBinding(Binding binding) + private string GetStringFromBinding(CompiledBindingExtension binding) { // Good info: https://github.com/AvaloniaUI/Avalonia/discussions/11401 var dummy = new TextBlock(); From f162b50991ea761a765b96ab8d241f6693669551 Mon Sep 17 00:00:00 2001 From: sewer56lol Date: Thu, 17 Aug 2023 12:18:11 +0100 Subject: [PATCH 07/11] Added: A test that ensures binding is correctly setup. --- .../Localization/LocalizedExtension.cs | 4 --- .../Localization/BasicLocalizedTests.cs | 34 +++++++++++++++++++ 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/NexusMods.App.UI/Localization/LocalizedExtension.cs b/src/NexusMods.App.UI/Localization/LocalizedExtension.cs index 37be2a4bc6..377a753321 100644 --- a/src/NexusMods.App.UI/Localization/LocalizedExtension.cs +++ b/src/NexusMods.App.UI/Localization/LocalizedExtension.cs @@ -27,10 +27,6 @@ public override object ProvideValue(IServiceProvider serviceProvider) { // Tip: This binds the [] method of the Localizer class. // Build binding for '$"[{Key}]"'. - - // Note: CompiledBindingExtension would be nicer to use, for performance reasons; but the things - // needed to manually create one, are internal :( - var x = new CompiledBindingPathBuilder(); x = x.SetRawSource(Localizer.Instance); x = x.Property( diff --git a/tests/NexusMods.UI.Tests/Localization/BasicLocalizedTests.cs b/tests/NexusMods.UI.Tests/Localization/BasicLocalizedTests.cs index ca0732f059..b0cbaa87d2 100644 --- a/tests/NexusMods.UI.Tests/Localization/BasicLocalizedTests.cs +++ b/tests/NexusMods.UI.Tests/Localization/BasicLocalizedTests.cs @@ -25,6 +25,40 @@ public void ProvideValue_NewStringIsReturnedInCodeBehind() newString.Should().NotBe(originalString); } + [Fact] + public void ProvideValue_NewStringIsReturnedViaObservable_WhenLanguageIsChanged() + { + var localized = new LocalizedExtension("MyGames"); + var binding = (CompiledBindingExtension)localized.ProvideValue(_provider); + + var dummy = new TextBlock(); + var instanced = binding.Initiate(dummy, TextBlock.TextProperty); + var result = ""; + IDisposable disposable = null!; + if (instanced?.Source is { } observable) + { + disposable = observable.Subscribe(s => + { + result = (string)s!; + }); + } + else + { + Assert.Fail("Observable is null"); + } + + // Ensure we have default value. + result.Should().NotBeNullOrEmpty(); + var lastValue = result; + + // Change the locale, this should emit a new value in the observable. + Localizer.Instance.LoadLanguage("pl"); + var currentValue = result; + + lastValue.Should().NotBe(currentValue); + disposable.Dispose(); + } + private string GetStringFromBinding(CompiledBindingExtension binding) { // Good info: https://github.com/AvaloniaUI/Avalonia/discussions/11401 From 7db925723eb1c0b0a3767a8d4c409256f13bc835 Mon Sep 17 00:00:00 2001 From: sewer56lol Date: Thu, 17 Aug 2023 13:00:31 +0100 Subject: [PATCH 08/11] Updated: Docs for new locale system. --- docs/LocalizationAndTranslation.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/docs/LocalizationAndTranslation.md b/docs/LocalizationAndTranslation.md index be698b311e..a310445e83 100644 --- a/docs/LocalizationAndTranslation.md +++ b/docs/LocalizationAndTranslation.md @@ -68,16 +68,13 @@ string.Format(Resources.Hello, user) ### Reference from XAML -[Official Docs Here](https://docs.avaloniaui.net/docs/next/guides/implementation-guides/localizing) - -Use the `x:Static` markup extension to reference a static element. +Use the custom `LocalizedExtension` markup extension to reference a localizable string. ```xaml - + ``` If formatting is required, please do so in the code behind for the element to be consistent with the existing Reactive based code. -``` ### Overriding the Language at Boot Time @@ -89,7 +86,7 @@ This file is in the `NexusMods.App` project at time of writing and is copied to Language can be switched at runtime with the following code: ``` -Language.Culture = new CultureInfo(/* locale */); +Localizer.Instance.LoadLanguage(new CultureInfo(/* locale */)); ``` We run this code at startup in `OnFrameworkInitializationCompleted` to set the language at startup. \ No newline at end of file From 46ba5d3288937bacff6e0f99857dba5de0705c7e Mon Sep 17 00:00:00 2001 From: sewer56lol Date: Thu, 17 Aug 2023 17:24:43 +0100 Subject: [PATCH 09/11] Added: LocalizedStringUpdater helper & usage instructions. --- docs/LocalizationAndTranslation.md | 70 ++++++++++++++++++- .../Game/GameLeftMenuDesignViewModel.cs | 6 +- .../Home/HomeLeftMenuDesignViewModel.cs | 6 +- .../LeftMenu/Home/HomeLeftMenuViewModel.cs | 52 ++++++++------ .../LeftMenu/Items/IconViewModel.cs | 12 +++- .../Localization/LocalizedStringUpdater.cs | 28 ++++++++ .../Localization/Localizer.cs | 8 ++- .../Localization/BasicLocalizerTests.cs | 17 +++++ 8 files changed, 165 insertions(+), 34 deletions(-) create mode 100644 src/NexusMods.App.UI/Localization/LocalizedStringUpdater.cs diff --git a/docs/LocalizationAndTranslation.md b/docs/LocalizationAndTranslation.md index a310445e83..1a2ee57142 100644 --- a/docs/LocalizationAndTranslation.md +++ b/docs/LocalizationAndTranslation.md @@ -52,18 +52,82 @@ Manually. For example `Language.de.resx`. Both shorthands e.g. `de` (German) and ### Reference Text from C# +#### Basic Referencing + The items are exposed as static fields of the `Language` class; i.e. ```csharp Language.MyGames ``` -Is a valid way to reference string behind the code. +If you are in a context where the language cannot be dynamically changed (for example: in a dialog box that must be closed before the user can change the language again), using the static property is sufficient. + + +#### Formatted Text + In some cases, you might have to use `string.Format` to inject parameters into the text: ``` -// Resources.Hello is "Hello {0}" -string.Format(Resources.Hello, user) +// Language.Hello is "Hello {0}" +string.Format(Language.Hello, user) +``` + +#### Updating Strings Dynamically + +If you have a string in C# which is long lived, and will be live when the language is changed; it must be updated dynamically. + +For this purpose, the `LocalizedStringUpdater` class is provided; it is used like this: + +```csharp +_localizable = new LocalizedStringUpdater(() => ReactiveField = Language.MyGames); +``` + +To give an example: + +```csharp +public class SomeViewModel : AViewModel, ISomeViewModel, IDisposable +{ + [Reactive] + public string Name { get; set; } = ""; + + private readonly LocalizedStringUpdater _nameUpdater; + + public SomeViewModel(Func getName) + { + _nameUpdater = new LocalizedStringUpdater(() => Name = getName()); + } + + // Note: Multi-dispose guard not needed; LocalizedStringUpdater.Dispose by contract only unsubscribes from an event. + public void Dispose() => _nameUpdater.Dispose(); +} +``` + +When you call `new LocalizedStringUpdater` and every time the language is changed +through the use [Localizer.Instance.LoadLanguage](#switching-a-language), the callback given in the `LocalizedStringUpdater` +will be executed. + +As for the `[Reactive]` fields, the autogenerated `INotifyPropertyChanged` handlers will always +do a comparison on the string in the setter. So if a string remains unchanged, no UI redraw will occur +for the affected element as `PropertyChanged` will not fire. + +!!! note "The API is specifically designed like this to ensure compile time safety." + +!!! danger "`LocalizedStringUpdater` must be properly disposed when no longer in use. Due to nature of .NET events, lack of proper disposal will lead to memory leak as the class subscribes to `Localizer` which has singleton lifetime." + +##### Use within Reactive Code + +When possible, use the `WhenActivated` ReactiveUI method; alongside `DisposeWith`. This will ensure that `LocalizedStringUpdater(s)` and structures which use them will be disposed safely. + +```csharp +this.WhenActivated(disposable => +{ + var items = new ILeftMenuItemViewModel[] + { + new IconViewModel(() => Language.Newsfeed) { ... }.DisposeWith(disposable), + new IconViewModel(() => Language.MyGames) { ... }.DisposeWith(disposable), + new IconViewModel(() => Language.BrowseGames) { ... }.DisposeWith(disposable) + }; +}); ``` ### Reference from XAML diff --git a/src/NexusMods.App.UI/LeftMenu/Game/GameLeftMenuDesignViewModel.cs b/src/NexusMods.App.UI/LeftMenu/Game/GameLeftMenuDesignViewModel.cs index e26e87baed..418cdfbb75 100644 --- a/src/NexusMods.App.UI/LeftMenu/Game/GameLeftMenuDesignViewModel.cs +++ b/src/NexusMods.App.UI/LeftMenu/Game/GameLeftMenuDesignViewModel.cs @@ -33,9 +33,9 @@ public GameLeftMenuDesignViewModel() var items = new ILeftMenuItemViewModel[] { - new IconViewModel { Name = "Newsfeed", Icon = IconType.News}, - new IconViewModel { Name = "My loadout 1", Icon = IconType.ChevronRight }, - new IconViewModel { Name = "My other loadout", Icon = IconType.ChevronRight }, + new IconViewModel(() => "Newsfeed") { Icon = IconType.News}, + new IconViewModel(() => "My loadout 1") { Icon = IconType.ChevronRight }, + new IconViewModel(() => "My other loadout") { Icon = IconType.ChevronRight }, }; Items = items.ToReadOnlyObservableCollection(); } diff --git a/src/NexusMods.App.UI/LeftMenu/Home/HomeLeftMenuDesignViewModel.cs b/src/NexusMods.App.UI/LeftMenu/Home/HomeLeftMenuDesignViewModel.cs index e5347c0e3d..9d82d6fcd3 100644 --- a/src/NexusMods.App.UI/LeftMenu/Home/HomeLeftMenuDesignViewModel.cs +++ b/src/NexusMods.App.UI/LeftMenu/Home/HomeLeftMenuDesignViewModel.cs @@ -15,9 +15,9 @@ public HomeLeftMenuDesignViewModel() { var items = new ILeftMenuItemViewModel[] { - new IconViewModel { Name = Language.Newsfeed, Icon = IconType.News}, - new IconViewModel { Name = Language.MyGames, Icon = IconType.Bookmark }, - new IconViewModel { Name = Language.BrowseGames, Icon = IconType.Game } + new IconViewModel(() => Language.Newsfeed) { Icon = IconType.News}, + new IconViewModel(() => Language.MyGames) { Icon = IconType.Bookmark }, + new IconViewModel(() => Language.BrowseGames) { Icon = IconType.Game } }; Items = new ReadOnlyObservableCollection(new ObservableCollection(items)); } diff --git a/src/NexusMods.App.UI/LeftMenu/Home/HomeLeftMenuViewModel.cs b/src/NexusMods.App.UI/LeftMenu/Home/HomeLeftMenuViewModel.cs index af193de4a2..34be1170b4 100644 --- a/src/NexusMods.App.UI/LeftMenu/Home/HomeLeftMenuViewModel.cs +++ b/src/NexusMods.App.UI/LeftMenu/Home/HomeLeftMenuViewModel.cs @@ -1,6 +1,9 @@ using System.Collections.ObjectModel; +using System.Reactive.Disposables; +using Avalonia.Utilities; using NexusMods.App.UI.Icons; using NexusMods.App.UI.LeftMenu.Items; +using NexusMods.App.UI.Localization; using NexusMods.App.UI.Resources; using NexusMods.App.UI.RightContent; using NexusMods.App.UI.RightContent.MyGames; @@ -11,33 +14,38 @@ namespace NexusMods.App.UI.LeftMenu.Home; public class HomeLeftMenuViewModel : AViewModel, IHomeLeftMenuViewModel { - public ReadOnlyObservableCollection Items { get; } + static ReadOnlyObservableCollection _empty = new ReadOnlyObservableCollection(new ObservableCollection()); + + public ReadOnlyObservableCollection Items { get; private set; } = _empty; [Reactive] - public IRightContentViewModel RightContent { get; set; } = - Initializers.IRightContent; + public IRightContentViewModel RightContent { get; set; } = Initializers.IRightContent; public HomeLeftMenuViewModel(IMyGamesViewModel myGamesViewModel, IFoundGamesViewModel foundGamesViewModel) { - var items = new ILeftMenuItemViewModel[] + this.WhenActivated(disposable => { - new IconViewModel { Name = Language.Newsfeed, Icon = IconType.News, Activate = ReactiveCommand.Create( - () => - { - RightContent = Initializers.IRightContent; - })}, - new IconViewModel { Name = Language.MyGames, Icon = IconType.Bookmark, Activate = ReactiveCommand.Create( - () => - { - RightContent = myGamesViewModel; - }) }, - new IconViewModel { Name = Language.BrowseGames, Icon = IconType.Game, Activate = ReactiveCommand.Create( - () => - { - RightContent = foundGamesViewModel; - })} - }; - Items = new ReadOnlyObservableCollection( - new ObservableCollection(items)); + var items = new ILeftMenuItemViewModel[] + { + new IconViewModel(() => Language.Newsfeed) { Icon = IconType.News, Activate = ReactiveCommand.Create( + () => + { + RightContent = Initializers.IRightContent; + })}.DisposeWith(disposable), + new IconViewModel(() => Language.MyGames) { Icon = IconType.Bookmark, Activate = ReactiveCommand.Create( + () => + { + RightContent = myGamesViewModel; + }) }.DisposeWith(disposable), + new IconViewModel(() => Language.BrowseGames) { Icon = IconType.Game, Activate = ReactiveCommand.Create( + () => + { + RightContent = foundGamesViewModel; + })}.DisposeWith(disposable) + }; + + Items = new ReadOnlyObservableCollection( + new ObservableCollection(items)); + }); } } diff --git a/src/NexusMods.App.UI/LeftMenu/Items/IconViewModel.cs b/src/NexusMods.App.UI/LeftMenu/Items/IconViewModel.cs index 8fcddad386..92cf12c820 100644 --- a/src/NexusMods.App.UI/LeftMenu/Items/IconViewModel.cs +++ b/src/NexusMods.App.UI/LeftMenu/Items/IconViewModel.cs @@ -1,15 +1,23 @@ using System.Windows.Input; using NexusMods.App.UI.Icons; +using NexusMods.App.UI.Localization; using ReactiveUI.Fody.Helpers; namespace NexusMods.App.UI.LeftMenu.Items; -public class IconViewModel : AViewModel, IIconViewModel +public class IconViewModel : AViewModel, IIconViewModel, IDisposable { - [Reactive] public string Name { get; set; } = ""; + [Reactive] + public string Name { get; set; } = ""; [Reactive] public IconType Icon { get; set; } [Reactive] public ICommand Activate { get; set; } = Initializers.ICommand; + + private readonly LocalizedStringUpdater _nameUpdater; + + public IconViewModel(Func getName) => _nameUpdater = new LocalizedStringUpdater(() => Name = getName()); + + public void Dispose() => _nameUpdater.Dispose(); } diff --git a/src/NexusMods.App.UI/Localization/LocalizedStringUpdater.cs b/src/NexusMods.App.UI/Localization/LocalizedStringUpdater.cs new file mode 100644 index 0000000000..931623c3cd --- /dev/null +++ b/src/NexusMods.App.UI/Localization/LocalizedStringUpdater.cs @@ -0,0 +1,28 @@ +namespace NexusMods.App.UI.Localization; + +/// +/// Utility class for updating localized strings. +/// +public class LocalizedStringUpdater : IDisposable +{ + private readonly Action _refreshString; + + /// + /// Creates an instance of the utility class to update localized strings. + /// + /// + /// The callback that will be fired for every new string. + /// This callback is also fired during initialization of this constructor. + /// + public LocalizedStringUpdater(Action refreshString) + { + _refreshString = refreshString; + refreshString(); + Localizer.Instance.LocaleChanged += InstanceOnLocaleChanged; + } + + private void InstanceOnLocaleChanged() => _refreshString(); + + /// + public void Dispose() => Localizer.Instance.LocaleChanged -= InstanceOnLocaleChanged; +} diff --git a/src/NexusMods.App.UI/Localization/Localizer.cs b/src/NexusMods.App.UI/Localization/Localizer.cs index f03885e556..389f910afb 100644 --- a/src/NexusMods.App.UI/Localization/Localizer.cs +++ b/src/NexusMods.App.UI/Localization/Localizer.cs @@ -13,10 +13,15 @@ namespace NexusMods.App.UI.Localization; public class Localizer : INotifyPropertyChanged // <= INotifyPropertyChanged is required, so we can't be static { /// - /// Singleton instance of the + /// Singleton instance of the localizer. /// public static Localizer Instance { get; set; } = new(); + /// + /// This is called when the locale of an element is changed. + /// + public event Action? LocaleChanged; + public event PropertyChangedEventHandler? PropertyChanged; // These are 'conventional' names used by the UI framework to indicate that an indexer @@ -38,6 +43,7 @@ public void LoadLanguage(string locale) { Language.Culture = new CultureInfo(locale); Invalidate(); + LocaleChanged?.Invoke(); } /// diff --git a/tests/NexusMods.UI.Tests/Localization/BasicLocalizerTests.cs b/tests/NexusMods.UI.Tests/Localization/BasicLocalizerTests.cs index 9590bfbc72..65d7425572 100644 --- a/tests/NexusMods.UI.Tests/Localization/BasicLocalizerTests.cs +++ b/tests/NexusMods.UI.Tests/Localization/BasicLocalizerTests.cs @@ -1,5 +1,6 @@ using FluentAssertions; using NexusMods.App.UI.Localization; +using NexusMods.App.UI.Resources; namespace NexusMods.UI.Tests.Localization; @@ -14,4 +15,20 @@ public void WhenLanguageChanges_NewStringIsReturnedInCodeBehind() newString.Should().NotBe(originalString); } + + [Fact] + public void WhenLanguageChanges_CallbackIsFired() + { + // Verifies that the LocalizedStringUpdater is called back to from Localizer + var newString = ""; + using var localizable = new LocalizedStringUpdater(() => newString = Language.MyGames); + var original = Language.MyGames; + + // The action is executed upon initialization, so this should be equal. + original.Should().Be(newString); + + // Now change the locale, verify the string was changed. + Localizer.Instance.LoadLanguage("pl"); + original.Should().NotBe(newString); + } } From 7d58c38312987843728bdae841b8ce309309e482 Mon Sep 17 00:00:00 2001 From: sewer56lol Date: Thu, 17 Aug 2023 19:47:10 +0100 Subject: [PATCH 10/11] Changed: Reset machine specific settings in config [I didn't notice the defaults for my machine were there. Oopsie] --- src/NexusMods.App/AppConfig.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/NexusMods.App/AppConfig.json b/src/NexusMods.App/AppConfig.json index 5d0b9e70c6..70ee3c940e 100644 --- a/src/NexusMods.App/AppConfig.json +++ b/src/NexusMods.App/AppConfig.json @@ -5,9 +5,9 @@ "ArchiveLocations": [ "{EntryFolder}/Archives" ], - "MaxHashingJobs": 24, - "LoadoutDeploymentJobs": 24, - "MaxHashingThroughputBytesPerSecond": 0 + "MaxHashingJobs": -1, + "LoadoutDeploymentJobs": -1, + "MaxHashingThroughputBytesPerSecond": -1 }, "FileExtractorSettings": { "TempFolderLocation": "{EntryFolder}/Temp" From 7732677a6c7ec5cc0f0226375c9d23c6e28b5052 Mon Sep 17 00:00:00 2001 From: sewer56lol Date: Thu, 17 Aug 2023 19:59:59 +0100 Subject: [PATCH 11/11] Fixed: Build error from bad merge --- src/NexusMods.App/Services.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/NexusMods.App/Services.cs b/src/NexusMods.App/Services.cs index 26add228b7..eca7bb9daf 100644 --- a/src/NexusMods.App/Services.cs +++ b/src/NexusMods.App/Services.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using Microsoft.Extensions.DependencyInjection; using NexusMods.Abstractions.CLI; using NexusMods.App.CLI.Renderers; @@ -48,7 +49,7 @@ public static IServiceCollection AddApp(this IServiceCollection services, services.AddCLI() .AddFileSystem() - .AddUI() + .AddUI(config.LauncherSettings) // .AddFomodInstallerUi() .AddFileExtractors(config.FileExtractorSettings) .AddDataModel(config.DataModelSettings)