From 42e65ab8bb211351dd73b12ac85d3c846f864809 Mon Sep 17 00:00:00 2001 From: CPKreuz Date: Mon, 8 May 2023 19:32:36 +0200 Subject: [PATCH] Overhauled LocalizationDebugWindow UI and added ability to update source --- .../Data/Localization/Languages/en.json | 11 +- .../Data/Localization/LocalizationData.json | 2 +- .../SubViewModels/Main/DebugViewModel.cs | 1 + .../Localization/LocalizationDataContext.cs | 440 ++++++++++++++++++ .../Localization/LocalizationDebugWindow.xaml | 170 +++++++ .../LocalizationDebugWindow.xaml.cs | 39 ++ .../DebugDialogs/Localization/PoeLanguage.cs | 74 +++ .../DebugDialogs/LocalizationDebugWindow.xaml | 98 ---- .../LocalizationDebugWindow.xaml.cs | 343 -------------- 9 files changed, 735 insertions(+), 443 deletions(-) create mode 100644 src/PixiEditor/Views/Dialogs/DebugDialogs/Localization/LocalizationDataContext.cs create mode 100644 src/PixiEditor/Views/Dialogs/DebugDialogs/Localization/LocalizationDebugWindow.xaml create mode 100644 src/PixiEditor/Views/Dialogs/DebugDialogs/Localization/LocalizationDebugWindow.xaml.cs create mode 100644 src/PixiEditor/Views/Dialogs/DebugDialogs/Localization/PoeLanguage.cs delete mode 100644 src/PixiEditor/Views/Dialogs/DebugDialogs/LocalizationDebugWindow.xaml delete mode 100644 src/PixiEditor/Views/Dialogs/DebugDialogs/LocalizationDebugWindow.xaml.cs diff --git a/src/PixiEditor/Data/Localization/Languages/en.json b/src/PixiEditor/Data/Localization/Languages/en.json index d40144abf..d187c4974 100644 --- a/src/PixiEditor/Data/Localization/Languages/en.json +++ b/src/PixiEditor/Data/Localization/Languages/en.json @@ -540,5 +540,14 @@ "REMOVE": "Remove", "FILE_FORMAT_NOT_ASEPRITE_KEYS": "File is not a \".aseprite-keys\" file", "FILE_HAS_INVALID_SHORTCUT": "The file contains an invalid shortcut", - "FILE_EXTENSION_NOT_SUPPORTED": "The file type '{0}' is not supported" + "FILE_EXTENSION_NOT_SUPPORTED": "The file type '{0}' is not supported", + "OPEN_LOCALIZATION_DATA": "Do you want to open the LocalizationData.json?\nThe updated date has been put in the clipboard.\nNote that changes wont be applied until a restart", + "DOWNLOADING_LANGUAGE_FAILED": "Downloading language failed.\nAPI Key might have been overused.", + "LOCALIZATION_DATA_NOT_FOUND": "Localization data path not found", + "APPLY": "Apply", + "UPDATE_SOURCE": "Update source", + "COPY_TO_CLIPBOARD": "Copy to clipboard", + "LANGUAGE_FILE_NOT_FOUND": "Language file not found.\nLooking for {0}", + "PROJECT_ROOT_NOT_FOUND": "PixiEditor Project root not found.\nLooking for PixiEditor.csproj", + "LOCALIZATION_FOLDER_NOT_FOUND": "Localization folder not found.\nLooking for /Data/Localization" } \ No newline at end of file diff --git a/src/PixiEditor/Data/Localization/LocalizationData.json b/src/PixiEditor/Data/Localization/LocalizationData.json index c7067faae..0138e3baf 100644 --- a/src/PixiEditor/Data/Localization/LocalizationData.json +++ b/src/PixiEditor/Data/Localization/LocalizationData.json @@ -6,7 +6,7 @@ "code": "en", "localeFileName": "en.json", "iconFileName": "en.png", - "lastUpdated": "2023-05-07 13:31:45" + "lastUpdated": "2023-05-08 19:30:00" }, { "name": "Polski", diff --git a/src/PixiEditor/ViewModels/SubViewModels/Main/DebugViewModel.cs b/src/PixiEditor/ViewModels/SubViewModels/Main/DebugViewModel.cs index 1dd1ed681..2c2a8ee08 100644 --- a/src/PixiEditor/ViewModels/SubViewModels/Main/DebugViewModel.cs +++ b/src/PixiEditor/ViewModels/SubViewModels/Main/DebugViewModel.cs @@ -13,6 +13,7 @@ using PixiEditor.Models.Enums; using PixiEditor.Models.UserPreferences; using PixiEditor.Views.Dialogs.DebugDialogs; +using PixiEditor.Views.Dialogs.DebugDialogs.Localization; namespace PixiEditor.ViewModels.SubViewModels.Main; diff --git a/src/PixiEditor/Views/Dialogs/DebugDialogs/Localization/LocalizationDataContext.cs b/src/PixiEditor/Views/Dialogs/DebugDialogs/Localization/LocalizationDataContext.cs new file mode 100644 index 000000000..3c690c03f --- /dev/null +++ b/src/PixiEditor/Views/Dialogs/DebugDialogs/Localization/LocalizationDataContext.cs @@ -0,0 +1,440 @@ +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Net.Http; +using System.Windows; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Threading; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PixiEditor.Helpers; +using PixiEditor.Localization; +using PixiEditor.Models.Dialogs; +using PixiEditor.Models.UserPreferences; + +namespace PixiEditor.Views.Dialogs.DebugDialogs.Localization; + +internal class LocalizationDataContext : NotifyableObject +{ + private const int ProjectId = 400351; + + private Dispatcher dispatcher; + private string apiKey; + private bool loggedIn; + private LocalizedString statusMessage = "NOT_LOGGED_IN"; + private PoeLanguage 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 PoeLanguage SelectedLanguage + { + get => selectedLanguage; + set => SetProperty(ref selectedLanguage, value); + } + + public ObservableCollection LanguageCodes { get; } = new(); + + public RelayCommand LoadApiKeyCommand { get; } + + public RelayCommand ApplyLanguageCommand { get; } + + public RelayCommand CopySelectedUpdatedCommand { get; } + + public RelayCommand UpdateSourceCommand { get; } + + public LocalizationDataContext() + { + dispatcher = Application.Current.Dispatcher; + apiKey = PreferencesSettings.Current.GetLocalPreference("POEditor_API_Key"); + LoadApiKeyCommand = new RelayCommand(LoadApiKey, _ => !string.IsNullOrWhiteSpace(apiKey)); + ApplyLanguageCommand = + new RelayCommand(ApplyLanguage, _ => loggedIn && SelectedLanguage != null); + CopySelectedUpdatedCommand = new RelayCommand(_ => + Clipboard.SetText(SelectedLanguage.UpdatedUTC.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture))); + UpdateSourceCommand = new RelayCommand(UpdateSource); + } + + private void LoadApiKey(object parameter) + { + LanguageCodes.Clear(); + Mouse.OverrideCursor = Cursors.Wait; + + Task.Run(async () => + { + try + { + var result = await CheckProjectByIdAsync(ApiKey); + + dispatcher.Invoke(() => + { + LoggedIn = result.IsSuccess; + StatusMessage = result.Message; + + if (!result.IsSuccess) + { + return; + } + + foreach (var language in result.Output + .OrderByDescending(x => + CultureInfo.CurrentUICulture.TwoLetterISOLanguageName == x.Code || + CultureInfo.InstalledUICulture.TwoLetterISOLanguageName == x.Code) + .ThenByDescending(x => x.UpdatedUTC)) + { + language.LocalEquivalent = ILocalizationProvider.Current.LocalizationData.Languages + .OrderByDescending(x => language.Code == x.Code) + .FirstOrDefault(x => language.Code.StartsWith(x.Code)); + + LanguageCodes.Add(language); + } + }); + } + catch (Exception e) + { + LoggedIn = false; + StatusMessage = new LocalizedString("EXCEPTION_ERROR", e.Message); + } + finally + { + dispatcher.Invoke(() => Mouse.OverrideCursor = null); + } + }); + } + + private void ApplyLanguage(object parameter) + { + Mouse.OverrideCursor = Cursors.Wait; + + Task.Run(async () => + { + try + { + var result = await DownloadLanguage(ApiKey, SelectedLanguage.Code); + + dispatcher.Invoke(() => + { + StatusMessage = result.Message; + DebugViewModel.Owner.LocalizationProvider.LoadDebugKeys(result.Output, + SelectedLanguage.IsRightToLeft); + }); + } + catch (Exception e) + { + StatusMessage = new LocalizedString("EXCEPTION_ERROR", e.Message); + } + finally + { + dispatcher.Invoke(() => Mouse.OverrideCursor = null); + } + }); + } + + private void UpdateSource(object obj) + { + if (!GetProjectRoot(out var localizationRoot)) + { + return; + } + + var dataPath = Path.Combine(localizationRoot, "LocalizationData.json"); + + if (!File.Exists(dataPath)) + { + NoticeDialog.Show("LOCALIZATION_DATA_NOT_FOUND", "ERROR"); + } + + string code = SelectedLanguage.Code; + + if (!GetLanguageFile(code, localizationRoot, out string languagePath)) + { + return; + } + + Task.Run(async () => await UpdateSourceAsync(code, languagePath, dataPath)); + } + + private async Task UpdateSourceAsync(string code, string path, string dataPath) + { + // Fetch latest data to make sure data is up to date + var languages = await CheckProjectByIdAsync(apiKey); + + if (!languages.IsSuccess) + { + dispatcher.Invoke(() => + { + NoticeDialog.Show(new LocalizedString("DOWNLOADING_LANGUAGE_FAILED", languages.Message), "ERROR"); + }); + } + + var language = languages.Output.First(x => x.Code == code); + + try + { + var languageData = await DownloadLanguage(apiKey, code); + + if (!languageData.IsSuccess) + { + dispatcher.Invoke(() => + { + NoticeDialog.Show(new LocalizedString("DOWNLOADING_LANGUAGE_FAILED", languageData.Message), "ERROR"); + }); + } + + await File.WriteAllTextAsync(path, JsonConvert.SerializeObject(languageData.Output, Formatting.Indented)); + } + catch (Exception e) + { + dispatcher.Invoke(() => + { + NoticeDialog.Show(new LocalizedString("DOWNLOADING_LANGUAGE_FAILED", e), "ERROR"); + }); + } + + dispatcher.Invoke(() => + { + Clipboard.SetText(language.UpdatedUTC.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)); + + var dialog = new OptionsDialog("SUCCESS", new LocalizedString("OPEN_LOCALIZATION_DATA")); + dialog["VS Code"] = _ => ProcessHelpers.ShellExecute($"vscode://file/{dataPath}"); + dialog[new LocalizedString("DEFAULT")] = _ => ProcessHelpers.ShellExecute(dataPath); + dialog[new LocalizedString("CANCEL")] = null; + + dialog.ShowDialog(); + }); + } + + private static bool GetLanguageFile(string code, string root, [NotNullWhen(true)] out string? languagePath) + { + root = Path.Combine(root, "Languages"); + + languagePath = null; + string file; + + if (code.Length == 2) + { + file = Path.Combine(root, $"{code}.json"); + languagePath = file; + + if (!File.Exists(file)) + { + File.Create(file); + } + + return true; + } + + file = Path.Combine(root, $"{code}.json"); + + if (File.Exists(file)) + { + languagePath = file; + return true; + } + + string file2 = Path.Combine(root, $"{code[..2]}.json"); + + if (File.Exists(file2)) + { + languagePath = file2; + return true; + } + + NoticeDialog.Show(new LocalizedString("LANGUAGE_FILE_NOT_FOUND", $"{Path.GetFileName(file)} or {Path.GetFileName(file2)}"), "ERROR"); + return false; + } + + private static bool GetProjectRoot([NotNullWhen(true)] out string? root) + { + const string fileName = "PixiEditor.csproj"; + root = Directory.GetCurrentDirectory(); + + while (root != null) + { + string[] files = Directory.GetFiles(root, fileName, SearchOption.TopDirectoryOnly); + + if (files.Length > 0) + { + // Found the file in the current directory + break; + } + + // Move up to the parent directory + root = Directory.GetParent(root)?.FullName; + } + + if (!Directory.Exists(root)) + { + NoticeDialog.Show("PROJECT_ROOT_NOT_FOUND", "ERROR"); + return false; + } + + root = Path.Combine(root, "Data", "Localization"); + + if (!Directory.Exists(root)) + { + NoticeDialog.Show("LOCALIZATION_FOLDER_NOT_FOUND", "ERROR"); + return false; + } + + return true; + } + + private static async Task> + CheckProjectByIdAsync(string key) + { + using HttpClient client = new HttpClient(); + + // --- Check if user is part of project --- + var response = await PostAsync(client, "https://api.poeditor.com/v2/projects/list", key); + var result = await ParseResponseAsync(response); + + if (!result.IsSuccess) + { + return result.As(); + } + + var projects = (JArray)result.Output["result"]["projects"]; + + // Check if user is part of project + if (!projects.Any(x => x["id"].Value() == ProjectId)) + { + return Error("LOGGED_IN_NO_PROJECT_ACCESS"); + } + + response = await PostAsync(client, "https://api.poeditor.com/v2/languages/list", key, + ("id", ProjectId.ToString())); + result = await ParseResponseAsync(response); + + if (!result.IsSuccess) + { + return result.As(); + } + + var languages = result.Output["result"]["languages"].ToObject(); + + return Result.Success(new LocalizedString("LOGGED_IN"), languages); + + Result Error(LocalizedString message) => Result.Error(message); + } + + private static async Task>> DownloadLanguage( + string key, + string language) + { + using var client = new HttpClient(); + + // Get Url to key_value_json in language + var response = await PostAsync( + client, + "https://api.poeditor.com/v2/projects/export", + key, + ("id", ProjectId.ToString()), ("type", "key_value_json"), ("language", language)); + + var result = await ParseResponseAsync(response); + + if (!result.IsSuccess) + { + return result.As>(); + } + + response = await client.GetAsync(result.Output["result"]["url"].Value()); + + // Failed with an HTTP error code, according to API docs this should not be possible + if (!response.IsSuccessStatusCode) + { + return Error(new LocalizedString("HTTP_ERROR_MESSAGE", (int)response.StatusCode, response.StatusCode)); + } + + string responseJson = await response.Content.ReadAsStringAsync(); + var keys = JsonConvert.DeserializeObject>(responseJson); + + return Result.Success("SYNCED_SUCCESSFULLY", keys); + + Result> Error(LocalizedString message) => + Result.Error>(message); + } + + private static async Task> ParseResponseAsync(HttpResponseMessage response) + { + // Failed with an HTTP error code, according to API docs this should not be possible + if (!response.IsSuccessStatusCode) + { + return Error("HTTP_ERROR_MESSAGE", (int)response.StatusCode, response.StatusCode); + } + + string jsonResponse = await response.Content.ReadAsStringAsync(); + var root = JObject.Parse(jsonResponse); + + var rsp = root["response"]; + string rspCode = rsp["code"].Value(); + + // Failed with an error code from the POEditor API, alongside a message + if (rspCode != "200") + { + return Error("POE_EDITOR_ERROR", rspCode, rsp["message"].Value()); + } + + return Result.Success(root); + + Result Error(string key, params object[] param) => + Result.Error(new LocalizedString(key, param)); + } + + private static Task PostAsync(HttpClient client, string requestUri, string apiKey, + params (string key, string value)[] body) + { + var bodyKeys = new List>( + body.Select(x => new KeyValuePair(x.key, x.value))) { new("api_token", apiKey) }; + + return client.PostAsync(requestUri, new FormUrlEncodedContent(bodyKeys)); + } + + private struct Result + { + public static Result Error(LocalizedString message) => new(false, message, default); + + public static Result Success(LocalizedString message, T output) => new(true, message, output); + + public static Result Success(T output) => new(true, null, output); + } + + private record struct Result(bool IsSuccess, LocalizedString Message, T Output) + { + public Result As() + { + if (IsSuccess) + { + throw new ArgumentException("Result can't be a success"); + } + + return new Result(false, Message, default); + } + } +} diff --git a/src/PixiEditor/Views/Dialogs/DebugDialogs/Localization/LocalizationDebugWindow.xaml b/src/PixiEditor/Views/Dialogs/DebugDialogs/Localization/LocalizationDebugWindow.xaml new file mode 100644 index 000000000..b54d23d84 --- /dev/null +++ b/src/PixiEditor/Views/Dialogs/DebugDialogs/Localization/LocalizationDebugWindow.xaml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +