diff --git a/Wino.Core.Domain/Interfaces/IConfigurationService.cs b/Wino.Core.Domain/Interfaces/IConfigurationService.cs index a32410d6..e7ea462e 100644 --- a/Wino.Core.Domain/Interfaces/IConfigurationService.cs +++ b/Wino.Core.Domain/Interfaces/IConfigurationService.cs @@ -2,6 +2,8 @@ public interface IConfigurationService { + bool Contains(string key); + void Set(string key, object value); T Get(string key, T defaultValue = default); diff --git a/Wino.Core.Tests/Services/TranslationServiceTests.cs b/Wino.Core.Tests/Services/TranslationServiceTests.cs new file mode 100644 index 00000000..61bfb9f8 --- /dev/null +++ b/Wino.Core.Tests/Services/TranslationServiceTests.cs @@ -0,0 +1,23 @@ +using FluentAssertions; +using Wino.Core.Domain.Enums; +using Wino.Services; +using Xunit; + +namespace Wino.Core.Tests.Services; + +public class TranslationServiceTests +{ + [Theory] + [InlineData("pl-PL", AppLanguage.Polish)] + [InlineData("de-AT", AppLanguage.Deutsch)] + [InlineData("pt-PT", AppLanguage.PortugeseBrazil)] + [InlineData("zh-TW", AppLanguage.Chinese)] + [InlineData("tr_TR", AppLanguage.Turkish)] + [InlineData("nl-NL", AppLanguage.English)] + public void ResolveSupportedLanguage_ReturnsExpectedLanguage(string languageTag, AppLanguage expectedLanguage) + { + var result = TranslationService.ResolveSupportedLanguage([languageTag]); + + result.Should().Be(expectedLanguage); + } +} diff --git a/Wino.Mail.WinUI/Services/ConfigurationService.cs b/Wino.Mail.WinUI/Services/ConfigurationService.cs index afdc47c1..22e32189 100644 --- a/Wino.Mail.WinUI/Services/ConfigurationService.cs +++ b/Wino.Mail.WinUI/Services/ConfigurationService.cs @@ -9,6 +9,9 @@ namespace Wino.Mail.WinUI.Services; public class ConfigurationService : IConfigurationService { + public bool Contains(string key) + => ApplicationData.Current.LocalSettings.Values.ContainsKey(key); + public T Get(string key, T defaultValue = default!) => GetInternal(key, ApplicationData.Current.LocalSettings.Values, defaultValue); diff --git a/Wino.Mail.WinUI/Views/SettingsPage.xaml.cs b/Wino.Mail.WinUI/Views/SettingsPage.xaml.cs index 349fda6b..a4757f95 100644 --- a/Wino.Mail.WinUI/Views/SettingsPage.xaml.cs +++ b/Wino.Mail.WinUI/Views/SettingsPage.xaml.cs @@ -147,9 +147,30 @@ public sealed partial class SettingsPage : SettingsPageAbstract, public void Receive(SettingsRootNavigationRequested message) { - var currentRootPage = SettingsNavigationInfoProvider.GetRootPage(PageHistory.LastOrDefault()?.Request.PageType ?? WinoPage.SettingOptionsPage); - if (message.PageType != WinoPage.SettingOptionsPage && currentRootPage == message.PageType) + var activePage = PageHistory.LastOrDefault()?.Request.PageType ?? WinoPage.SettingOptionsPage; + var currentRootPage = SettingsNavigationInfoProvider.GetRootPage(activePage); + + if (message.PageType == currentRootPage) + { + if (activePage == currentRootPage) + return; + + var currentRootIndex = PageHistory + .Select((item, index) => new { item.Request.PageType, Index = index }) + .FirstOrDefault(item => item.PageType == currentRootPage)?.Index ?? -1; + + if (TryNavigateToBreadcrumbIndex(currentRootIndex)) + return; + } + + if (message.PageType == WinoPage.SettingOptionsPage) + { + if (activePage == WinoPage.SettingOptionsPage) + return; + + NavigateToSettingsHome(); return; + } NavigateToRootPage(message.PageType); } @@ -196,11 +217,7 @@ public sealed partial class SettingsPage : SettingsPageAbstract, private void NavigateToRootPage(WinoPage targetPage) { - PageHistory.Clear(); - SettingsFrame.BackStack.Clear(); - SettingsFrame.ForwardStack.Clear(); - - NavigateBreadcrumb(new BreadcrumbNavigationRequested(Translator.MenuSettings, WinoPage.SettingOptionsPage)); + NavigateToSettingsHome(); if (targetPage != WinoPage.SettingOptionsPage) { @@ -213,6 +230,43 @@ public sealed partial class SettingsPage : SettingsPageAbstract, UpdateWindowTitle(); } + private void NavigateToSettingsHome() + { + if (PageHistory.Count == 0 || SettingsFrame.Content == null) + { + ResetToSettingsHome(); + return; + } + + if (PageHistory.Count == 1) + return; + + if (!TryNavigateToBreadcrumbIndex(0)) + { + ResetToSettingsHome(); + } + } + + private bool TryNavigateToBreadcrumbIndex(int targetIndex) + { + if (!BreadcrumbNavigationHelper.NavigateTo(SettingsFrame, PageHistory, targetIndex)) + return false; + + UpdateBackNavigationState(); + _ = RefreshCurrentPageStateAsync(); + UpdateWindowTitle(); + return true; + } + + private void ResetToSettingsHome() + { + PageHistory.Clear(); + SettingsFrame.BackStack.Clear(); + SettingsFrame.ForwardStack.Clear(); + + NavigateBreadcrumb(new BreadcrumbNavigationRequested(Translator.MenuSettings, WinoPage.SettingOptionsPage)); + } + public void ResetForModeSwitch() { while (PageHistory.Count > 1 && SettingsFrame.CanGoBack) diff --git a/Wino.Services/Properties/AssemblyInfo.cs b/Wino.Services/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..8e625798 --- /dev/null +++ b/Wino.Services/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Wino.Core.Tests")] diff --git a/Wino.Services/TranslationService.cs b/Wino.Services/TranslationService.cs index 5d2d36e4..b26e41c1 100644 --- a/Wino.Services/TranslationService.cs +++ b/Wino.Services/TranslationService.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Text.Json; @@ -19,17 +20,19 @@ public class TranslationService : ITranslationService private ILogger _logger = Log.ForContext(); private readonly IPreferencesService _preferencesService; + private readonly IConfigurationService _configurationService; private bool isInitialized = false; public AppLanguageModel CurrentLanguageModel { get; private set; } - public TranslationService(IPreferencesService preferencesService) + public TranslationService(IPreferencesService preferencesService, IConfigurationService configurationService) { _preferencesService = preferencesService; + _configurationService = configurationService; } // Initialize default language with ignoring current language check. - public Task InitializeAsync() => InitializeLanguageAsync(_preferencesService.CurrentLanguage, ignoreCurrentLanguageCheck: true); + public Task InitializeAsync() => InitializeLanguageAsync(GetInitialLanguage(), ignoreCurrentLanguageCheck: true); public async Task InitializeLanguageAsync(AppLanguage language, bool ignoreCurrentLanguageCheck = false) { @@ -65,6 +68,66 @@ public class TranslationService : ITranslationService WeakReferenceMessenger.Default.Send(new LanguageChanged()); } + private AppLanguage GetInitialLanguage() + { + if (_configurationService.Contains(nameof(IPreferencesService.CurrentLanguage))) + return _preferencesService.CurrentLanguage; + + var windowsDisplayLanguage = CultureInfo.CurrentUICulture?.Name; + var initialLanguage = ResolveSupportedLanguage( + [ + windowsDisplayLanguage ?? string.Empty, + CultureInfo.CurrentUICulture?.TwoLetterISOLanguageName ?? string.Empty + ]); + + _logger.Information("No saved app language preference found. Using Windows display language {LanguageTag} -> {Language}.", + string.IsNullOrWhiteSpace(windowsDisplayLanguage) ? "" : windowsDisplayLanguage, + initialLanguage); + + return initialLanguage; + } + + internal static AppLanguage ResolveSupportedLanguage(IEnumerable languageTags) + { + foreach (var languageTag in languageTags) + { + if (string.IsNullOrWhiteSpace(languageTag)) + continue; + + var normalizedLanguageTag = languageTag.Replace('_', '-').Trim(); + var languageCode = normalizedLanguageTag.Split('-')[0]; + + if (TryResolveSupportedLanguage(languageCode, out var supportedLanguage)) + return supportedLanguage; + } + + return DefaultAppLanguage; + } + + private static bool TryResolveSupportedLanguage(string languageCode, out AppLanguage supportedLanguage) + { + supportedLanguage = languageCode.ToLowerInvariant() switch + { + "cs" => AppLanguage.Czech, + "de" => AppLanguage.Deutsch, + "el" => AppLanguage.Greek, + "en" => AppLanguage.English, + "es" => AppLanguage.Spanish, + "fr" => AppLanguage.French, + "id" => AppLanguage.Indonesian, + "it" => AppLanguage.Italian, + "pl" => AppLanguage.Polish, + "pt" => AppLanguage.PortugeseBrazil, + "ro" => AppLanguage.Romanian, + "ru" => AppLanguage.Russian, + "tr" => AppLanguage.Turkish, + "zh" => AppLanguage.Chinese, + _ => AppLanguage.None + }; + + return supportedLanguage != AppLanguage.None; + } + public List GetAvailableLanguages() { return