|
@@ -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<PoeLanguage> 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<string>("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<string>("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<Result<PoeLanguage[]>>
|
|
|
+ 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<PoeLanguage[]>();
|
|
|
+ }
|
|
|
+
|
|
|
+ var projects = (JArray)result.Output["result"]["projects"];
|
|
|
+
|
|
|
+ // Check if user is part of project
|
|
|
+ if (!projects.Any(x => x["id"].Value<int>() == 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<PoeLanguage[]>();
|
|
|
+ }
|
|
|
+
|
|
|
+ var languages = result.Output["result"]["languages"].ToObject<PoeLanguage[]>();
|
|
|
+
|
|
|
+ return Result.Success(new LocalizedString("LOGGED_IN"), languages);
|
|
|
+
|
|
|
+ Result<PoeLanguage[]> Error(LocalizedString message) => Result.Error<PoeLanguage[]>(message);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static async Task<Result<Dictionary<string, string>>> 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<Dictionary<string, string>>();
|
|
|
+ }
|
|
|
+
|
|
|
+ response = await client.GetAsync(result.Output["result"]["url"].Value<string>());
|
|
|
+
|
|
|
+ // 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<Dictionary<string, string>>(responseJson);
|
|
|
+
|
|
|
+ return Result.Success("SYNCED_SUCCESSFULLY", keys);
|
|
|
+
|
|
|
+ Result<Dictionary<string, string>> Error(LocalizedString message) =>
|
|
|
+ Result.Error<Dictionary<string, string>>(message);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static async Task<Result<JObject>> 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<string>();
|
|
|
+
|
|
|
+ // Failed with an error code from the POEditor API, alongside a message
|
|
|
+ if (rspCode != "200")
|
|
|
+ {
|
|
|
+ return Error("POE_EDITOR_ERROR", rspCode, rsp["message"].Value<string>());
|
|
|
+ }
|
|
|
+
|
|
|
+ return Result.Success(root);
|
|
|
+
|
|
|
+ Result<JObject> Error(string key, params object[] param) =>
|
|
|
+ Result.Error<JObject>(new LocalizedString(key, param));
|
|
|
+ }
|
|
|
+
|
|
|
+ private static Task<HttpResponseMessage> PostAsync(HttpClient client, string requestUri, string apiKey,
|
|
|
+ params (string key, string value)[] body)
|
|
|
+ {
|
|
|
+ var bodyKeys = new List<KeyValuePair<string, string>>(
|
|
|
+ body.Select(x => new KeyValuePair<string, string>(x.key, x.value))) { new("api_token", apiKey) };
|
|
|
+
|
|
|
+ return client.PostAsync(requestUri, new FormUrlEncodedContent(bodyKeys));
|
|
|
+ }
|
|
|
+
|
|
|
+ private struct Result
|
|
|
+ {
|
|
|
+ public static Result<T> Error<T>(LocalizedString message) => new(false, message, default);
|
|
|
+
|
|
|
+ public static Result<T> Success<T>(LocalizedString message, T output) => new(true, message, output);
|
|
|
+
|
|
|
+ public static Result<T> Success<T>(T output) => new(true, null, output);
|
|
|
+ }
|
|
|
+
|
|
|
+ private record struct Result<T>(bool IsSuccess, LocalizedString Message, T Output)
|
|
|
+ {
|
|
|
+ public Result<TOther> As<TOther>()
|
|
|
+ {
|
|
|
+ if (IsSuccess)
|
|
|
+ {
|
|
|
+ throw new ArgumentException("Result can't be a success");
|
|
|
+ }
|
|
|
+
|
|
|
+ return new Result<TOther>(false, Message, default);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|