瀏覽代碼

Localization Window WIP

CPKreuz 2 年之前
父節點
當前提交
9d877f1923

+ 13 - 1
src/PixiEditor/Data/Localization/Languages/en.json

@@ -570,5 +570,17 @@
   "REDDIT": "Reddit",
   "GITHUB": "GitHub",
   "YOUTUBE": "YouTube",
-  "DONATE": "Donate"
+  "DONATE": "Donate",
+  
+  "OPEN_LOCALIZATION_DEBUG_WINDOW": "Open Localization Debug Window",
+  "API_KEY": "API Key",
+  "LOCALIZATION_VIEW_TYPE": "Localization View Type",
+  "LOAD_LANGUAGE_FROM_FILE": "Load language from file",
+  
+  "NOT_LOGGED_IN": "Not logged in",
+  "POE_EDITOR_ERROR": "POEditor Error: {0} {1}",
+  "HTTP_ERROR_MESSAGE": "HTTP Error: {0} {1}",
+  "LOGGED_IN": "Logged in",
+  "SYNCED_SUCCESSFULLY": "Synced successfully",
+  "EXCEPTION_ERROR": "Exception: {0}"
 }

+ 36 - 0
src/PixiEditor/Helpers/EnumExtension.cs

@@ -0,0 +1,36 @@
+using System.ComponentModel;
+using System.Windows.Markup;
+
+namespace PixiEditor.Helpers;
+
+public class EnumExtension : MarkupExtension
+{
+    private Type _enumType;
+
+    public EnumExtension(Type enumType)
+    {
+        EnumType = enumType ?? throw new ArgumentNullException(nameof(enumType));
+    }
+
+    public Type EnumType
+    {
+        get { return _enumType; }
+        private set
+        {
+            if (_enumType == value)
+                return;
+
+            var enumType = Nullable.GetUnderlyingType(value) ?? value;
+
+            if (enumType.IsEnum == false)
+                throw new ArgumentException("Type must be an Enum.");
+
+            _enumType = value;
+        }
+    }
+
+    public override object ProvideValue(IServiceProvider serviceProvider) // or IXamlServiceProvider for UWP and WinUI
+    {
+        return Enum.GetValues(EnumType);
+    }
+}

+ 2 - 0
src/PixiEditor/Localization/ILocalizationProvider.cs

@@ -16,5 +16,7 @@ public interface ILocalizationProvider
     /// </summary>
     public void LoadData();
     public void LoadLanguage(LanguageData languageData);
+    public void LoadDebugKeys(Dictionary<string, string> languageKeys);
+    public void ReloadLanguage();
     public Language DefaultLanguage { get; }
 }

+ 17 - 0
src/PixiEditor/Localization/LocalizationProvider.cs

@@ -5,10 +5,13 @@ namespace PixiEditor.Localization;
 
 internal class LocalizationProvider : ILocalizationProvider
 {
+    private Language debugLanguage;
     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 ReloadLanguage() => OnLanguageChanged?.Invoke(CurrentLanguage);
+
     public Language DefaultLanguage { get; private set; }
 
     public void LoadData()
@@ -73,6 +76,20 @@ internal class LocalizationProvider : ILocalizationProvider
         }
     }
 
+    public void LoadDebugKeys(Dictionary<string, string> languageKeys)
+    {
+        debugLanguage = new Language(
+            new LanguageData
+        {
+            Code = "debug",
+            Name = "Debug"
+        }, languageKeys);
+
+        CurrentLanguage = debugLanguage;
+        
+        OnLanguageChanged?.Invoke(debugLanguage);
+    }
+
     private Language LoadLanguageInternal(LanguageData languageData)
     {
         string localePath = Path.Combine("Data", "Localization", "Languages", languageData.LocaleFileName);

+ 10 - 2
src/PixiEditor/Localization/LocalizedString.cs

@@ -1,4 +1,7 @@
-namespace PixiEditor.Localization;
+using System.Text;
+using PixiEditor.Models.Enums;
+
+namespace PixiEditor.Localization;
 
 public struct LocalizedString
 {
@@ -13,7 +16,12 @@ public struct LocalizedString
             #if DEBUG_LOCALIZATION
             Value = key;
             #else
-            Value = GetValue(value);
+            Value = ViewModelMain.Current.DebugSubViewModel?.LocalizationKeyShowMode switch
+            {
+                LocalizationKeyShowMode.Key => Key,
+                LocalizationKeyShowMode.ValueKey => $"{GetValue(value)} ({Key})",
+                _ => GetValue(value)
+            };
             #endif
         }
     }

