Browse Source

XAML attached prop translator works

Krzysztof Krysiński 2 năm trước cách đây
mục cha
commit
9bbb4bd94e

+ 5 - 0
src/PixiEditor/Data/Localization/Languages/en.json

@@ -0,0 +1,5 @@
+{
+  "RECENT_FILES": "Recent Files",
+  "OPEN_FILE": "Open",
+  "NEW_FILE": "New"
+}

+ 5 - 0
src/PixiEditor/Data/Localization/Languages/pl.json

@@ -0,0 +1,5 @@
+{
+  "RECENT_FILES": "Ostatnie pliki",
+  "OPEN_FILE": "Otwórz",
+  "NEW_FILE": "Nowy"
+}

+ 8 - 1
src/PixiEditor/Data/Localization/LocalizationData.json

@@ -3,7 +3,14 @@
     {
       "name": "English",
       "code": "en",
-      "localeFileName": "en.json"
+      "localeFileName": "en.json",
+      "iconFileName": "en.png"
+    },
+    {
+      "name": "Polish",
+        "code": "pl",
+        "localeFileName": "pl.json",
+        "iconFileName": "pl.png"
     }
   ]
 }

+ 17 - 0
src/PixiEditor/Helpers/Converters/LangConverter.cs

@@ -0,0 +1,17 @@
+using System.Globalization;
+using PixiEditor.Localization;
+
+namespace PixiEditor.Helpers.Converters;
+
+internal class LangConverter : SingleInstanceConverter<LangConverter>
+{
+    public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+    {
+        if (value is string key)
+        {
+            return new LocalizedString(key);
+        }
+
+        return value;
+    }
+}

+ 1 - 1
src/PixiEditor/Helpers/Extensions/ServiceCollectionHelpers.cs

@@ -23,7 +23,7 @@ internal static class ServiceCollectionHelpers
     public static IServiceCollection AddPixiEditor(this IServiceCollection collection) => collection
         .AddSingleton<ViewModelMain>()
         .AddSingleton<IPreferences, PreferencesSettings>()
-        .AddSingleton<ILocalizationProvider>()
+        .AddSingleton<ILocalizationProvider, LocalizationProvider>()
         // View Models
         .AddSingleton<StylusViewModel>()
         .AddSingleton<WindowViewModel>()

+ 14 - 1
src/PixiEditor/Localization/ILocalizationProvider.cs

@@ -1,6 +1,19 @@
-namespace PixiEditor.Localization;
+using System.IO;
+
+namespace PixiEditor.Localization;
 
 public interface ILocalizationProvider
 {
     public static ILocalizationProvider Current => ViewModelMain.Current.LocalizationProvider;
+    
+    public string LocalizationDataPath { get; }
+    public LocalizationData LocalizationData { get; }
+    public Language CurrentLanguage { get; set; }
+    public event Action<Language> OnLanguageChanged;
+
+    /// <summary>
+    ///     Loads the localization data from the specified file.
+    /// </summary>
+    public void LoadData();
+    public void LoadLanguage(LanguageData languageData);
 }

+ 16 - 0
src/PixiEditor/Localization/Language.cs

@@ -0,0 +1,16 @@
+using System.Diagnostics;
+
+namespace PixiEditor.Localization;
+
+[DebuggerDisplay("{LanguageData.Name}, strings: {Locale.Count}")]
+public class Language
+{
+    public LanguageData LanguageData { get; }
+    public IReadOnlyDictionary<string, string> Locale { get; }
+    
+    public Language(LanguageData languageData, Dictionary<string, string> locale)
+    {
+        LanguageData = languageData;
+        Locale = locale;
+    }
+}

+ 2 - 1
src/PixiEditor/Localization/LanguageData.cs

@@ -3,6 +3,7 @@
 public class LanguageData
 {
     public string Name { get; set; }
-    public string LanguageCode { get; set; }
+    public string Code { get; set; }
     public string LocaleFileName { get; set; }
+    public string IconFileName { get; set; }
 }

+ 5 - 2
src/PixiEditor/Localization/LocalizationData.cs

@@ -1,6 +1,9 @@
-namespace PixiEditor.Localization;
+using System.Diagnostics;
 
+namespace PixiEditor.Localization;
+
+[DebuggerDisplay("{Languages.Length} Language(s)")]
 public class LocalizationData
 {
-    
+    public LanguageData[] Languages { get; set; }
 }

+ 70 - 2
src/PixiEditor/Localization/LocalizationProvider.cs

@@ -1,6 +1,74 @@
-namespace PixiEditor.Localization;
+using System.IO;
+using PixiEditor.Models.Commands.Attributes.Commands;
+
+namespace PixiEditor.Localization;
 
 internal class LocalizationProvider : ILocalizationProvider
 {
-    
+    public string LocalizationDataPath { get; } = Path.Combine("Data", "Localization", "LocalizationData.json");
+    public LocalizationData LocalizationData { get; private set; }
+    public Language CurrentLanguage { get; set; }
+    public event Action<Language> OnLanguageChanged;
+
+    public void LoadData()
+    {
+        Newtonsoft.Json.JsonSerializer serializer = new();
+        
+        if (!File.Exists(LocalizationDataPath))
+        {
+            throw new FileNotFoundException("Localization data file not found.", LocalizationDataPath);
+        }
+        
+        using StreamReader reader = new(LocalizationDataPath);
+        LocalizationData = serializer.Deserialize<LocalizationData>(new Newtonsoft.Json.JsonTextReader(reader));
+            
+        if (LocalizationData is null)
+        {
+            throw new InvalidDataException("Localization data is null.");
+        }
+        
+        if (LocalizationData.Languages is null || LocalizationData.Languages.Length == 0)
+        {
+            throw new InvalidDataException("Localization data does not contain any languages.");
+        }
+        
+        LoadLanguage(LocalizationData.Languages[0]);
+    }
+
+    public void LoadLanguage(LanguageData languageData)
+    {
+        if (languageData is null)
+        {
+            throw new ArgumentNullException(nameof(languageData));
+        }
+        
+        if(languageData.Code == CurrentLanguage?.LanguageData.Code)
+        {
+            return;
+        }
+        
+        string localePath = Path.Combine("Data", "Localization", "Languages", languageData.LocaleFileName);
+        
+        if(!File.Exists(localePath))
+        {
+            throw new FileNotFoundException("Locale file not found.", localePath);
+        }
+        
+        Newtonsoft.Json.JsonSerializer serializer = new();
+        using StreamReader reader = new(localePath);
+        Dictionary<string, string> locale = serializer.Deserialize<Dictionary<string, string>>(new Newtonsoft.Json.JsonTextReader(reader));
+        
+        if (locale is null)
+        {
+            throw new InvalidDataException("Locale is null.");
+        }
+        
+        bool firstLoad = CurrentLanguage is null;
+        CurrentLanguage = new(languageData, locale);
+        
+        if (!firstLoad)
+        {
+            OnLanguageChanged?.Invoke(CurrentLanguage);
+        }
+    }
 }

+ 16 - 0
src/PixiEditor/Localization/LocalizedString.cs

@@ -0,0 +1,16 @@
+namespace PixiEditor.Localization;
+
+public struct LocalizedString
+{
+    public string Key { get; }
+    public string Value { get; }
+
+    public LocalizedString(string key)
+    {
+        Key = key;
+        Value = ILocalizationProvider.Current.CurrentLanguage.Locale.ContainsKey(key) ? ILocalizationProvider.Current.CurrentLanguage.Locale[key] : key;
+    }
+    
+    public static implicit operator LocalizedString(string key) => new(key);
+    public static implicit operator string(LocalizedString localizedString) => localizedString.Value;
+}

+ 6 - 0
src/PixiEditor/PixiEditor.csproj

@@ -446,6 +446,12 @@
     <Content Include="Data\ShortcutActionMaps\*">
       <CopyToOutputDirectory>Always</CopyToOutputDirectory>
     </Content>
+    <Content Include="Data\Localization\*">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </Content>
+    <Content Include="Data\Localization\Languages\*">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </Content>
   </ItemGroup>
   <ItemGroup>
     <Folder Include="Models\Colors" />

+ 4 - 0
src/PixiEditor/ViewModels/ViewModelMain.cs

@@ -6,6 +6,7 @@ using PixiEditor.Helpers;
 using PixiEditor.Helpers.Collections;
 using PixiEditor.Localization;
 using PixiEditor.Models.Commands;