+ 19 - 0
src/PixiEditor/Models/Enums/LocalizationKeyShowMode.cs

@@ -0,0 +1,19 @@
+namespace PixiEditor.Models.Enums;
+
+internal enum LocalizationKeyShowMode
+{
+    /// <summary>
+    /// Shows just the value e.g. Open
+    /// </summary>
+    Value,
+    
+    /// <summary>
+    /// Shows the value and the key in brackets e.g. Open (OPEN)
+    /// </summary>
+    ValueKey,
+    
+    /// <summary>
+    /// Shows just the key e.g. OPEN
+    /// </summary>
+    Key
+}

+ 33 - 0
src/PixiEditor/ViewModels/SubViewModels/Main/DebugViewModel.cs

@@ -10,6 +10,7 @@ using PixiEditor.Models.Commands.Attributes;
 using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.Models.Commands.Templates.Parsers;
 using PixiEditor.Models.Dialogs;
+using PixiEditor.Models.Enums;
 using PixiEditor.Models.UserPreferences;
 using PixiEditor.Views.Dialogs;
 
@@ -29,6 +30,20 @@ internal class DebugViewModel : SubViewModel<ViewModelMain>
         set => SetProperty(ref useDebug, value);
     }
 
+    private LocalizationKeyShowMode localizationKeyShowMode;
+
+    public LocalizationKeyShowMode LocalizationKeyShowMode
+    {
+        get => localizationKeyShowMode;
+        set
+        {
+            if (SetProperty(ref localizationKeyShowMode, value))
+            {
+                Owner.LocalizationProvider.ReloadLanguage();
+            }
+        }
+    }
+
     public DebugViewModel(ViewModelMain owner, IPreferences preferences)
         : base(owner)
     {
@@ -148,6 +163,24 @@ internal class DebugViewModel : SubViewModel<ViewModelMain>
         Mouse.OverrideCursor = null;
     }
 