+using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Dialogs;
@@ -87,6 +88,9 @@ internal class ViewModelMain : ViewModelBase
     public void Setup(IServiceProvider services)
     {
         Services = services;
+        
+        LocalizationProvider = services.GetRequiredService<ILocalizationProvider>();
+        LocalizationProvider.LoadData();
 
         Preferences = services.GetRequiredService<IPreferences>();
 

+ 11 - 3
src/PixiEditor/Views/Dialogs/HelloTherePopup.xaml

@@ -9,6 +9,8 @@
         xmlns:uc="clr-namespace:PixiEditor.Views.UserControls"
         xmlns:cmds="clr-namespace:PixiEditor.Models.Commands.XAML"
         xmlns:local="clr-namespace:PixiEditor.Views.Dialogs"
+        xmlns:models="clr-namespace:PixiEditor.Models"
+        xmlns:views="clr-namespace:PixiEditor.Views"
         mc:Ignorable="d" ShowInTaskbar="False"
         Title="Hello there!" Height="662" Width="632" MinHeight="500" MinWidth="500"
         d:DataContext="{d:DesignInstance local:HelloTherePopup}"
@@ -61,12 +63,18 @@
                 </StackPanel>
 
                 <StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Center">
-                    <Button Style="{StaticResource DarkRoundButton}" Command="{Binding OpenFileCommand}" Width="150" Margin="10">Open</Button>
-                    <Button Style="{StaticResource DarkRoundButton}" Command="{Binding OpenNewFileCommand}" Width="150" Margin="10">New</Button>
+                    <Button Style="{StaticResource DarkRoundButton}" Command="{Binding OpenFileCommand}" Width="150" Margin="10"
+                            views:Translator.Key="OPEN_FILE"
+                            Content="{Binding Path=(views:Translator.Value), RelativeSource={RelativeSource Mode=Self}}"/>
+                    <Button Style="{StaticResource DarkRoundButton}" Command="{Binding OpenNewFileCommand}" Width="150" Margin="10"
+                            views:Translator.Key="NEW_FILE"
+                            Content="{Binding Path=(views:Translator.Value), RelativeSource={RelativeSource Mode=Self}}"/>
                 </StackPanel>
 
                 <StackPanel Grid.Row="2" HorizontalAlignment="Center" Margin="0,30,0,0">
-                    <TextBlock FontSize="23" FontWeight="SemiBold" HorizontalAlignment="Center">Recent Files</TextBlock>
+                    <TextBlock FontSize="23" FontWeight="SemiBold" HorizontalAlignment="Center"
+                               views:Translator.Key="RECENT_FILES"
+                               Text="{Binding Path=(views:Translator.Value), RelativeSource={RelativeSource Self}}"/>
                     <TextBlock Margin="0,12.5,0,0" Foreground="LightGray" HorizontalAlignment="Center">
                         <TextBlock.Visibility>
                             <Binding Path="RecentlyOpened.Count"

+ 48 - 0
src/PixiEditor/Views/Translator.cs

@@ -0,0 +1,48 @@
+using System.Windows;
+using PixiEditor.Localization;
+
+namespace PixiEditor.Views;
+
+public class Translator : UIElement
+{
+    private static void CurrentOnOnLanguageChanged(DependencyObject obj, Language newLanguage)
+    {
+        obj.SetValue(ValueProperty, new LocalizedString(GetKey(obj)).Value);
+    }
+
+    public static readonly DependencyProperty KeyProperty = DependencyProperty.RegisterAttached(
+        "Key",
+        typeof(string),
+        typeof(Translator),
+        new FrameworkPropertyMetadata(default(string), FrameworkPropertyMetadataOptions.AffectsRender, PropertyChangedCallback));
+
+    private static void PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
+    {
+        if (e.NewValue is string key)
+        {
+            d.SetValue(ValueProperty, new LocalizedString(key).Value);
+            ILocalizationProvider.Current.OnLanguageChanged += (lang) => CurrentOnOnLanguageChanged(d, lang);
+        }
+    }
+
+    public static readonly DependencyProperty ValueProperty = DependencyProperty.RegisterAttached(
+        "Value",
+        typeof(string),
+        typeof(Translator),
+        new FrameworkPropertyMetadata(default(string), FrameworkPropertyMetadataOptions.AffectsRender));
+
+    public static void SetKey(DependencyObject element, string value)
+    {
+        element.SetValue(KeyProperty, value);
+    }
+    
+    public static string GetKey(DependencyObject element)
+    {
+        return (string)element.GetValue(KeyProperty);
+    }
+
+    public static string GetValue(DependencyObject element)
+    {
+        return (string)element.GetValue(ValueProperty);
+    }
+}