+    [Command.Debug("PixiEditor.Debug.OpenLocalizationDebugWindow", "OPEN_LOCALIZATION_DEBUG_WINDOW", "OPEN_LOCALIZATION_DEBUG_WINDOW")]
+    public void OpenLocalizationDebugWindow()
+    {
+        new LocalizationDebugWindow().Show();
+    }
+
+    [Command.Internal("PixiEditor.Debug.SetLanguageFromFilePicker")]
+    public void SetLanguageFromFilePicker()
+    {
+        var file = new OpenFileDialog { Filter = "key-value json (*.json)|*.json" };
+
+        if (file.ShowDialog().GetValueOrDefault())
+        {
+            Owner.LocalizationProvider.LoadDebugKeys(
+                JsonConvert.DeserializeObject<Dictionary<string, string>>(File.ReadAllText(file.FileName)));
+        }
+    }
+
     [Command.Debug("PixiEditor.Debug.OpenInstallDirectory", "OPEN_INSTALLATION_DIR", "OPEN_INSTALLATION_DIR", IconPath = "Folder.png")]
     public static void OpenInstallLocation()
     {

+ 76 - 0
src/PixiEditor/Views/Dialogs/LocalizationDebugWindow.xaml

@@ -0,0 +1,76 @@
+<Window x:Class="PixiEditor.Views.Dialogs.LocalizationDebugWindow"
+        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+        xmlns:local="clr-namespace:PixiEditor.Views.Dialogs"
+        xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
+        xmlns:behaviours="clr-namespace:PixiEditor.Helpers.Behaviours"
+        xmlns:views="clr-namespace:PixiEditor.Views"
+        xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters"
+        xmlns:enums="clr-namespace:PixiEditor.Models.Enums"
+        xmlns:helpers="clr-namespace:PixiEditor.Helpers"
+        xmlns:xaml="clr-namespace:PixiEditor.Models.Commands.XAML"
+        x:Name="popup"
+        mc:Ignorable="d"
+        Foreground="White"
+        Title="LocalizationDebugWindow" 
+        MinHeight="210" MinWidth="480"
+        Height="240" Width="500">
+
+    <WindowChrome.WindowChrome>
+        <WindowChrome CaptionHeight="32"  GlassFrameThickness="0.1"
+                      ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" />
+    </WindowChrome.WindowChrome>
+
+    <Window.CommandBindings>
+        <CommandBinding Command="{x:Static SystemCommands.CloseWindowCommand}" CanExecute="CommandBinding_CanExecute"
+                        Executed="CommandBinding_Executed_Close" />
+    </Window.CommandBindings>
+
+    <Grid Background="{StaticResource AccentColor}" Focusable="True">
+        <Grid.RowDefinitions>
+            <RowDefinition Height="Auto"/>
+            <RowDefinition/>
+        </Grid.RowDefinitions>
+        
+        <b:Interaction.Behaviors>
+            <behaviours:ClearFocusOnClickBehavior/>
+        </b:Interaction.Behaviors>
+
+        <local:DialogTitleBar TitleText="Localization Debug Window" CloseCommand="{x:Static SystemCommands.CloseWindowCommand}" />
+
+        <StackPanel Grid.Row="1" Margin="5">
+            <StackPanel Orientation="Horizontal" Height="25">
+                <TextBlock views:Translator.Key="LOCALIZATION_VIEW_TYPE" Margin="0,0,5,0"></TextBlock>
+                <ComboBox SelectedItem="{Binding DataContext.DebugViewModel.LocalizationKeyShowMode, ElementName=popup}"
+                          ItemsSource="{helpers:Enum {x:Type enums:LocalizationKeyShowMode}}"/>
+            </StackPanel>
+            <Button views:Translator.Key="LOAD_LANGUAGE_FROM_FILE" Command="{xaml:Command PixiEditor.Debug.SetLanguageFromFilePicker}"
+                    Style="{StaticResource DarkRoundButton}" Margin="0,5,0,0"/>
+            <TextBlock Text="POEditor" FontWeight="Bold" FontSize="22" Margin="0,10,0,0"/>
+            <Grid Height="25">
+                <Grid.ColumnDefinitions>
+                    <ColumnDefinition Width="120"/>
+                    <ColumnDefinition/>
+                    <ColumnDefinition Width="100"/>
+                </Grid.ColumnDefinitions>
+                <TextBlock views:Translator.Key="API_KEY" Margin="0,0,5,0"></TextBlock>
+                <TextBox Grid.Column="1" Style="{StaticResource DarkTextBoxStyle}" TextChanged="ApiKeyChanged" Text="{Binding DataContext.ApiKey, ElementName=popup}"></TextBox>
+                <Button Style="{StaticResource DarkRoundButton}" Margin="5,0,0,0" Grid.Column="2" Content="Log in" Command="{Binding DataContext.LoadApiKeyCommand, ElementName=popup}"/>
+            </Grid>
+            <Grid Visibility="{Binding DataContext.LoggedIn, ElementName=popup, Converter={BoolToVisibilityConverter}}" Margin="0,5,0,0" Height="25">
+                <Grid.ColumnDefinitions>
+                    <ColumnDefinition Width="120"/>
+                    <ColumnDefinition/>
+                    <ColumnDefinition Width="100"/>
+                </Grid.ColumnDefinitions>
+                <TextBlock views:Translator.Key="LANGUAGE" Margin="0,0,5,0"/>
+                <ComboBox Grid.Column="1" ItemsSource="{Binding DataContext.LanguageCodes, ElementName=popup}" SelectedItem="{Binding DataContext.SelectedLanguage, ElementName=popup}"
+                          SelectedValue="{Binding DataContext.SelectedLanguage, ElementName=popup}"/>
+                <Button Grid.Column="2" Style="{StaticResource DarkRoundButton}" Content="Sync" Command="{Binding DataContext.SyncLanguageCommand, ElementName=popup}" Margin="5,0,0,0"/>
+            </Grid>
+            <TextBlock Text="{Binding DataContext.StatusMessage, ElementName=popup}"/>
+        </StackPanel>
+    </Grid>
+</Window>

+ 285 - 0
src/PixiEditor/Views/Dialogs/LocalizationDebugWindow.xaml.cs

@@ -0,0 +1,285 @@
+using System.Collections.ObjectModel;
+using System.Net;
+using System.Net.Http;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using PixiEditor.Helpers;
+using PixiEditor.Localization;
+using PixiEditor.Models.UserPreferences;
+
+namespace PixiEditor.Views.Dialogs;
+
+public partial class LocalizationDebugWindow : Window
+{
+    private LocalizationDataContext dataContext;
+
+    public LocalizationDebugWindow()
+    {
+        InitializeComponent();
+        DataContext = dataContext = new LocalizationDataContext(this);
+    }
+
+    private void ApiKeyChanged(object sender, TextChangedEventArgs e)
+    {
+        dataContext.LoggedIn = false;
+        dataContext.StatusMessage = "Not logged in";
+    }
+
+    private void CommandBinding_Executed_Close(object sender, ExecutedRoutedEventArgs e)
+    {
+        SystemCommands.CloseWindow(this);
+    }
+
+    private void CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e)
+    {
+        e.CanExecute = true;
+    }
+
+    private class LocalizationDataContext : NotifyableObject
+    {
+        private readonly LocalizationDebugWindow window;
+        private string apiKey;
+        private bool loggedIn;
+        private LocalizedString statusMessage = "NOT_LOGGED_IN";
+        private string selectedLanguage;
+
+        public DebugViewModel DebugViewModel { get; } = ViewModelMain.Current.DebugSubViewModel;
+
+        public string ApiKey
+        {
+            get => apiKey;
+            set
+            {
+                if (SetProperty(ref apiKey, value))
+                {
+                    PreferencesSettings.Current.UpdateLocalPreference("POEditor_API_Key", apiKey);
+                }
+            }
+        }
+
+        public bool LoggedIn
+        {
+            get => loggedIn;
+            set => SetProperty(ref loggedIn, value);
+        }
+
+        public LocalizedString StatusMessage
+        {
+            get => statusMessage;
+            set => SetProperty(ref statusMessage, value);
+        }
+
+        public string SelectedLanguage
+        {
+            get => selectedLanguage;
+            set => SetProperty(ref selectedLanguage, value);
+        }
+
+        public ObservableCollection<string> LanguageCodes { get; } = new();
+
+        public RelayCommand LoadApiKeyCommand { get; }
+
+        public RelayCommand SyncLanguageCommand { get; }
+
+        public LocalizationDataContext(LocalizationDebugWindow window)
+        {
+            this.window = window;
+            apiKey = PreferencesSettings.Current.GetLocalPreference<string>("POEditor_API_Key");
+            LoadApiKeyCommand = new RelayCommand(LoadApiKey, _ => !string.IsNullOrWhiteSpace(apiKey));
+            SyncLanguageCommand =
+                new RelayCommand(SyncLanguage, _ => loggedIn && !string.IsNullOrWhiteSpace(SelectedLanguage) && SelectedLanguage != "Select your language");
+        }
+
+        private void LoadApiKey(object parameter)
+        {
+            LanguageCodes.Clear();
+            Mouse.OverrideCursor = Cursors.Wait;
+
+            Task.Run(async () =>
+            {
+                try
+                {
+                    var result = await CheckProjectByIdAsync(ApiKey);
+
+                    window.Dispatcher.Invoke(() =>
+                    {
+                        LoggedIn = result.success;
+                        StatusMessage = result.message;
+
+                        if (result.languages != null)
+                        {
+                            foreach (string code in result.languages)
+                            {
+                                LanguageCodes.Add(code);
+                            }
+                        }
+
+                        if (LoggedIn)
+                        {
+                            SelectedLanguage = "Select your language";
+                        }
+                    });
+                }
+                finally
+                {
+                    window.Dispatcher.Invoke(() => Mouse.OverrideCursor = null);
+                }
+            });
+        }
+
+        private void SyncLanguage(object parameter)
+        {
+            Mouse.OverrideCursor = Cursors.Wait;
+            
+            Task.Run(async () =>
+            {
+                try
+                {
+                    var result = await DownloadLanguage(ApiKey, SelectedLanguage);
+
+                    window.Dispatcher.Invoke(() =>
+                    {
+                        StatusMessage = result.status;
+                        DebugViewModel.Owner.LocalizationProvider.LoadDebugKeys(result.response);
+                    });
+                }
+                finally
+                {
+                    window.Dispatcher.Invoke(() => Mouse.OverrideCursor = null);
+                }
+            });
+        }
+
+        private static async Task<(bool success, LocalizedString message, string[] languages)> CheckProjectByIdAsync(string key)
+        {
+            try
+            {
+                using HttpClient httpClient = new HttpClient();
+
+                // --- Check if user is part of project ---
+                var response = await httpClient.PostAsync("https://api.poeditor.com/v2/projects/list",
+                    new FormUrlEncodedContent(new[] { new KeyValuePair<string, string>("api_token", key) }));
+
+                // Failed with an HTTP error code, according to API docs this should not be possible
+                if (!response.IsSuccessStatusCode)
+                {
+                    return (false, new LocalizedString("HTTP_ERROR_MESSAGE", (int)response.StatusCode, response.StatusCode), null);
+                }
+
+                string jsonResponse = await response.Content.ReadAsStringAsync();
+                JObject root = JObject.Parse(jsonResponse);
+
+                var rsp = root["response"];
+                var rspCode = rsp["code"].Value<string>();
+
+                // Failed with an error code from the POEditor API, alongside a message
+                if (rspCode != "200")
+                {
+                    return (false, new LocalizedString("POE_EDITOR_ERROR", rspCode, rsp["message"].Value<string>()), null);
+                }
+
+                var projects = (JArray)root["result"]["projects"];
+
+                // Check if user is part of project
+                if (!projects.Any(x => x["id"].Value<int>() == 400351))
+                {
+                    return (false, new LocalizedString("LOGGED_IN_NO_PROJECT_ACCESS"), null);
+                }
+
+                // --- Fetch languages ---
+                response = await httpClient.PostAsync("https://api.poeditor.com/v2/languages/list",
+                    new FormUrlEncodedContent(new[]
+                    {
+                        new KeyValuePair<string, string>("api_token", key),
+                        new KeyValuePair<string, string>("id", "400351")
+                    }));
+
+                // Failed with an HTTP error code, according to API docs this should not be possible
+                if (!response.IsSuccessStatusCode)
+                {
+                    return (false, new LocalizedString("HTTP_ERROR_MESSAGE", (int)response.StatusCode, response.StatusCode), null);
+                }
+
+                jsonResponse = await response.Content.ReadAsStringAsync();
+                root = JObject.Parse(jsonResponse);
+
+                rsp = root["response"];
+                rspCode = rsp["code"].Value<string>();
+
+                // Failed with an error code from the POEditor API, alongside a message
+                if (rspCode != "200")
+                {
+                    return (false, new LocalizedString("POE_EDITOR_ERROR", rspCode, rsp["message"].Value<string>()), null);
+                }
+
+                var languages = ((JArray)root["result"]["languages"]).Select(x => x["code"].Value<string>());
+
+                return (true, new LocalizedString("LOGGED_IN"), languages.ToArray());
+            }
+            catch (Exception e)
+            {
+                return (false, new LocalizedString("EXCEPTION_ERROR", e.Message), null);
+            }
+        }
+
+        private static async Task<(LocalizedString status, Dictionary<string, string> response)> DownloadLanguage(string key,
+            string language)
+        {
+            try
+            {
+                using HttpClient httpClient = new HttpClient();
+
+                var response = await httpClient.PostAsync(
+                    "https://api.poeditor.com/v2/projects/export",
+                    new FormUrlEncodedContent(new[]
+                    {
+                        new KeyValuePair<string, string>("api_token", key),
+                        new KeyValuePair<string, string>("id", "400351"),
+                        new KeyValuePair<string, string>("type", "key_value_json"),
+                        new KeyValuePair<string, string>("language", language)
+                    }));
+
+
+                // Failed with an HTTP error code, according to API docs this should not be possible
+                if (!response.IsSuccessStatusCode)
+                {
+                    return (new LocalizedString("HTTP_ERROR_MESSAGE", (int)response.StatusCode, response.StatusCode), null);
+                }
+
+                string jsonResponse = await response.Content.ReadAsStringAsync();
+                JObject root = JObject.Parse(jsonResponse);
+
+                var rsp = root["response"];
+                var rspCode = rsp["code"].Value<string>();
+
+                // Failed with an error code from the POEditor API, alongside a message
+                if (rspCode != "200")
+                {
+                    return (new LocalizedString("POE_EDITOR_ERROR", rspCode, rsp["message"].Value<string>()), null);
+                }
+
+                var url = root["result"]["url"].Value<string>();
+
+                response = await httpClient.GetAsync(url);
+
+                // Failed with an HTTP error code, according to API docs this should not be possible
+                if (!response.IsSuccessStatusCode)
+                {
+                    return (new LocalizedString("HTTP_ERROR_MESSAGE", (int)response.StatusCode, response.StatusCode), null);
+                }
+
+                var responseJson = await response.Content.ReadAsStringAsync();
+                var keys = JsonConvert.DeserializeObject<Dictionary<string, string>>(responseJson);
+
+                return (new LocalizedString("SYNCED_SUCCESSFULLY"), keys);
+            }
+            catch (Exception e)
+            {
+                return (new LocalizedString("EXCEPTION_ERROR", e.Message), null);
+            }
+        }
+    }
+}

+ 3 - 0
src/PixiEditor/Views/MainWindow.xaml

@@ -357,6 +357,9 @@
                         <MenuItem
                             views:Translator.Key="OPEN_COMMAND_DEBUG_WINDOW"
                             cmds:Menu.Command="PixiEditor.Debug.OpenCommandDebugWindow"/>
+                        <MenuItem
+                            views:Translator.Key="OPEN_LOCALIZATION_DEBUG_WINDOW"
+                            cmds:Menu.Command="PixiEditor.Debug.OpenLocalizationDebugWindow"/>
                         <Separator/>
                         <MenuItem
                             views:Translator.Key="OPEN_LOCAL_APPDATA_DIR"