diff --git a/.github/workflows/crowdin-sync.yml b/.github/workflows/crowdin-sync.yml new file mode 100644 index 0000000..54a2ff7 --- /dev/null +++ b/.github/workflows/crowdin-sync.yml @@ -0,0 +1,43 @@ +name: Synchronize Crowdin Changes + +on: + workflow_dispatch: + +jobs: + synchronize-with-crowdin: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Get Branch Name + shell: bash + run: echo "::set-output name=branch::${GITHUB_REF#refs/heads/}" + id: branch + - name: Update localization + uses: crowdin/github-action@1.1.0 + with: + upload_translations: true + upload_sources: true + auto_approve_imported: true + import_eq_suggestions: true + + download_translations: true + skip_untranslated_strings: true + skip_untranslated_files: true + export_only_approved: true + push_translations: true + commit_message: '[Crowdin] Synchronize Localization Changes' + create_pull_request: true + pull_request_title: '[Crowdin] Synchronize Localization Changes' + pull_request_body: 'Update localization with latest translations from crowdin' + + crowdin_branch_name: ${{ steps.branch.outputs.branch }} + localization_branch_name: 'crowdin-${{ steps.branch.outputs.branch }}' + pull_request_base_branch_name: ${{ steps.branch.outputs.branch }} + + config: 'crowdin.yml' + + env: + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} + CROWDIN_PROJECT_ID: ${{secrets.CROWDIN_PROJECT_ID}} + CROWDIN_PERSONAL_TOKEN: ${{secrets.CROWDIN_PERSONAL_TOKEN}} \ No newline at end of file diff --git a/.github/workflows/crowdin-upload.yml b/.github/workflows/crowdin-upload.yml new file mode 100644 index 0000000..dbfe311 --- /dev/null +++ b/.github/workflows/crowdin-upload.yml @@ -0,0 +1,26 @@ +name: Upload to Crowdin + +on: + push: + branches: [ localization ] + +jobs: + crowdin-upload: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Get Branch Name + shell: bash + run: echo "::set-output name=branch::${GITHUB_REF#refs/heads/}" + id: branch + - name: Crowdin Action + uses: crowdin/github-action@1.0.4 + with: + upload_sources: true + upload_translations: true + download_translations: false + crowdin_branch_name: ${{ steps.branch.outputs.branch }} + env: + CROWDIN_PROJECT_ID: ${{secrets.CROWDIN_PROJECT_ID}} + CROWDIN_PERSONAL_TOKEN: ${{secrets.CROWDIN_PERSONAL_TOKEN}} \ No newline at end of file diff --git a/FFXIVPlugin/ActionExecutor/FixedCommandStrategy.cs b/FFXIVPlugin/ActionExecutor/FixedCommandStrategy.cs index dccc375..0a6e1e6 100644 --- a/FFXIVPlugin/ActionExecutor/FixedCommandStrategy.cs +++ b/FFXIVPlugin/ActionExecutor/FixedCommandStrategy.cs @@ -6,6 +6,7 @@ using Lumina.Excel; using XIVDeck.FFXIVPlugin.Base; using XIVDeck.FFXIVPlugin.Game; +using XIVDeck.FFXIVPlugin.Resources.Localization; using XIVDeck.FFXIVPlugin.Utils; namespace XIVDeck.FFXIVPlugin.ActionExecutor; @@ -37,7 +38,7 @@ public List GetAllowedItems() { ExcelSheet sheet = Injections.DataManager.Excel.GetSheet()!; if (sheet == null) { - throw new NullReferenceException($"A sheet of type {typeof(T).Name} does not exist."); + throw new NullReferenceException(string.Format(UIStrings.FixedCommandStrategy_SheetNotFoundError, typeof(T).Name)); } foreach (var row in sheet) { @@ -68,13 +69,13 @@ public List GetAllowedItems() { public void Execute(uint actionId, dynamic? options = null) { if (this.GetIllegalActionIDs().Contains(actionId)) - throw new ArgumentException($"The action with ID {actionId} is marked as illegal and cannot be used."); + throw new ArgumentException(string.Format(UIStrings.FixedCommandStrategy_IllegalActionError, actionId)); var action = GetActionById(actionId); if (action == null) { throw new ArgumentNullException(nameof(actionId), - $"An action of type {typeof(T)} with ID {actionId} does not exist."); + string.Format(UIStrings.FixedCommandStrategy_ActionNotFoundError, typeof(T), actionId)); } var command = this.GetCommandToCallAction(action); diff --git a/FFXIVPlugin/ActionExecutor/Strategies/CollectionStrategy.cs b/FFXIVPlugin/ActionExecutor/Strategies/CollectionStrategy.cs index 67f687d..2d74b41 100644 --- a/FFXIVPlugin/ActionExecutor/Strategies/CollectionStrategy.cs +++ b/FFXIVPlugin/ActionExecutor/Strategies/CollectionStrategy.cs @@ -5,6 +5,7 @@ using XIVDeck.FFXIVPlugin.Base; using XIVDeck.FFXIVPlugin.Game; using XIVDeck.FFXIVPlugin.Game.Sheets; +using XIVDeck.FFXIVPlugin.Resources.Localization; namespace XIVDeck.FFXIVPlugin.ActionExecutor.Strategies; @@ -53,7 +54,7 @@ public void Execute(uint actionId, dynamic? _) { var mcguffin = GetMcGuffinById(actionId); if (mcguffin == null) { - throw new ArgumentOutOfRangeException(nameof(actionId), $"No Collection with ID {actionId} exists."); + throw new ArgumentOutOfRangeException(nameof(actionId), string.Format(UIStrings.CollectionStrategy_CollectionNotFoundError, actionId)); } TickScheduler.Schedule(delegate { diff --git a/FFXIVPlugin/ActionExecutor/Strategies/EmoteStrategy.cs b/FFXIVPlugin/ActionExecutor/Strategies/EmoteStrategy.cs index 54a22e1..f8cb024 100644 --- a/FFXIVPlugin/ActionExecutor/Strategies/EmoteStrategy.cs +++ b/FFXIVPlugin/ActionExecutor/Strategies/EmoteStrategy.cs @@ -6,6 +6,7 @@ using XIVDeck.FFXIVPlugin.Base; using XIVDeck.FFXIVPlugin.Exceptions; using XIVDeck.FFXIVPlugin.Game; +using XIVDeck.FFXIVPlugin.Resources.Localization; using XIVDeck.FFXIVPlugin.Utils; namespace XIVDeck.FFXIVPlugin.ActionExecutor.Strategies; @@ -50,11 +51,11 @@ public void Execute(uint actionId, dynamic? _) { var textCommand = emote.TextCommand.Value; if (textCommand == null) { - throw new KeyNotFoundException($"The emote \"{emote.Name}\" does not have an associated text command."); + throw new KeyNotFoundException(string.Format(UIStrings.EmoteStrategy_EmoteDoesntHaveCommandError, emote.Name)); } if (!GameStateCache.IsEmoteUnlocked(emote.RowId)) { - throw new ActionLockedException($"The emote \"{emote.Name}\" isn't unlocked and therefore can't be used."); + throw new ActionLockedException(string.Format(UIStrings.EmoteStrategy_EmoteLockedError, emote.Name)); } PluginLog.Debug($"Would execute command: {textCommand.Command}"); diff --git a/FFXIVPlugin/ActionExecutor/Strategies/ExtraCommandStrategy.cs b/FFXIVPlugin/ActionExecutor/Strategies/ExtraCommandStrategy.cs index c8c7fc0..a4ab6f0 100644 --- a/FFXIVPlugin/ActionExecutor/Strategies/ExtraCommandStrategy.cs +++ b/FFXIVPlugin/ActionExecutor/Strategies/ExtraCommandStrategy.cs @@ -1,6 +1,7 @@ using System; using FFXIVClientStructs.FFXIV.Client.UI.Misc; using XIVDeck.FFXIVPlugin.Game.Sheets; +using XIVDeck.FFXIVPlugin.Resources.Localization; namespace XIVDeck.FFXIVPlugin.ActionExecutor.Strategies; @@ -24,7 +25,7 @@ protected override string GetCommandToCallAction(ExtraCommand action) { 1 => "/grouppose", 2 => "/idlingcamera", 3 => "/alarm", - _ => throw new ArgumentException($"No command exists for Extra Command {action.Name}. REPORT THIS BUG!") + _ => throw new ArgumentException(string.Format(UIStrings.ExtraCommandStrategy_NoCommandError, action.Name)) }; } } \ No newline at end of file diff --git a/FFXIVPlugin/ActionExecutor/Strategies/GearsetStrategy.cs b/FFXIVPlugin/ActionExecutor/Strategies/GearsetStrategy.cs index ca74183..49cca28 100644 --- a/FFXIVPlugin/ActionExecutor/Strategies/GearsetStrategy.cs +++ b/FFXIVPlugin/ActionExecutor/Strategies/GearsetStrategy.cs @@ -5,6 +5,7 @@ using Dalamud.Memory; using FFXIVClientStructs.FFXIV.Client.UI.Misc; using XIVDeck.FFXIVPlugin.Game; +using XIVDeck.FFXIVPlugin.Resources.Localization; using XIVDeck.FFXIVPlugin.Utils; namespace XIVDeck.FFXIVPlugin.ActionExecutor.Strategies; @@ -55,7 +56,7 @@ public void Execute(uint actionSlot, dynamic? _) { var gearset = GetGearsetBySlot(actionSlot); if (gearset == null) - throw new ArgumentException($"No gearset exists in slot number {actionSlot}."); + throw new ArgumentException(string.Format(UIStrings.GearsetStrategy_GearsetNotFoundError, actionSlot)); var command = $"/gearset change {gearset.Value.Slot + 1}"; diff --git a/FFXIVPlugin/ActionExecutor/Strategies/GeneralActionStrategy.cs b/FFXIVPlugin/ActionExecutor/Strategies/GeneralActionStrategy.cs index e24fffd..b59e03c 100644 --- a/FFXIVPlugin/ActionExecutor/Strategies/GeneralActionStrategy.cs +++ b/FFXIVPlugin/ActionExecutor/Strategies/GeneralActionStrategy.cs @@ -9,6 +9,7 @@ using XIVDeck.FFXIVPlugin.Base; using XIVDeck.FFXIVPlugin.Exceptions; using XIVDeck.FFXIVPlugin.Game; +using XIVDeck.FFXIVPlugin.Resources.Localization; using XIVDeck.FFXIVPlugin.Utils; namespace XIVDeck.FFXIVPlugin.ActionExecutor.Strategies; @@ -70,16 +71,16 @@ public unsafe void Execute(uint actionId, dynamic? _) { var action = GetActionById(actionId); if (action == null) { - throw new ArgumentOutOfRangeException(nameof(actionId), $"No action with ID {actionId} exists."); + throw new ActionNotFoundException(HotbarSlotType.GeneralAction, actionId); } if (this.GetIllegalActionIDs().Contains(actionId)) { throw new ArgumentOutOfRangeException(nameof(actionId), - $"The action \"{action.Name}\" (ID {actionId}) is marked as illegal and cannot be used."); + string.Format(UIStrings.GeneralActionStrategy_ActionIllegalError, action.Name, actionId)); } if (action.UnlockLink != 0 && !UIState.Instance()->IsUnlockLinkUnlocked(action.UnlockLink)) { - throw new ActionLockedException($"The action \"{action.Name}\" is not yet unlocked."); + throw new ActionLockedException(string.Format(UIStrings.GeneralActionStrategy_ActionLockedError, action.Name)); } if (actionId == 12 && UIState.Instance()->IsUnlockLinkUnlocked(12)) { diff --git a/FFXIVPlugin/ActionExecutor/Strategies/InstrumentStrategy.cs b/FFXIVPlugin/ActionExecutor/Strategies/InstrumentStrategy.cs index d0389f0..48e47e3 100644 --- a/FFXIVPlugin/ActionExecutor/Strategies/InstrumentStrategy.cs +++ b/FFXIVPlugin/ActionExecutor/Strategies/InstrumentStrategy.cs @@ -9,6 +9,7 @@ using XIVDeck.FFXIVPlugin.Base; using XIVDeck.FFXIVPlugin.Exceptions; using XIVDeck.FFXIVPlugin.Game; +using XIVDeck.FFXIVPlugin.Resources.Localization; namespace XIVDeck.FFXIVPlugin.ActionExecutor.Strategies; @@ -49,17 +50,17 @@ public void Execute(uint actionId, dynamic? _) { // about this. if (!this.IsPerformUnlocked()) { - throw new ActionLockedException("Performance mode hasn't yet been unlocked."); + throw new ActionLockedException(UIStrings.InstrumentStrategy_PerformanceLockedError); } if (Injections.Condition[ConditionFlag.Performing]) { - throw new IllegalGameStateException("Cannot switch instruments while actively in Perform mode."); + throw new IllegalGameStateException(UIStrings.InstrumentStrategy_CurrentlyPerformingError); } var instrument = GetActionById(actionId); if (instrument == null) { - throw new ArgumentOutOfRangeException(nameof(actionId), $"No instrument with ID {actionId} exists."); + throw new ArgumentOutOfRangeException(nameof(actionId), string.Format(UIStrings.InstrumentStrategy_InstrumentNotFoundError, actionId)); } TickScheduler.Schedule(delegate { diff --git a/FFXIVPlugin/ActionExecutor/Strategies/MacroStrategy.cs b/FFXIVPlugin/ActionExecutor/Strategies/MacroStrategy.cs index d127463..a4e3d4c 100644 --- a/FFXIVPlugin/ActionExecutor/Strategies/MacroStrategy.cs +++ b/FFXIVPlugin/ActionExecutor/Strategies/MacroStrategy.cs @@ -4,6 +4,7 @@ using FFXIVClientStructs.FFXIV.Client.UI.Shell; using XIVDeck.FFXIVPlugin.Exceptions; using XIVDeck.FFXIVPlugin.Game; +using XIVDeck.FFXIVPlugin.Resources.Localization; namespace XIVDeck.FFXIVPlugin.ActionExecutor.Strategies; @@ -44,7 +45,7 @@ public unsafe void Execute(uint actionId, dynamic? _) { // Safety check to make sure we aren't triggering an empty macro if (RaptureMacroModule.Instance->GetLineCount(macro) == 0) { - throw new IllegalGameStateException("The specified macro is empty and cannot be used."); + throw new IllegalGameStateException(UIStrings.MacroStrategy_MacroEmptyError); } PluginLog.Debug($"Would execute macro number {macroNumber}"); diff --git a/FFXIVPlugin/ActionExecutor/Strategies/MainCommandStrategy.cs b/FFXIVPlugin/ActionExecutor/Strategies/MainCommandStrategy.cs index 7297dd7..71c9fc6 100644 --- a/FFXIVPlugin/ActionExecutor/Strategies/MainCommandStrategy.cs +++ b/FFXIVPlugin/ActionExecutor/Strategies/MainCommandStrategy.cs @@ -6,6 +6,7 @@ using Lumina.Excel.GeneratedSheets; using XIVDeck.FFXIVPlugin.Base; using XIVDeck.FFXIVPlugin.Game; +using XIVDeck.FFXIVPlugin.Resources.Localization; namespace XIVDeck.FFXIVPlugin.ActionExecutor.Strategies; @@ -39,7 +40,7 @@ public MainCommandStrategy() { public unsafe void Execute(uint actionId, dynamic? _) { if (this._mainCommandCache.All(command => actionId != command.RowId)) - throw new InvalidOperationException($"Main command action ID {actionId} is not valid."); + throw new InvalidOperationException(string.Format(UIStrings.MainCommandStrategy_ActionInvalidError, actionId)); TickScheduler.Schedule(delegate { Framework.Instance()->GetUiModule()->ExecuteMainCommand(actionId); diff --git a/FFXIVPlugin/ActionExecutor/Strategies/MinionStrategy.cs b/FFXIVPlugin/ActionExecutor/Strategies/MinionStrategy.cs index 50884fe..a0f411d 100644 --- a/FFXIVPlugin/ActionExecutor/Strategies/MinionStrategy.cs +++ b/FFXIVPlugin/ActionExecutor/Strategies/MinionStrategy.cs @@ -7,6 +7,7 @@ using XIVDeck.FFXIVPlugin.Base; using XIVDeck.FFXIVPlugin.Exceptions; using XIVDeck.FFXIVPlugin.Game; +using XIVDeck.FFXIVPlugin.Resources.Localization; using XIVDeck.FFXIVPlugin.Utils; namespace XIVDeck.FFXIVPlugin.ActionExecutor.Strategies; @@ -42,11 +43,11 @@ public void Execute(uint actionId, dynamic? _) { var minion = GetMinionById(actionId); if (minion == null) { - throw new ArgumentNullException(nameof(actionId), $"No minion with ID {actionId} exists."); + throw new ArgumentNullException(nameof(actionId), string.Format(UIStrings.MinionStrategy_MinionNotFoundError, actionId)); } if (!GameStateCache.IsMinionUnlocked(actionId)) { - throw new ActionLockedException($"The minion \"{minion.Singular}\" isn't unlocked and therefore can't be used."); + throw new ActionLockedException(string.Format(UIStrings.MinionStrategy_MinionLockedError, minion.Singular)); } var command = $"/minion \"{minion.Singular}\""; diff --git a/FFXIVPlugin/ActionExecutor/Strategies/MountStrategy.cs b/FFXIVPlugin/ActionExecutor/Strategies/MountStrategy.cs index 5a05b50..2e61193 100644 --- a/FFXIVPlugin/ActionExecutor/Strategies/MountStrategy.cs +++ b/FFXIVPlugin/ActionExecutor/Strategies/MountStrategy.cs @@ -7,6 +7,7 @@ using XIVDeck.FFXIVPlugin.Base; using XIVDeck.FFXIVPlugin.Exceptions; using XIVDeck.FFXIVPlugin.Game; +using XIVDeck.FFXIVPlugin.Resources.Localization; using XIVDeck.FFXIVPlugin.Utils; namespace XIVDeck.FFXIVPlugin.ActionExecutor.Strategies; @@ -38,11 +39,11 @@ public void Execute(uint actionId, dynamic? _) { Mount? mount = GetMountById(actionId); if (mount == null) { - throw new ArgumentNullException(nameof(actionId), $"No mount with ID {actionId} exists."); + throw new ArgumentNullException(nameof(actionId), string.Format(UIStrings.MountStrategy_MountNotFoundError, actionId)); } if (!_gameStateCache.IsMountUnlocked(actionId)) { - throw new ActionLockedException($"The mount \"{mount.Singular}\" isn't unlocked and therefore can't be used."); + throw new ActionLockedException(string.Format(UIStrings.MountStrategy_MountLockedError, mount.Singular)); } String command = $"/mount \"{mount.Singular}\""; diff --git a/FFXIVPlugin/ActionExecutor/Strategies/OrnamentStrategy.cs b/FFXIVPlugin/ActionExecutor/Strategies/OrnamentStrategy.cs index 5c4f62b..e6c7c83 100644 --- a/FFXIVPlugin/ActionExecutor/Strategies/OrnamentStrategy.cs +++ b/FFXIVPlugin/ActionExecutor/Strategies/OrnamentStrategy.cs @@ -6,6 +6,7 @@ using XIVDeck.FFXIVPlugin.Base; using XIVDeck.FFXIVPlugin.Exceptions; using XIVDeck.FFXIVPlugin.Game; +using XIVDeck.FFXIVPlugin.Resources.Localization; using XIVDeck.FFXIVPlugin.Utils; namespace XIVDeck.FFXIVPlugin.ActionExecutor.Strategies; @@ -46,7 +47,7 @@ public void Execute(uint actionId, dynamic? _) { } if (!GameStateCache.IsOrnamentUnlocked(actionId)) { - throw new ActionLockedException($"The fashion accessory \"{ornament.Singular}\" isn't unlocked and therefore can't be used."); + throw new ActionLockedException(string.Format(UIStrings.OrnamentStrategy_OrnamentLockedError, ornament.Singular)); } var command = $"/fashion \"{ornament.Singular}\""; diff --git a/FFXIVPlugin/Constants.cs b/FFXIVPlugin/Constants.cs index 5ba4796..008609b 100644 --- a/FFXIVPlugin/Constants.cs +++ b/FFXIVPlugin/Constants.cs @@ -1,7 +1,6 @@ -namespace XIVDeck.FFXIVPlugin { - public static class Constants { - public const string PluginName = "XIVDeck Game Plugin"; - public const string MinimumSDPluginVersion = "0.1.0"; - public const string GithubUrl = "https://github.com/KazWolfe/XIVDeck"; - } -} \ No newline at end of file +namespace XIVDeck.FFXIVPlugin; + +public static class Constants { + public const string MinimumSDPluginVersion = "0.1.0"; + public const string GithubUrl = "https://github.com/KazWolfe/XIVDeck"; +} \ No newline at end of file diff --git a/FFXIVPlugin/Exceptions/GameExceptions.cs b/FFXIVPlugin/Exceptions/GameExceptions.cs index 6522d30..fbc25d4 100644 --- a/FFXIVPlugin/Exceptions/GameExceptions.cs +++ b/FFXIVPlugin/Exceptions/GameExceptions.cs @@ -1,5 +1,6 @@ using System; using FFXIVClientStructs.FFXIV.Client.UI.Misc; +using XIVDeck.FFXIVPlugin.Resources.Localization; namespace XIVDeck.FFXIVPlugin.Exceptions; @@ -11,11 +12,11 @@ public IllegalGameStateException(string message) : public class PlayerNotLoggedInException : IllegalGameStateException { public PlayerNotLoggedInException() : - base("A player is not logged in to the game.") { } + base(UIStrings.Exceptions_PlayerNotLoggedIn) { } } public class ActionLockedException : IllegalGameStateException { public ActionLockedException(HotbarSlotType type, uint actionId) : - base($"The {type} ID {actionId} is has not been unlocked and cannot be used.") { } + base(string.Format(UIStrings.Exceptions_ActionLocked, type, actionId)) { } public ActionLockedException(string message) : base(message) { } @@ -23,5 +24,5 @@ public ActionLockedException(string message) : public class ActionNotFoundException : ArgumentException { public ActionNotFoundException(HotbarSlotType actionType, uint actionId) : - base($"No {actionType} with ID {actionId} was found") { } + base(string.Format(UIStrings.Exceptions_ActionNotFound, actionType, actionId)) { } } \ No newline at end of file diff --git a/FFXIVPlugin/Game/HotbarWatcher.cs b/FFXIVPlugin/Game/HotbarWatcher.cs index 6d2d330..d12e9f9 100644 --- a/FFXIVPlugin/Game/HotbarWatcher.cs +++ b/FFXIVPlugin/Game/HotbarWatcher.cs @@ -21,7 +21,7 @@ private unsafe void OnGameUpdate(Framework framework) { GetRaptureHotbarModule(); var hotbarUpdated = false; - + for (var hotbarId = 0; hotbarId < 17; hotbarId++) { var hotbar = hotbarModule->HotBar[hotbarId]; diff --git a/FFXIVPlugin/Game/IconManager.cs b/FFXIVPlugin/Game/IconManager.cs index 78df125..bf74d4e 100644 --- a/FFXIVPlugin/Game/IconManager.cs +++ b/FFXIVPlugin/Game/IconManager.cs @@ -73,7 +73,7 @@ private void LoadIconTexture(int iconId, bool hq = false) { ClientLanguage.German => "de/", ClientLanguage.French => "fr/", _ => throw new ArgumentOutOfRangeException(nameof(iconLanguage), - "Unknown Language: " + Injections.DataManager.Language) + @"Unknown Language: " + Injections.DataManager.Language) }; return this.GetIcon(language, iconId, hq, highres); diff --git a/FFXIVPlugin/Game/SigHelper.cs b/FFXIVPlugin/Game/SigHelper.cs index 932cca7..729b048 100644 --- a/FFXIVPlugin/Game/SigHelper.cs +++ b/FFXIVPlugin/Game/SigHelper.cs @@ -7,10 +7,13 @@ using FFXIVClientStructs.FFXIV.Client.System.Framework; using FFXIVClientStructs.FFXIV.Client.System.Memory; using FFXIVClientStructs.FFXIV.Client.System.String; +using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI.Misc; +using XIVDeck.FFXIVPlugin.Base; using XIVDeck.FFXIVPlugin.Game.Structs; using XIVDeck.FFXIVPlugin.Server; using XIVDeck.FFXIVPlugin.Server.Messages.Outbound; +using XIVDeck.FFXIVPlugin.Utils; // ReSharper disable InconsistentNaming - matching expected documentation things // ReSharper disable UnusedAutoPropertyAccessor.Local - handled by siggingway and reflection @@ -88,6 +91,36 @@ public void ExecuteHotbarAction(HotbarSlotType commandType, uint commandId, bool Marshal.FreeHGlobal(ptr); } + public void PulseHotbarSlot(int hotbarId, int slotId) { + var isCrossHotbar = GameUtils.IsCrossHotbar(hotbarId); + + // Handle the main hotbar, which is a bit interesting as it can behave oddly at times. + var mainBarName = isCrossHotbar ? "_ActionCross" : "_ActionBar"; + var mainBarPtr = Injections.GameGui.GetAddonByName(mainBarName, 1); + + if (mainBarPtr != IntPtr.Zero) { + var activeHotbarId = *(byte*) (mainBarPtr + 0x23C); // offset to RaptureHotbarId + + if (activeHotbarId == hotbarId) { + this.SafePulseBar((AddonActionBarBase*) mainBarPtr, slotId); + } + } else { + PluginLog.Debug($"Couldn't find main hotbar addon {mainBarName}!"); + } + + // Aaand handle normal hotbars, if targeted. + if (!isCrossHotbar) { + var actionBarName = $"_ActionBar{hotbarId:00}"; + var actionBarPtr = Injections.GameGui.GetAddonByName(actionBarName, 1); + + if (actionBarPtr != IntPtr.Zero) { + this.SafePulseBar((AddonActionBarBase*) actionBarPtr, slotId); + } else { + PluginLog.Debug($"Couldn't find hotbar addon {actionBarName}"); + } + } + } + public string GetSanitizedString(string input) { var uString = Utf8String.FromString(input); @@ -109,9 +142,9 @@ public void SendChatMessage(string message) { switch (messageBytes.Length) { case 0: - throw new ArgumentException("Message cannot be empty", nameof(message)); + throw new ArgumentException(@"Message cannot be empty", nameof(message)); case > 500: - throw new ArgumentException("Message exceeds 500char limit", nameof(message)); + throw new ArgumentException(@"Message exceeds 500char limit", nameof(message)); } var payloadMem = Marshal.AllocHGlobal(400); @@ -147,4 +180,16 @@ private IntPtr DetourMacroUpdate(IntPtr a1, IntPtr macroPage, IntPtr macroSlot) return tmp; } + + private void SafePulseBar(AddonActionBarBase* actionBar, int slotId) { + if (slotId is < 0 or > 15) { + return; + } + + if (!actionBar->AtkUnitBase.IsVisible) { + return; + } + + actionBar->PulseActionBarSlot(slotId); + } } \ No newline at end of file diff --git a/FFXIVPlugin/Game/TickScheduler.cs b/FFXIVPlugin/Game/TickScheduler.cs index 5311bbb..5ac3d7c 100644 --- a/FFXIVPlugin/Game/TickScheduler.cs +++ b/FFXIVPlugin/Game/TickScheduler.cs @@ -3,8 +3,9 @@ using Dalamud.Game; using Dalamud.Logging; using XIVDeck.FFXIVPlugin.Base; +using XIVDeck.FFXIVPlugin.Resources.Localization; -namespace XIVDeck.FFXIVPlugin.Game; +namespace XIVDeck.FFXIVPlugin.Game; // borrowed from https://github.com/Eternita-S/NotificationMaster/blob/master/NotificationMaster/TickScheduler.cs // ToDo: Deprecate when https://github.com/goatcorp/Dalamud/pull/832 is merged @@ -14,10 +15,10 @@ internal static TickScheduler Schedule(Action function, Framework? framework = n return new TickScheduler(function, framework, delay); } - + internal static Task RunOnNextFrame(Func function, Framework? framework = null, long delay = 0) { framework ??= Injections.Framework; - + var tcs = new TaskCompletionSource(); var _ = new TickScheduler(() => { @@ -53,13 +54,15 @@ public void Dispose() { private void Execute(object _) { if (Environment.TickCount64 < this._executeAt) return; - + try { this._function(); } catch (Exception e) { PluginLog.Error(e, "Exception running a Framework tick event"); - Injections.Chat.PrintError($"[XIVDeck] There was an issue running a task: {e.GetType()}: {e.Message}"); + ErrorNotifier.ShowError($"[{string.Format(UIStrings.ErrorHandler_ErrorPrefix, UIStrings.XIVDeck)}] " + + $"{string.Format(UIStrings.TickScheduler_ExceptionHandler, e.GetType(), e.Message)}"); } + this.Dispose(); } } \ No newline at end of file diff --git a/FFXIVPlugin/Resources/Localization/UIStrings.Designer.cs b/FFXIVPlugin/Resources/Localization/UIStrings.Designer.cs new file mode 100644 index 0000000..360db9c --- /dev/null +++ b/FFXIVPlugin/Resources/Localization/UIStrings.Designer.cs @@ -0,0 +1,654 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace XIVDeck.FFXIVPlugin.Resources.Localization { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class UIStrings { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal UIStrings() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("XIVDeck.FFXIVPlugin.Resources.Localization.UIStrings", typeof(UIStrings).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to No registered action of type {0} was found.. + /// + internal static string ActionController_UnknownActionTypeError { + get { + return ResourceManager.GetString("ActionController_UnknownActionTypeError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to API key missing or invalid.. + /// + internal static string AuthModule_BadAPIKeyError { + get { + return ResourceManager.GetString("AuthModule_BadAPIKeyError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cannot switch to a class with ID less than 1.. + /// + internal static string ClassController_ClassLessThan1Error { + get { + return ResourceManager.GetString("ClassController_ClassLessThan1Error", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No gearset was found for {0}, so your gearset for {1} was used instead.. + /// + internal static string ClassController_FallbackClassUsed { + get { + return ResourceManager.GetString("ClassController_FallbackClassUsed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A class with ID {0} does not exist!. + /// + internal static string ClassController_InvalidClassIdError { + get { + return ResourceManager.GetString("ClassController_InvalidClassIdError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Couldn't switch to {0} because you don't have a gearset for this class. Create one and try again.. + /// + internal static string ClassController_NoGearsetForClassError { + get { + return ResourceManager.GetString("ClassController_NoGearsetForClassError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No Collection with ID {0} exists.. + /// + internal static string CollectionStrategy_CollectionNotFoundError { + get { + return ResourceManager.GetString("CollectionStrategy_CollectionNotFoundError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A command to run must be specified.. + /// + internal static string CommandController_MissingCommandError { + get { + return ResourceManager.GetString("CommandController_MissingCommandError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Commands must start with a slash.. + /// + internal static string CommandController_NotCommandError { + get { + return ResourceManager.GetString("CommandController_NotCommandError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The emote "{0}" does not have an associated text command.. + /// + internal static string EmoteStrategy_EmoteDoesntHaveCommandError { + get { + return ResourceManager.GetString("EmoteStrategy_EmoteDoesntHaveCommandError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The emote "{0}" isn't unlocked and therefore can't be used.. + /// + internal static string EmoteStrategy_EmoteLockedError { + get { + return ResourceManager.GetString("EmoteStrategy_EmoteLockedError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} - ERROR. + /// + internal static string ErrorHandler_ErrorPrefix { + get { + return ResourceManager.GetString("ErrorHandler_ErrorPrefix", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The {0} ID {1} has not been unlocked and cannot be used.. + /// + internal static string Exceptions_ActionLocked { + get { + return ResourceManager.GetString("Exceptions_ActionLocked", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No {0} with ID {1} was found.. + /// + internal static string Exceptions_ActionNotFound { + get { + return ResourceManager.GetString("Exceptions_ActionNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A player is not logged in to the game.. + /// + internal static string Exceptions_PlayerNotLoggedIn { + get { + return ResourceManager.GetString("Exceptions_PlayerNotLoggedIn", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No command exists for Extra Command {0}. REPORT THIS BUG!. + /// + internal static string ExtraCommandStrategy_NoCommandError { + get { + return ResourceManager.GetString("ExtraCommandStrategy_NoCommandError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An action of type {0} with ID {1} does not exist.. + /// + internal static string FixedCommandStrategy_ActionNotFoundError { + get { + return ResourceManager.GetString("FixedCommandStrategy_ActionNotFoundError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The action with ID {0} is marked as illegal and cannot be used.. + /// + internal static string FixedCommandStrategy_IllegalActionError { + get { + return ResourceManager.GetString("FixedCommandStrategy_IllegalActionError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A sheet of type {0} does not exist.. + /// + internal static string FixedCommandStrategy_SheetNotFoundError { + get { + return ResourceManager.GetString("FixedCommandStrategy_SheetNotFoundError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The XIVDeck Stream Deck Plugin is critically out of date and has been disabled.. + /// + internal static string ForcedUpdateNag_Headline { + get { + return ResourceManager.GetString("ForcedUpdateNag_Headline", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Please download and install the latest version of the Stream Deck Plugin from the XIVDeck GitHub to continue using XIVDeck.. + /// + internal static string ForcedUpdateNag_ResolutionHelp { + get { + return ResourceManager.GetString("ForcedUpdateNag_ResolutionHelp", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A class with ID {0} does not exist.. + /// + internal static string GameClass_NotFoundError { + get { + return ResourceManager.GetString("GameClass_NotFoundError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unrecognized job category for class ID {0}. REPORT THIS BUG!. + /// + internal static string GameClass_UncategorizedError { + get { + return ResourceManager.GetString("GameClass_UncategorizedError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No gearset exists in slot number {0}.. + /// + internal static string GearsetStrategy_GearsetNotFoundError { + get { + return ResourceManager.GetString("GearsetStrategy_GearsetNotFoundError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The action "{0}" (ID {1}) is marked as illegal and cannot be used.. + /// + internal static string GeneralActionStrategy_ActionIllegalError { + get { + return ResourceManager.GetString("GeneralActionStrategy_ActionIllegalError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The action "{0}" is not yet unlocked.. + /// + internal static string GeneralActionStrategy_ActionLockedError { + get { + return ResourceManager.GetString("GeneralActionStrategy_ActionLockedError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to When Hotbar ID >= 10, Slot ID must be between 0 and 15. + /// + internal static string HotbarController_CrossHotbarInvalidSlotError { + get { + return ResourceManager.GetString("HotbarController_CrossHotbarInvalidSlotError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Hotbar ID must be between 0 and 17. + /// + internal static string HotbarController_InvalidHotbarIdError { + get { + return ResourceManager.GetString("HotbarController_InvalidHotbarIdError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An invalid hotbar or slot was triggered.. + /// + internal static string HotbarController_InvalidHotbarOrSlotError { + get { + return ResourceManager.GetString("HotbarController_InvalidHotbarOrSlotError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to When Hotbar ID < 10, Slot ID must be between 0 and 11. + /// + internal static string HotbarController_NormalHotbarInvalidSlotError { + get { + return ResourceManager.GetString("HotbarController_NormalHotbarInvalidSlotError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cannot switch instruments while in Performance mode.. + /// + internal static string InstrumentStrategy_CurrentlyPerformingError { + get { + return ResourceManager.GetString("InstrumentStrategy_CurrentlyPerformingError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No instrument with ID {0} exists.. + /// + internal static string InstrumentStrategy_InstrumentNotFoundError { + get { + return ResourceManager.GetString("InstrumentStrategy_InstrumentNotFoundError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Performance mode hasn't been unlocked yet.. + /// + internal static string InstrumentStrategy_PerformanceLockedError { + get { + return ResourceManager.GetString("InstrumentStrategy_PerformanceLockedError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The specified macro is empty and cannot be used.. + /// + internal static string MacroStrategy_MacroEmptyError { + get { + return ResourceManager.GetString("MacroStrategy_MacroEmptyError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Main command action ID {0} is not valid.. + /// + internal static string MainCommandStrategy_ActionInvalidError { + get { + return ResourceManager.GetString("MainCommandStrategy_ActionInvalidError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The minion "{0}" isn't unlocked and therefore can't be used.. + /// + internal static string MinionStrategy_MinionLockedError { + get { + return ResourceManager.GetString("MinionStrategy_MinionLockedError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No minion with ID {0} exists.. + /// + internal static string MinionStrategy_MinionNotFoundError { + get { + return ResourceManager.GetString("MinionStrategy_MinionNotFoundError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The mount "{0}" isn't unlocked and therefore can't be used.. + /// + internal static string MountStrategy_MountLockedError { + get { + return ResourceManager.GetString("MountStrategy_MountLockedError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No mount with ID {0} exists.. + /// + internal static string MountStrategy_MountNotFoundError { + get { + return ResourceManager.GetString("MountStrategy_MountNotFoundError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open XIVDeck Download Page. + /// + internal static string Nag_OpenGithubDownloadButton { + get { + return ResourceManager.GetString("Nag_OpenGithubDownloadButton", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open XIVDeck Settings. + /// + internal static string Nag_OpenSettingsButton { + get { + return ResourceManager.GetString("Nag_OpenSettingsButton", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The fashion accessory "{0}" isn't unlocked and therefore can't be used.. + /// + internal static string OrnamentStrategy_OrnamentLockedError { + get { + return ResourceManager.GetString("OrnamentStrategy_OrnamentLockedError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to API Port. + /// + internal static string SettingsWindow_APIPort { + get { + return ResourceManager.GetString("SettingsWindow_APIPort", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Default port: {0} + /// + ///Range: {1}-{2}. + /// + internal static string SettingsWindow_APIPort_Help { + get { + return ResourceManager.GetString("SettingsWindow_APIPort_Help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Apply Settings. + /// + internal static string SettingsWindow_ApplyButton { + get { + return ResourceManager.GetString("SettingsWindow_ApplyButton", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use Penumbra Icons. + /// + internal static string SettingsWindow_EnablePenumbraIPC { + get { + return ResourceManager.GetString("SettingsWindow_EnablePenumbraIPC", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to When enabled, this feature will attempt to display icons from Penumbra on the Stream Deck. Note that Penumbra must be installed for this setting to have any effect. + /// + ///If disabled, original game icons will be used instead. + /// + ///Default: Off. + /// + internal static string SettingsWindow_EnablePenumbraIPC_Help { + get { + return ResourceManager.GetString("SettingsWindow_EnablePenumbraIPC_Help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use /micon Icons. + /// + internal static string SettingsWindow_Experiment_MIcon { + get { + return ResourceManager.GetString("SettingsWindow_Experiment_MIcon", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Experimental Settings. + /// + internal static string SettingsWindow_ExperimentalSettings { + get { + return ResourceManager.GetString("SettingsWindow_ExperimentalSettings", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to XIVDeck GitHub. + /// + internal static string SettingsWindow_GitHubLink { + get { + return ResourceManager.GetString("SettingsWindow_GitHubLink", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Listen IP: {0}. + /// + internal static string SettingsWindow_ListenIP { + get { + return ResourceManager.GetString("SettingsWindow_ListenIP", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to DANGER: SAFE MODE DISABLED! You may be able to send illegal commands from your Stream Deck to the game.. + /// + internal static string SettingsWindow_SafeModeDisabledWarning { + get { + return ResourceManager.GetString("SettingsWindow_SafeModeDisabledWarning", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to XIVDeck Game Plugin Settings. + /// + internal static string SettingsWindow_Title { + get { + return ResourceManager.GetString("SettingsWindow_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to If the XIVDeck Stream Deck Plugin is already installed, please make sure the port is set correctly in the configuration and that you've created at least one button.. + /// + internal static string SetupNag_AlreadyInstalledHelp { + get { + return ResourceManager.GetString("SetupNag_AlreadyInstalledHelp", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Current port: {0}. + /// + internal static string SetupNag_CurrentPort { + get { + return ResourceManager.GetString("SetupNag_CurrentPort", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to To dismiss this message, resolve the above problem or uninstall the XIVDeck plugin.. + /// + internal static string SetupNag_DismissHelp { + get { + return ResourceManager.GetString("SetupNag_DismissHelp", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A Stream Deck has never connected to the game.. + /// + internal static string SetupNag_Headline { + get { + return ResourceManager.GetString("SetupNag_Headline", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to If you need to change the port the server is hosted on, you may do so from XIVDeck's settings.. + /// + internal static string SetupNag_PortChangeHelp { + get { + return ResourceManager.GetString("SetupNag_PortChangeHelp", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to If you haven't done so already, please make sure you've downloaded and installed the companion XIVDeck Stream Deck Plugin from GitHub.. + /// + internal static string SetupNag_ResolutionHelp { + get { + return ResourceManager.GetString("SetupNag_ResolutionHelp", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Stream Deck. + /// + internal static string StreamDeck { + get { + return ResourceManager.GetString("StreamDeck", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to There was an issue running a task: {0}: {1}. + /// + internal static string TickScheduler_ExceptionHandler { + get { + return ResourceManager.GetString("TickScheduler_ExceptionHandler", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Thank you for installing the Stream Deck plugin. XIVDeck is now ready to go!. + /// + internal static string WSInitOpcode_ThanksForInstall { + get { + return ResourceManager.GetString("WSInitOpcode_ThanksForInstall", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No message exists with opcode {0}. + /// + internal static string WSOpcodeWiring_UnknownOpcodeError { + get { + return ResourceManager.GetString("WSOpcodeWiring_UnknownOpcodeError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to XIVDeck. + /// + internal static string XIVDeck { + get { + return ResourceManager.GetString("XIVDeck", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to XIVDeck Game Plugin. + /// + internal static string XIVDeck_Title { + get { + return ResourceManager.GetString("XIVDeck_Title", resourceCulture); + } + } + } +} diff --git a/FFXIVPlugin/Resources/Localization/UIStrings.de.resx b/FFXIVPlugin/Resources/Localization/UIStrings.de.resx new file mode 100644 index 0000000..a62e417 --- /dev/null +++ b/FFXIVPlugin/Resources/Localization/UIStrings.de.resx @@ -0,0 +1,238 @@ + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + XIVDeck + Product name. Do not change. + + + Stream Deck + Product name. Do not change. + + + XIVDeck Spiele-Plugin + + + Ein Spieler ist nicht im Spiel eingeloggt. + + + {0} ID {1} ist nicht freigeschaltet und kann nicht verwendet werden. + Exceptions_ActionLocked +Semi-internal. Generally can be translated as `An action of type {0} with ID {1} has not been unlocked and cannot be used.` This string, in English, would be `An Emote with ID 123 has not been...` + + + Kein {0} mit der ID {1} gefunden. + Exceptions_ActionNotFound +Semi-internal. Generally can be translated as `An action of type {0} with ID {1} cannot be found.` This string, in English, would be `An Emote with ID 123 cannot be found.` + + + Ein auszuführender Befehl muss angegeben werden. + + + Befehle müssen mit einem Schrägstrich anfangen. + + + Es wurde keine registrierte Aktion vom Typ {0} gefunden. + + + Kann nicht zu einer Klasse mit ID kleiner 1 wechseln. + + + Eine Klasse mit ID {0} existiert nicht! + + + Konnte nicht zu {0} wechseln, da kein Ausrüstungsset für diese Klasse existiert. Erstelle eins und versuche es erneut. + + + Hotbar-ID muss zwischen 0 und 17 liegen + + + Wenn Hotbar-ID < 10, muss Slot-ID zwischen 0 und 11 liegen + + + Wenn Hotbar-ID >= 10, muss Slot-ID zwischen 0 und 15 liegen + + + Eine ungültige Hotbar oder Slot wurde ausgelöst. + + + API-Schlüssel nicht vorhanden oder ungültig. + + + Es existiert keine Nachricht mit opcode {0} + + + Das Emote "{0}" hat keinen zugehörigen Text-Befehl. + + + Das Emote "{0}" ist nicht freigeschaltet und kann daher nicht verwendet werden. + + + Kein Befehl für Extra Befehl {0} vorhanden. BERICHTE DIESEN FEHLER! + This error message is shown when the command to use an "Extra Command" is not found. Because management of extra commands is done manually, additional instructions have been added for the user to report a bug to the developer. + + + + Eine Aktion des Typs {0} mit der ID {1} existiert nicht. + + + Die Aktion mit der ID {0} ist als ungültig markiert und kann nicht benutzt werden. + Shown when the plugin blocks a user from calling an action marked as illegal (unsafe). + + + Eine Tabelle vom Typ {0} existiert nicht. + + + XIVDeck Spiel-Plugin Einstellungen + + + WARNUNG: SICHERER MODUS DEAKTIVIERT! Du kannst möglicherweise verbotene oder ungültige Befehle von deinem Stream Deck an das Spiel senden. + "Illegal commands" in this case mean commands that normally cannot be sent to the game and may cause damage or harm if proper care is not taken. + + + Penumbra Icons verwenden + Setting to enable integration with another app named Penumbra. + + + Wenn aktiviert, wird versucht, Icons von Penumbra auf dem Stream Deck anzuzeigen. Beachte, dass Penumbra installiert sein muss, damit diese Einstellung eine Wirkung hat. + +Wenn deaktiviert, werden stattdessen originale Spielsymbole verwendet. + +Standard: Aus + + + Experimentelle Einstellungen + Header for configuration window section. + + + /micon Icons verwenden + /micon is a game-provided command. It can be left as-is. + + + XIVDeck GitHub + + + Übernehmen + Used as a "save and apply" button for a settings dialog box. Should be a short string (under 20 characters) + + + API-Port + + + Listen IP: {0} + + + Standardport: {0} + +Bereich: {1}-{2} + + + Das XIVDeck Stream Deck Plugin ist veraltet und wurde deaktiviert. + + + XIVDeck Downloadseite öffnen + + + XIVDeck Einstellungen öffnen + + + Bitte lade die neueste Version des Stream Deck Plugins vom XIVDeck GitHub herunter und installiere diese, um XIVDeck weiterzunutzen. + + + Ein Stream Deck hat sich nie mit dem Spiel verbunden. + SetupNag_Headline +Displayed when the user has never had a successful connection to any Stream Deck. Shows upon first install, or immediately after changing the server port without updating the Stream Deck Plugin side. Essentially, "there has never been a successful connection to the server's currently-defined port." + + + Falls dies noch nicht getan wurde, stelle bitte sicher, dass das XIVDeck Stream Deck Plugin von GitHub heruntergeladen und installiert ist. + + + Falls das XIVDeck Stream Deck Plugin bereits installiert ist, stelle bitte sicher, dass der Port in der Konfiguration korrekt eingestellt ist und dass mindestens eine Schaltfläche angelegt ist. + + + Aktueller Port: {0} + + + Falls der Port, auf dem der Server gehostet wird, geändert werden muss, kann dies in den XIVDeck Einstellungen getan werden. + + + Um diese Nachricht zu verwerfen, löse das obige Problem oder deinstalliere das XIVDeck Plugin. + + + Es gibt keine Kuriosität mit der ID {0}. + + + Kein Ausrüstungsset in Slot Nummer {0} vorhanden. + + + Die Aktion "{0}" ist noch nicht freigeschaltet. + + + Die Aktion "{0}" (ID {1}) ist als ungültig markiert und kann nicht verwendet werden. + Shown when the plugin blocks a user from calling an action marked as illegal (unsafe). + + + Bardenkompositionenmodus wurde noch nicht freigeschaltet. + Shown when a user attempts to use a musical instrument without having Performance actions unlocked. + + + Instrumente können nicht im Bardenkompositionenmodus geändert werden. + + Shown when a user attempts to switch musical instruments while actively in a Performance. + + + Es gibt kein Instrument mit der ID {0}. + Shown when a user attempts to use an invalid or unknown musical instrument. + + + Das angegebene Makro ist leer und kann nicht benutzt werden. + + + Haupt-Befehlsaktion ID {0} ist ungültig. + + + Es gibt keinen Begleiter mit der ID {0}. + + + Der Begleiter "{0}" ist nicht freigeschaltet und kann daher nicht verwendet werden. + + + Es gibt kein Reittier mit der ID {0}. + + + Das Reittier "{0}" ist nicht freigeschaltet und kann daher nicht verwendet werden. + + + Das Mode Zubehör "{0}" ist nicht freigeschaltet und kann daher nicht verwendet werden. + + + {0} - FEHLER + + + Es gab ein Problem bei der Ausführung: {0}: {1} + "Task" may be substituted for "job" in this case. This string will display when a background job fails to execute. + + + Eine Klasse mit ID {0} existiert nicht. + + + Nicht erkannte Job-Kategorie für Klassen-ID {0}. BERICHTE DIESEN FEHLER! + Shown when plugin data is invalid and requires an update from the developer. + + \ No newline at end of file diff --git a/FFXIVPlugin/Resources/Localization/UIStrings.fr.resx b/FFXIVPlugin/Resources/Localization/UIStrings.fr.resx new file mode 100644 index 0000000..1635f8c --- /dev/null +++ b/FFXIVPlugin/Resources/Localization/UIStrings.fr.resx @@ -0,0 +1,237 @@ + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + XIVDeck + Product name. Do not change. + + + Stream Deck + Product name. Do not change. + + + Plugin de jeu XIVDeck + + + Aucun joueur n'est connecté au jeu. + + + L'action de type {0} avec l'ID {1} n'a pas été débloquée et ne peut pas être utilisée. + Exceptions_ActionLocked +Semi-internal. Generally can be translated as `An action of type {0} with ID {1} has not been unlocked and cannot be used.` This string, in English, would be `An Emote with ID 123 has not been...` + + + L'action de type {0} avec l'ID {1} n'existe pas. + Exceptions_ActionNotFound +Semi-internal. Generally can be translated as `An action of type {0} with ID {1} cannot be found.` This string, in English, would be `An Emote with ID 123 cannot be found.` + + + La commande à exécuter doit être spécifiée. + + + Les commandes doivent commencer par un slash. + + + Aucune action enregistrée de type {0} n’a été trouvée. + + + Impossible de passer à une classe dont l'ID est inférieur à 1. + + + Aucune classe avec l'ID {0} n'existe ! + + + Impossible de passer {0} parce que vous n'avez pas de tenue assignée pour cette classe. Créez-en une et réessayez. + + + L'ID de la barre de raccourcis doit être compris entre 0 et 17 + + + Lorsque l'ID de la barre de raccourcis est < 10, l'ID de l'emplacement doit être compris entre 0 et 11 + + + Lorsque l'ID de la barre de raccourcis est >= 10, l'ID de l'emplacement doit être compris entre 0 et 15 + + + Une barre de raccourcis ou un emplacement invalide a été activé. + + + Clé d'API manquante ou invalide. + + + Aucun message avec l'opcode {0} n'existe + + + L'émote «{0}» n'a pas de commande texte associée. + + + L'émote «{0}» n'est pas débloquée et ne peut donc pas être utilisée. + + + Aucune commande {0} n'existe parmi les commandes Autres. VEUILLEZ SIGNALER CE BUG ! + This error message is shown when the command to use an "Extra Command" is not found. Because management of extra commands is done manually, additional instructions have been added for the user to report a bug to the developer. + + + + Aucune action de type {0} avec l'ID {1} n'existe. + + + L'action avec l'ID {0} est marquée comme illégale et ne peut être utilisée. + Shown when the plugin blocks a user from calling an action marked as illegal (unsafe). + + + Aucune page de type {0} n'existe. + + + Paramètres du Plugin en Jeu pour XIVDeck + + + DANGER : MODE SÉCURISÉ DÉSACTIVÉ ! Vous risquez d'envoyer des commandes illégales depuis votre Stream Deck vers le jeu. + "Illegal commands" in this case mean commands that normally cannot be sent to the game and may cause damage or harm if proper care is not taken. + + + Utiliser les icônes de Penumbra + Setting to enable integration with another app named Penumbra. + + + Lorsqu'elle est activée, cette fonctionnalité tentera d'afficher les icônes provenant de Penumbra sur le Stream Deck. Notez que Penumbra doit être installé pour que cela fonctionne. + +Si désactivé, les icônes par défaut du jeu seront utilisées à la place. + +Par défaut : Désactivé + + + Fonctionnalités expérimentales + Header for configuration window section. + + + Utiliser les icônes de la commande /micon + /micon is a game-provided command. It can be left as-is. + + + GitHub de XIVDeck + + + Appliquer + Used as a "save and apply" button for a settings dialog box. Should be a short string (under 20 characters) + + + Port de l’API + + + En écoute sur l'IP : {0} + + + Port par défaut : {0} + +Plage : {1}-{2} + + + Cette version du Plugin XIVDeck pour Stream Deck est très dépassée et a été désactivée. + + + Ouvrir la Page de Téléchargement de XIVDeck + + + Ouvrir les Paramètres de XIVDeck + + + Veuillez télécharger et installer la dernière version du plugin Stream Deck à partir de la page GitHub de XIVDeck pour continuer à utiliser XIVDeck. + + + Un Stream Deck n'a encore jamais été connecté au jeu jusqu'à maintenant. + SetupNag_Headline +Displayed when the user has never had a successful connection to any Stream Deck. Shows upon first install, or immediately after changing the server port without updating the Stream Deck Plugin side. Essentially, "there has never been a successful connection to the server's currently-defined port." + + + Si vous ne l'avez pas déjà fait, assurez-vous d'avoir téléchargé et installé le plugin XIVDeck Stream Deck depuis GitHub. + + + Si le plugin XIVDeck Stream Deck est déjà installé, assurez-vous que le port est correctement défini dans la configuration et que vous avez créé au moins un bouton. + + + Port actuel : {0} + + + Si vous avez besoin de modifier le port sur lequel le serveur écoute, vous pouvez le faire à partir des paramètres de XIVDeck. + + + Pour fermer cette fenêtre, résolvez le problème ci-dessus ou désinstallez le plugin XIVDeck. + + + Aucun Objet Précieux avec l'ID {0} n'existe. + + + Aucune tenue n'existe dans l'emplacement numéro {0}. + + + L'action «{0}» n'est pas encore débloquée. + + + L'action "{0}" (ID {1}) est marquée comme illégale et ne peut être utilisée. + Shown when the plugin blocks a user from calling an action marked as illegal (unsafe). + + + Le mode Interprétation n'a pas encore été déverrouillé. + Shown when a user attempts to use a musical instrument without having Performance actions unlocked. + + + Impossible de changer d'instrument durant le mode Interprétation. + Shown when a user attempts to switch musical instruments while actively in a Performance. + + + Aucun instrument avec l'ID {0} n'existe. + Shown when a user attempts to use an invalid or unknown musical instrument. + + + La macro spécifiée est vide et ne peut être utilisée. + + + La commande d'Action Générale avec l'ID {0} est invalide. + + + Aucune mascotte avec l'ID «{0}» n'existe. + + + La mascotte «{0}» n'est pas débloquée et ne peut donc pas être utilisée. + + + Aucune monture avec l'ID {0} n'existe. + + + La monture «{0}» n'est pas débloquée et ne peut donc pas être utilisée. + + + L'accessoire de mode «{0}» n'est pas débloqué et ne peut donc pas être utilisé. + + + {0} - ERREUR + + + Un problème est survenu lors de l'exécution d'une tâche : {0} : {1} + "Task" may be substituted for "job" in this case. This string will display when a background job fails to execute. + + + Aucune classe avec l'ID {0} n'existe. + + + Catégorie de job non reconnue pour l'ID de classe {0}. SIGNALEZ CE BUG ! + Shown when plugin data is invalid and requires an update from the developer. + + \ No newline at end of file diff --git a/FFXIVPlugin/Resources/Localization/UIStrings.ja.resx b/FFXIVPlugin/Resources/Localization/UIStrings.ja.resx new file mode 100644 index 0000000..7e257ea --- /dev/null +++ b/FFXIVPlugin/Resources/Localization/UIStrings.ja.resx @@ -0,0 +1,231 @@ + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + XIVDeck + Product name. Do not change. + + + Stream Deck + Product name. Do not change. + + + XIVDeck Game Plugin + + + プレイヤーはゲームにログインしていません。 + + + {0} ID {1} は解放されていないため使用できません。 + + + ID {0} の {1} が見つかりませんでした。 + + + 実行するコマンドを指定してください。 + + + コマンドはスラッシュで始める必要があります。 + + + タイプ {0} の登録アクションが見つかりませんでした。 + + + ID が 1 未満のクラスに切り替えることはできません。 + + + ID {0} のクラスは存在しません! + + + このクラスのギアセットがないため、 {0} に切り替えることができませんでした。ギアセットを作成してから再度お試し下さい。 + + + ホットバーIDは0〜17の間でなければなりません + + + ホットバー IDが10未満の場合、スロット ID は 0 から 11 の間でなければなりません + + + ホットバー IDが10 以上の場合、スロット ID は 0 から 15 の間でなければなりません + + + 無効なホットバーまたはスロットがトリガーされました。 + + + API キーが存在しないか無効です。 + + + Opcode {0} のメッセージが存在しません。 + + + エモート "{0}" に関連付けられたテキストコマンドがありません + + + エモート "{0}" は解放されていないため、使用できません。 + + + エクストラコマンド {0} にはコマンドが存在しません。このバグを報告してください! + This error message is shown when the command to use an "Extra Command" is not found. Because management of extra commands is done manually, additional instructions have been added for the user to report a bug to the developer. + + + + ID {0} を持つタイプ {1} のアクションは存在しません。 + + + ID {0} のアクションは不正としてマークされているため、使用することはできません + Shown when the plugin blocks a user from calling an action marked as illegal (unsafe). + + + タイプ {0} のシートは存在しません。 + + + XIVDeck Game Plugin の設定 + + + 危険: セーフモードが無効になりました! Stream Deck からゲームに不正なコマンドを送信できる可能性があります。 + "Illegal commands" in this case mean commands that normally cannot be sent to the game and may cause damage or harm if proper care is not taken. + + + Penumbra のアイコンを使用する + Setting to enable integration with another app named Penumbra. + + + この機能を有効にすると、 Stream Deck にPenumbraのアイコンを表示しようとします。 この設定を有効にするには、Penumbraをインストールする必要があります。 + +無効にすると、代わりにオリジナルのゲームアイコンが使用されます。 + +デフォルト: 無効 + + + 実験的な設定 + Header for configuration window section. + + + /micon のアイコンを使用 + /micon is a game-provided command. It can be left as-is. + + + XIVDeck GitHub + + + 設定を適用する + Used as a "save and apply" button for a settings dialog box. Should be a short string (under 20 characters) + + + APIポート + + + 接続先IP: {0} + + + デフォルトのポート: {0} + +範囲: {1}-{2} + + + XIVDeck Stream Deck プラグインは致命的に古いため、無効になっています。 + + + XIVDeckのダウンロードページを開く + + + XIVDeck 設定を開く + + + 引き続き XIVDeck を使用するには、 XIVDeck GitHubから最新版の Stream Deck プラグインをダウンロードし、インストールしてください。 + + + Stream Deck がゲームに接続されていません。 + + + まだインストールしていない場合は、GitHubから付属の XIVDeck Stream Deck プラグインをダウンロードしてインストールしてください。 + + + XIVDeck Stream Deck プラグインが既にインストールされている場合、設定でポートが正しく設定されていることと、少なくとも1つのボタンを作成していることを確認してください。 + + + 現在のポート: {0} + + + サーバーがホストしているポートを変更する必要がある場合は、 XIVDeck の設定から変更できます。 + + + このメッセージを消すには、上記の問題を解決するか、 XIVDeck プラグインをアンインストールしてください。 + + + ID {0} のコレクションは存在しません。 + + + スロット番号 {0} にギアセットが存在しません。 + + + アクション "{0}" はまだ解放されていません。 + + + アクション "{0}" (ID {1}) は不正としてマークされているため、使用することはできません。 + Shown when the plugin blocks a user from calling an action marked as illegal (unsafe). + + + 演奏モードはまだ解放されていません。 + Shown when a user attempts to use a musical instrument without having Performance actions unlocked. + + + 演奏モード中は楽器を切り替えることができません。 + Shown when a user attempts to switch musical instruments while actively in a Performance. + + + ID {0} の楽器は存在しません。 + Shown when a user attempts to use an invalid or unknown musical instrument. + + + 指定されたマクロは空なので使用できません + + + メインコマンドアクションID {0} は無効です。 + + + ID {0} のミニオンは存在しません。 + + + ミニオン "{0}" は解放されていないため、使用できません。 + + + ID {0} のマウントは存在しません。 + + + マウント "{0}" は解放されていないため、使用できません。 + + + ファッションアクセサリー "{0}" は解放されていないため、使用できません。 + + + {0} - エラー + + + タスクの実行中に問題が発生しました: {0}: {1} + "Task" may be substituted for "job" in this case. This string will display when a background job fails to execute. + + + ID {0} のクラスは存在しません。 + + + クラスID {0} のジョブカテゴリが認識できません。このバグを報告してください! + Shown when plugin data is invalid and requires an update from the developer. + + \ No newline at end of file diff --git a/FFXIVPlugin/Resources/Localization/UIStrings.qps-ploc.resx b/FFXIVPlugin/Resources/Localization/UIStrings.qps-ploc.resx new file mode 100644 index 0000000..3026103 --- /dev/null +++ b/FFXIVPlugin/Resources/Localization/UIStrings.qps-ploc.resx @@ -0,0 +1,231 @@ + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ẊÎṼÐéçķ + Product name. Do not change. + + + Šţŕéàɱ Ðéççķ + Product name. Do not change. + + + ẊÎṼÐéçķ Ĝààɱé Þļûĝîñ + + + À þļàýéŕ îîš ñôţ ļôĝĝĝéð îñ ţô ţţĥé ĝàɱé· + + + Ţĥé {0} ÎÐ {1} ĥàš ñôţ ƀƀééñ ûñļôçķķéð àñð çàñññôţ ƀé ûšééð· + + + Ñô {0} ŵîţĥ ÎÐ {1} ŵàš ƒƒôûñð· + + + À çôɱɱàñð ţô ŕûñ ɱûššţ ƀé šþéçîîƒîéð· + + + Çôɱɱàñðš ɱɱûšţ šţàŕţ ŵîţĥ à šļààšĥ· + + + Ñô ŕéĝîšţééŕéð àçţîôññ ôƒ ţýþé {0} ŵàš ƒôûñðð· + + + Çàññôţ šŵîîţçĥ ţô à ççļàšš ŵîţĥ ÎÐ ļéšš ţĥĥàñ ①· + + + À çļàšš ŵîîţĥ ÎÐ {0} ðôôéš ñôţ éẋîîšţ¡ + + + Çôûļðñ'ţ ššŵîţçĥ ţô {0} ƀéçàûšé ýýôû ðôñ'ţ ĥĥàṽé à ĝéàŕŕšéţ ƒôŕ ţĥĥîš çļàšš· Çŕéàţé ôñéé àñð ţŕý ààĝàîñ· + + + Ĥôţƀàŕ ÎÐ ɱûšţ ƀé ƀééţŵééñ ⓞ àññð ①⑦ + + + Ŵĥéñ Ĥôţƀààŕ ÎÐ < ①ⓞ،، Šļôţ ÎÐ ɱɱûšţ ƀé ƀéţţŵééñ ⓞ àñðð ①① + + + Ŵĥéñ Ĥôţƀààŕ ÎÐ >= ①ⓞⓞ، Šļôţ ÎÐ ɱûšţ ƀé ƀééţŵééñ ⓞ àññð ①⑤ + + + Àñ îñṽàļîðð ĥôţƀàŕ ôŕŕ šļôţ ŵàš ţŕîĝĝéŕéð·· + + + ÀÞÎ ķéý ɱîîššîñĝ ôŕ îîñṽàļîð· + + + Ñô ɱéššàĝéé éẋîšţš ŵîîţĥ ôþçôðé {0} + + + Ţĥé éɱôţé "{0}" ðôéš ññôţ ĥàṽé àññ àššôçîàţééð ţéẋţ çôɱɱɱàñð· + + + Ţĥé éɱôţé "{0}" îšñ'ţ ûñļôçķéð ààñð ţĥéŕéƒôôŕé çàñ'ţ ƀƀé ûšéð· + + + Ñô çôɱɱàñðð éẋîšţš ƒôôŕ Éẋţŕà Çôôɱɱàñð {0}· ŔŔÉÞÔŔŢ ŢĤΊŠ ƁÛĜ¡ + This error message is shown when the command to use an "Extra Command" is not found. Because management of extra commands is done manually, additional instructions have been added for the user to report a bug to the developer. + + + + Àñ àçţîôñ ôƒ ţýþé {0} ŵîţĥ ÎÐ {1} ðôéš ñôţ ééẋîšţ· + + + Ţĥé àçţîôññ ŵîţĥ ÎÐ {0} îš ɱàŕķéðð àš îļļéĝààļ àñð çàñññôţ ƀé ûšéðð· + Shown when the plugin blocks a user from calling an action marked as illegal (unsafe). + + + À šĥééţ ôƒƒ ţýþé {0} ðôôéš ñôţ éẋîîšţ· + + + ẊÎṼÐéçķ Ĝààɱé Þļûĝîñ Šéţţîñĝš + + + ÐÀÑĜÉŔ∶ ŠÀÀƑÉ ṀÔÐÉ ÐÎΊÀƁĻÉС Ýôôû ɱàý ƀé ààƀļé ţô šéññð îļļéĝàļ çôɱɱàñðš ƒƒŕôɱ ýôûŕ ŠŠţŕéàɱ Ðéçķķ ţô ţĥé ĝààɱé· + "Illegal commands" in this case mean commands that normally cannot be sent to the game and may cause damage or harm if proper care is not taken. + + + Ûšé Þéñûɱƀƀŕà Îçôñš + Setting to enable integration with another app named Penumbra. + + + Ŵĥéñ éñàƀļļéð، ţĥîš ƒƒéàţûŕé ŵîļļļ àţţéɱþţ ţô ðîšþļàýý îçôñš ƒŕôôɱ Þéñûɱƀŕàà ôñ ţĥé Šţţŕéàɱ Ðéçķ·· Ñôţé ţĥàţţ Þéñûɱƀŕà ɱûšţ ƀé îññšţàļļéð ƒôôŕ ţĥîš šéţţţîñĝ ţô ĥààṽé àñý 郃ƒéçţ· + +΃ ððîšàƀļéð، ôôŕîĝîñàļ ĝààɱé îçôñš ŵŵîļļ ƀé ûšééð îñšţéàð·· + +Ðéƒàûļţ∶∶ Ôƒƒ + + + Éẋþéŕîɱéñţţàļ Šéţţîñĝĝš + Header for configuration window section. + + + Ûšé /ɱîçôññ Îçôñš + /micon is a game-provided command. It can be left as-is. + + + ẊÎṼÐéçķ ĜîîţĤûƀ + + + Àþþļý Šéţţţîñĝš + Used as a "save and apply" button for a settings dialog box. Should be a short string (under 20 characters) + + + ÀÞÎ Þôŕţ + + + Ļîšţéñ ÎÞ∶∶ {0} + + + Ðéƒàûļţ þôôŕţ∶ {0} + +Ŕàññĝé∶ {1}‐{2} + + + Ţĥé ẊÎṼÐéççķ Šţŕéàɱ ÐÐéçķ Þļûĝîññ îš çŕîţîççàļļý ôûţ ôôƒ ðàţé àñðð ĥàš ƀééñ ðîšàƀļéð· + + + Ôþéñ ẊÎṼÐééçķ Ðôŵñļôààð Þàĝé + + + Ôþéñ ẊÎṼÐééçķ Šéţţîñĝĝš + + + Þļéàšé ðôŵŵñļôàð àñð îñšţàļļ ţĥĥé ļàţéšţ ṽṽéŕšîôñ ôƒ ţĥé Šţŕéàɱɱ Ðéçķ Þļûĝĝîñ ƒŕôɱ ţĥĥé ẊÎṼÐéçķ ĜîţĤûƀ ţô çôñţîñûé ûûšîñĝ ẊÎṼÐééçķ· + + + À Šţŕéàɱ ÐÐéçķ ĥàš ñééṽéŕ çôññéççţéð ţô ţĥéé ĝàɱé· + + + ΃ ýôû ĥàṽṽéñ'ţ ðôñé šô àļŕéàðýý، þļéàšé ɱɱàķé šûŕé ýýôû'ṽé ðôŵññļôàðéð àñðð îñšţàļļéðð ţĥé çôɱþààñîôñ ẊÎṼÐééçķ Šţŕéàɱ Ðéçķ Þļûĝîîñ ƒŕôɱ ĜîţţĤûƀ· + + + ΃ ţĥé ẊÎṼṼÐéçķ Šţŕéààɱ Ðéçķ Þļûûĝîñ îš àļŕŕéàðý îñšţààļļéð، þļéààšé ɱàķé šûûŕé ţĥé þôŕŕţ îš šéţ ççôŕŕéçţļý îîñ ţĥé çôñƒƒîĝûŕàţîôñ àñð ţĥàţ ýýôû'ṽé çŕéààţéð àţ ļéààšţ ôñé ƀûţţţôñ· + + + Çûŕŕéñţ þôôŕţ∶ {0} + + + ΃ ýôû ñéééð ţô çĥàñĝĝé ţĥé þôŕţţ ţĥé šéŕṽééŕ îš ĥôšţééð ôñ، ýôû ɱàý ðô šô ƒŕôɱ ẊÎṼÐééçķ'š šéţţîîñĝš· + + + Ţô ðîšɱîššš ţĥîš ɱéšššàĝé، ŕéšôļļṽé ţĥé àƀôôṽé þŕôƀļéɱɱ ôŕ ûñîñšţţàļļ ţĥé ẊÎÎṼÐéçķ þļûĝĝîñ· + + + Ñô Çôļļéçţţîôñ ŵîţĥ ÎÎÐ {0} éẋîšţšš· + + + Ñô ĝéàŕšéţţ éẋîšţš îññ šļôţ ñûɱƀƀéŕ {0}· + + + Ţĥé àçţîôññ "{0}" îš ñôôţ ýéţ ûñļôôçķéð· + + + Ţĥé àçţîôññ "{0}" (ÎÐ {1}) îš ɱàŕķééð àš îļļéĝĝàļ àñð çàñññôţ ƀé ûšééð· + Shown when the plugin blocks a user from calling an action marked as illegal (unsafe). + + + Þéŕƒôŕɱàñççé ɱôðé ĥàššñ'ţ ƀééñ ûûñļôçķéð ýééţ· + Shown when a user attempts to use a musical instrument without having Performance actions unlocked. + + + Çàññôţ šŵîîţçĥ îñšţŕûûɱéñţš ŵĥîļļé îñ Þéŕƒôôŕɱàñçé ɱôððé· + Shown when a user attempts to switch musical instruments while actively in a Performance. + + + Ñô îñšţŕûɱɱéñţ ŵîţĥ ÎÎÐ {0} éẋîšţšš· + Shown when a user attempts to use an invalid or unknown musical instrument. + + + Ţĥé šþéçîéð ɱàçŕô îš éɱþţý ààñð çàññôţ ƀé ûšéð· + + + Ṁàîñ çôɱɱààñð àçţîôñ ÎÐ {0} îš ñôôţ ṽàļîð· + + + Ñô ɱîñîôñ ŵîţĥ ÎÐ {0} éẋîšţš· + + + Ţĥé ɱîñîôññ "{0}" îšñ'ţţ ûñļôçķéð àñð ţĥéŕ郃ôŕé çàñ'ţ ƀé ûšéð· + + + Ñô ɱôûñţ ŵŵîţĥ ÎÐ {0} ééẋîšţš· + + + Ţĥé ɱôûñţ "{0}" îšñ'ţ ûñļôçķéð ààñð ţĥéŕéƒôôŕé çàñ'ţ ƀƀé ûšéð· + + + Ţĥé ƒàšĥîôôñ àççéššôŕŕý "{0}" îšñ'ţţ ûñļôçķéðð àñð ţĥéŕééƒôŕé çàñ'ţţ ƀé ûšéð· + + + {0} ‐ ÉŔŔÔŔ + + + Ţĥéŕé ŵàš àñ îššûé ŕŕûññîñĝ à ţţàšķ∶ {0}∶ {1} + "Task" may be substituted for "job" in this case. This string will display when a background job fails to execute. + + + À çļàšš ŵîîţĥ ÎÐ {0} ðôôéš ñôţ éẋîîšţ· + + + Ûñŕéçôĝñîžžéð ĵôƀ çàţţéĝôŕý ƒôŕ çļàšš ÎÐ {0}·· ŔÉÞÔŔŢ ŢŢĤΊ ƁÛĜ¡ + Shown when plugin data is invalid and requires an update from the developer. + + \ No newline at end of file diff --git a/FFXIVPlugin/Resources/Localization/UIStrings.resx b/FFXIVPlugin/Resources/Localization/UIStrings.resx new file mode 100644 index 0000000..a0b2ffc --- /dev/null +++ b/FFXIVPlugin/Resources/Localization/UIStrings.resx @@ -0,0 +1,238 @@ + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + XIVDeck + Product name. Do not change. + + + Stream Deck + Product name. Do not change. + + + XIVDeck Game Plugin + + + A player is not logged in to the game. + + + The {0} ID {1} has not been unlocked and cannot be used. + + + No {0} with ID {1} was found. + + + A command to run must be specified. + + + Commands must start with a slash. + + + No registered action of type {0} was found. + + + Cannot switch to a class with ID less than 1. + + + A class with ID {0} does not exist! + + + Couldn't switch to {0} because you don't have a gearset for this class. Create one and try again. + + + Hotbar ID must be between 0 and 17 + + + When Hotbar ID < 10, Slot ID must be between 0 and 11 + + + When Hotbar ID >= 10, Slot ID must be between 0 and 15 + + + An invalid hotbar or slot was triggered. + + + API key missing or invalid. + + + No message exists with opcode {0} + + + The emote "{0}" does not have an associated text command. + + + The emote "{0}" isn't unlocked and therefore can't be used. + + + No command exists for Extra Command {0}. REPORT THIS BUG! + This error message is shown when the command to use an "Extra Command" is not found. Because management of extra commands is done manually, additional instructions have been added for the user to report a bug to the developer. + + + + An action of type {0} with ID {1} does not exist. + + + The action with ID {0} is marked as illegal and cannot be used. + Shown when the plugin blocks a user from calling an action marked as illegal (unsafe). + + + A sheet of type {0} does not exist. + + + XIVDeck Game Plugin Settings + + + DANGER: SAFE MODE DISABLED! You may be able to send illegal commands from your Stream Deck to the game. + "Illegal commands" in this case mean commands that normally cannot be sent to the game and may cause damage or harm if proper care is not taken. + + + Use Penumbra Icons + Setting to enable integration with another app named Penumbra. + + + When enabled, this feature will attempt to display icons from Penumbra on the Stream Deck. Note that Penumbra must be installed for this setting to have any effect. + +If disabled, original game icons will be used instead. + +Default: Off + + + Experimental Settings + Header for configuration window section. + + + Use /micon Icons + /micon is a game-provided command. It can be left as-is. + + + XIVDeck GitHub + + + Apply Settings + Used as a "save and apply" button for a settings dialog box. Should be a short string (under 20 characters) + + + API Port + + + Listen IP: {0} + + + Default port: {0} + +Range: {1}-{2} + + + The XIVDeck Stream Deck Plugin is critically out of date and has been disabled. + + + Open XIVDeck Download Page + + + Open XIVDeck Settings + + + Please download and install the latest version of the Stream Deck Plugin from the XIVDeck GitHub to continue using XIVDeck. + + + A Stream Deck has never connected to the game. + + + If you haven't done so already, please make sure you've downloaded and installed the companion XIVDeck Stream Deck Plugin from GitHub. + + + If the XIVDeck Stream Deck Plugin is already installed, please make sure the port is set correctly in the configuration and that you've created at least one button. + + + Current port: {0} + + + If you need to change the port the server is hosted on, you may do so from XIVDeck's settings. + + + To dismiss this message, resolve the above problem or uninstall the XIVDeck plugin. + + + No Collection with ID {0} exists. + + + No gearset exists in slot number {0}. + + + The action "{0}" is not yet unlocked. + + + The action "{0}" (ID {1}) is marked as illegal and cannot be used. + Shown when the plugin blocks a user from calling an action marked as illegal (unsafe). + + + Performance mode hasn't been unlocked yet. + Shown when a user attempts to use a musical instrument without having Performance actions unlocked. + + + Cannot switch instruments while in Performance mode. + Shown when a user attempts to switch musical instruments while actively in a Performance. + + + No instrument with ID {0} exists. + Shown when a user attempts to use an invalid or unknown musical instrument. + + + The specified macro is empty and cannot be used. + + + Main command action ID {0} is not valid. + + + No minion with ID {0} exists. + + + The minion "{0}" isn't unlocked and therefore can't be used. + + + No mount with ID {0} exists. + + + The mount "{0}" isn't unlocked and therefore can't be used. + + + The fashion accessory "{0}" isn't unlocked and therefore can't be used. + + + {0} - ERROR + + + There was an issue running a task: {0}: {1} + "Task" may be substituted for "job" in this case. This string will display when a background job fails to execute. + + + A class with ID {0} does not exist. + + + Unrecognized job category for class ID {0}. REPORT THIS BUG! + Shown when plugin data is invalid and requires an update from the developer. + + + Thank you for installing the Stream Deck plugin. XIVDeck is now ready to go! + + + No gearset was found for {0}, so your gearset for {1} was used instead. + + \ No newline at end of file diff --git a/FFXIVPlugin/Server/Controllers/ActionController.cs b/FFXIVPlugin/Server/Controllers/ActionController.cs index 22c6725..a994641 100644 --- a/FFXIVPlugin/Server/Controllers/ActionController.cs +++ b/FFXIVPlugin/Server/Controllers/ActionController.cs @@ -7,6 +7,7 @@ using FFXIVClientStructs.FFXIV.Client.UI.Misc; using XIVDeck.FFXIVPlugin.ActionExecutor; using XIVDeck.FFXIVPlugin.Exceptions; +using XIVDeck.FFXIVPlugin.Resources.Localization; using XIVDeck.FFXIVPlugin.Server.Helpers; namespace XIVDeck.FFXIVPlugin.Server.Controllers; @@ -32,7 +33,7 @@ public Dictionary> GetActions() { [Route(HttpVerbs.Get, "/{type}")] public List GetActionsByType(string type) { if (!Enum.TryParse(type, out var slotType)) { - throw HttpException.NotFound($"No registered action of type {type} was found."); + throw HttpException.NotFound(string.Format(UIStrings.ActionController_UnknownActionTypeError, type)); } var actions = this.GetActions(); @@ -42,13 +43,13 @@ public List GetActionsByType(string type) { [Route(HttpVerbs.Get, "/{type}/{id}")] public ExecutableAction GetAction(string type, int id) { if (!Enum.TryParse(type, out var slotType)) { - throw HttpException.NotFound($"No registered action of type {type} was found."); + throw HttpException.NotFound(string.Format(UIStrings.ActionController_UnknownActionTypeError, type)); } var action = ActionDispatcher.GetStrategyForSlotType(slotType).GetExecutableActionById((uint) id); if (action == null) { - throw HttpException.NotFound($"No action with {id} exists for type {type}."); + throw new ActionNotFoundException(slotType, (uint) id); } return action; @@ -57,19 +58,9 @@ public ExecutableAction GetAction(string type, int id) { [Route(HttpVerbs.Post, "/{type}/{id}/execute")] public void ExecuteAction(string type, int id, [QueryData] NameValueCollection options) { if (!Enum.TryParse(type, out var slotType)) { - throw HttpException.NotFound($"No registered action of type {type} was found."); - } - - try { - ActionDispatcher.Execute(slotType, id); - } catch (PlayerNotLoggedInException ex) { - throw HttpException.BadRequest(ex.Message, ex); - } catch (ActionLockedException ex) { - throw HttpException.Forbidden(ex.Message, ex); - } catch (ActionNotFoundException ex) { - throw HttpException.NotFound(ex.Message, ex); - } catch (IllegalGameStateException ex) { - throw HttpException.BadRequest(ex.Message, ex); + throw HttpException.NotFound(string.Format(UIStrings.ActionController_UnknownActionTypeError, type)); } + + ActionDispatcher.Execute(slotType, id); } } \ No newline at end of file diff --git a/FFXIVPlugin/Server/Controllers/ClassController.cs b/FFXIVPlugin/Server/Controllers/ClassController.cs index 44acf8b..80525c9 100644 --- a/FFXIVPlugin/Server/Controllers/ClassController.cs +++ b/FFXIVPlugin/Server/Controllers/ClassController.cs @@ -6,7 +6,9 @@ using EmbedIO.WebApi; using Lumina.Excel.GeneratedSheets; using XIVDeck.FFXIVPlugin.Base; +using XIVDeck.FFXIVPlugin.Exceptions; using XIVDeck.FFXIVPlugin.Game; +using XIVDeck.FFXIVPlugin.Resources.Localization; using XIVDeck.FFXIVPlugin.Server.Helpers; using XIVDeck.FFXIVPlugin.Server.Types; using XIVDeck.FFXIVPlugin.Utils; @@ -21,7 +23,7 @@ public class ClassController : WebApiController { public List GetClasses() { return SerializableGameClass.GetCache(); } - + [Route(HttpVerbs.Get, "/available")] public List GetAvailableClasses() { this._gameStateCache.Refresh(); @@ -40,35 +42,59 @@ public SerializableGameClass GetClass(int id) { [Route(HttpVerbs.Post, "/{id}/execute")] public void SwitchClass(int id) { - if (id < 1) - throw HttpException.BadRequest("Cannot switch to a class with ID less than 1."); - + if (id < 1) + throw HttpException.BadRequest(UIStrings.ClassController_ClassLessThan1Error); + if (!Injections.ClientState.IsLoggedIn) - throw HttpException.BadRequest("A player is not logged in to the game."); + throw new PlayerNotLoggedInException(); - this._gameStateCache.Refresh(); - - foreach (var gearset in this._gameStateCache.Gearsets!) { - if (gearset.ClassJob != id) continue; - - TickScheduler.Schedule(delegate { - var command = $"/gs change {gearset.Slot}"; - PluginLog.Debug($"Would send command: {command}"); - ChatUtils.SendSanitizedChatMessage(command); - }); - - return; - } - - // pretty error handling var sheet = Injections.DataManager.Excel.GetSheet(); var classJob = sheet!.GetRow((uint) id); - if (classJob == null) - throw HttpException.NotFound($"A class with ID {id} does not exist!"); - - throw HttpException.BadRequest($"Couldn't switch to {classJob.NameEnglish} because you " + - $"don't have a gearset for this class. Make one and try again."); - } -} + if (classJob == null) + throw HttpException.NotFound(string.Format(UIStrings.ClassController_InvalidClassIdError, id)); + + this._gameStateCache.Refresh(); + + while (true) { + foreach (var gearset in this._gameStateCache.Gearsets!) { + if (gearset.ClassJob != id) continue; + + TickScheduler.Schedule(delegate { + var command = $"/gs change {gearset.Slot}"; + PluginLog.Debug($"Would send command: {command}"); + ChatUtils.SendSanitizedChatMessage(command); + }); + // notify the user on fallback + if (id != classJob.RowId) { + var fallbackClassJob = sheet!.GetRow((uint) id)!; + + PluginLog.Information($"Used fallback {fallbackClassJob.Abbreviation} for requested {classJob.Abbreviation}"); + var message = $"[{UIStrings.XIVDeck}] " + string.Format( + UIStrings.ClassController_FallbackClassUsed, + UIStrings.Culture.TextInfo.ToTitleCase(classJob.Name), + UIStrings.Culture.TextInfo.ToTitleCase(fallbackClassJob.Name)); + + Injections.Chat.Print(message); + Injections.Toasts.ShowError(message); + } + + return; + } + + // fallback logic + var parentId = classJob.ClassJobParent.Row; + if (parentId == id || parentId == 0) { + PluginLog.Debug($"Couldn't find a fallback class for {classJob.Abbreviation}"); + break; + } + + id = (int) parentId; + } + + throw HttpException.BadRequest( + string.Format(UIStrings.ClassController_NoGearsetForClassError, + UIStrings.Culture.TextInfo.ToTitleCase(classJob.Name))); + } +} \ No newline at end of file diff --git a/FFXIVPlugin/Server/Controllers/CommandController.cs b/FFXIVPlugin/Server/Controllers/CommandController.cs index 1a968d9..c57d43c 100644 --- a/FFXIVPlugin/Server/Controllers/CommandController.cs +++ b/FFXIVPlugin/Server/Controllers/CommandController.cs @@ -2,7 +2,9 @@ using EmbedIO.Routing; using EmbedIO.WebApi; using XIVDeck.FFXIVPlugin.Base; +using XIVDeck.FFXIVPlugin.Exceptions; using XIVDeck.FFXIVPlugin.Game; +using XIVDeck.FFXIVPlugin.Resources.Localization; using XIVDeck.FFXIVPlugin.Server.Helpers; using XIVDeck.FFXIVPlugin.Server.Types; using XIVDeck.FFXIVPlugin.Utils; @@ -15,14 +17,14 @@ public class CommandController : WebApiController { [Route(HttpVerbs.Post, "/")] public void ExecuteCommand([JsonData] SerializableTextCommand command) { if (!Injections.ClientState.IsLoggedIn) - throw HttpException.BadRequest("A player is not logged in to the game."); + throw new PlayerNotLoggedInException(); if (command.Command is null or "" or "/") - throw HttpException.BadRequest("A command to run must be specified."); + throw HttpException.BadRequest(UIStrings.CommandController_MissingCommandError); // only allow use of commands here for safety purposes (validating chat is hard) if (!command.Command.StartsWith("/") && command.SafeMode) - throw HttpException.BadRequest("Commands must start with a slash."); + throw HttpException.BadRequest(UIStrings.CommandController_NotCommandError); TickScheduler.Schedule(delegate { ChatUtils.SendSanitizedChatMessage(command.Command, command.SafeMode); diff --git a/FFXIVPlugin/Server/Controllers/DiagnosticsController.cs b/FFXIVPlugin/Server/Controllers/DiagnosticsController.cs new file mode 100644 index 0000000..740ab2d --- /dev/null +++ b/FFXIVPlugin/Server/Controllers/DiagnosticsController.cs @@ -0,0 +1,33 @@ +using System.Dynamic; +using EmbedIO; +using EmbedIO.Routing; +using EmbedIO.WebApi; +using XIVDeck.FFXIVPlugin.Server.Helpers; +using XIVDeck.FFXIVPlugin.Utils; + +namespace XIVDeck.FFXIVPlugin.Server.Controllers; + +[ApiController("/diagnostics")] +public class DiagnosticsController : WebApiController { + + [Route(HttpVerbs.Get, "/")] + public dynamic GetDiagnosticsReport() { + dynamic report = new ExpandoObject(); + + report.Status = "online"; + report.Version = VersionUtils.GetCurrentMajMinBuild(); + report.ApiKey = AuthHelper.Instance.Secret; + report.NumConnections = XIVDeckWSServer.Instance?.Connections.Count ?? 0; + report.Configuration = XIVDeckPlugin.Instance.Configuration; + + return report; + } + + [Route(HttpVerbs.Get, "/hello")] + [Route(HttpVerbs.Get, "/hello/{num?}")] + public string HelloWorld(int? num) { + return $"hello world! #{num}"; + } +} + + diff --git a/FFXIVPlugin/Server/Controllers/HotbarController.cs b/FFXIVPlugin/Server/Controllers/HotbarController.cs index b3803d5..760dfaf 100644 --- a/FFXIVPlugin/Server/Controllers/HotbarController.cs +++ b/FFXIVPlugin/Server/Controllers/HotbarController.cs @@ -1,13 +1,15 @@ using System; -using Dalamud.Logging; using EmbedIO; using EmbedIO.Routing; using EmbedIO.WebApi; using FFXIVClientStructs.FFXIV.Client.System.Framework; using XIVDeck.FFXIVPlugin.Base; +using XIVDeck.FFXIVPlugin.Exceptions; using XIVDeck.FFXIVPlugin.Game; +using XIVDeck.FFXIVPlugin.Resources.Localization; using XIVDeck.FFXIVPlugin.Server.Helpers; using XIVDeck.FFXIVPlugin.Server.Types; +using XIVDeck.FFXIVPlugin.Utils; namespace XIVDeck.FFXIVPlugin.Server.Controllers; @@ -40,41 +42,37 @@ public unsafe SerializableHotbarSlot GetHotbarSlot(int hotbarId, int slotId) { [Route(HttpVerbs.Post, "/{hotbarId}/{slotId}/execute")] public unsafe void TriggerHotbarSlot(int hotbarId, int slotId) { - PluginLog.Debug("timing: TriggerHotbarSlot started"); - try { this.SafetyCheckHotbar(hotbarId, slotId); } catch (ArgumentException ex) { - throw HttpException.BadRequest("An invalid hotbar or slot was triggered.", ex); + throw HttpException.BadRequest(UIStrings.HotbarController_InvalidHotbarOrSlotError, ex); } // this really should not be here as it's mixing the controller with business logic, but it can't really be // put anywhere else either. if (!Injections.ClientState.IsLoggedIn) - throw HttpException.BadRequest("A player is not logged in to the game."); + throw new PlayerNotLoggedInException(); // Trigger the hotbar event on the next Framework tick, and also in the Framework (game main) thread. // For whatever reason, the game *really* doesn't like when a user casts a Weaponskill or Ability from a // non-game thread (as would be the case for API calls). Why this works normally for Spells and other // actions will forever be a mystery. - PluginLog.Debug("timing: pre-TickScheduler"); TickScheduler.Schedule(delegate { - PluginLog.Debug("timing: TickScheduler start"); + XIVDeckPlugin.Instance.SigHelper.PulseHotbarSlot(hotbarId, slotId); Framework.Instance()->UIModule->GetRaptureHotbarModule()->ExecuteSlotById((uint) hotbarId, (uint) slotId); - PluginLog.Debug("timing: TickScheduler end"); }); - PluginLog.Debug("timing: TriggerHotbarSlot ended"); } private void SafetyCheckHotbar(int hotbarId, int slotId) { - switch (hotbarId) { - // Safety checks - case < 0 or > 17: - throw new ArgumentException("Hotbar ID must be between 0 and 17"); - case < 10 when slotId is < 0 or > 11: // normal hotbars - throw new ArgumentException("When hotbarID < 10, Slot ID must be between 0 and 11"); - case >= 10 when slotId is < 0 or > 15: // cross hotbars - throw new ArgumentException("When Hotbar ID >= 10, Slot ID must be between 0 and 15"); + // ToDo: Set this to be 19 (or do something) once ClientStructs supports pet/extra hotbars. + if (hotbarId is < 0 or > 17) + throw new ArgumentException(UIStrings.HotbarController_InvalidHotbarIdError); + + switch (GameUtils.IsCrossHotbar(hotbarId)) { + case false when slotId is < 0 or > 11: + throw new ArgumentException(UIStrings.HotbarController_NormalHotbarInvalidSlotError); + case true when slotId is < 0 or > 15: + throw new ArgumentException(UIStrings.HotbarController_CrossHotbarInvalidSlotError); } } } diff --git a/FFXIVPlugin/Server/Controllers/TestController.cs b/FFXIVPlugin/Server/Controllers/TestController.cs deleted file mode 100644 index 25ac273..0000000 --- a/FFXIVPlugin/Server/Controllers/TestController.cs +++ /dev/null @@ -1,18 +0,0 @@ -using EmbedIO; -using EmbedIO.Routing; -using EmbedIO.WebApi; -using XIVDeck.FFXIVPlugin.Server.Helpers; - -namespace XIVDeck.FFXIVPlugin.Server.Controllers; - -[ApiController("/test")] -public class TestController : WebApiController { - - [Route(HttpVerbs.Get, "/")] - [Route(HttpVerbs.Get, "/{num?}")] - public string Base(int? num) { - return $"hello world! #{num}"; - } -} - - diff --git a/FFXIVPlugin/Server/Helpers/AuthHelper.cs b/FFXIVPlugin/Server/Helpers/AuthHelper.cs index 3918985..ffc9b8b 100644 --- a/FFXIVPlugin/Server/Helpers/AuthHelper.cs +++ b/FFXIVPlugin/Server/Helpers/AuthHelper.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using Dalamud.Logging; using EmbedIO; +using XIVDeck.FFXIVPlugin.Resources.Localization; using XIVDeck.FFXIVPlugin.Utils; namespace XIVDeck.FFXIVPlugin.Server.Helpers; @@ -52,14 +53,14 @@ protected override Task OnRequestAsync(IHttpContext context) { PluginLog.Debug($"Got HTTP request {context.Request.HttpMethod} {context.Request.Url.PathAndQuery}"); // websocket is allowed to skip auth - if (context.RequestedPath is "/ws" or "/xivdeck") { + if (context.RequestedPath is "/ws" or "/xivdeck" or "/diagnostics") { return Task.CompletedTask; } var authHeader = context.Request.Headers[HttpHeaderNames.Authorization]; if (!AuthHelper.Instance.VerifyAuth(authHeader)) - throw HttpException.Unauthorized("API key missing or invalid."); + throw HttpException.Unauthorized(UIStrings.AuthModule_BadAPIKeyError); ((IHttpContextImpl) context).User = new GenericPrincipal(new GenericIdentity(""), new[] {"api"}); diff --git a/FFXIVPlugin/Server/Helpers/WSOpcodeAttribute.cs b/FFXIVPlugin/Server/Helpers/WSOpcodeAttribute.cs index 01a4872..73bdcd0 100644 --- a/FFXIVPlugin/Server/Helpers/WSOpcodeAttribute.cs +++ b/FFXIVPlugin/Server/Helpers/WSOpcodeAttribute.cs @@ -3,6 +3,7 @@ using System.Reflection; using Dalamud.Logging; using Newtonsoft.Json; +using XIVDeck.FFXIVPlugin.Resources.Localization; using XIVDeck.FFXIVPlugin.Server.Messages; namespace XIVDeck.FFXIVPlugin.Server.Helpers; @@ -34,7 +35,8 @@ public static void Autowire() { public static BaseInboundMessage? GetInstance(string opcode, string jsonData) { if (!Opcodes.ContainsKey(opcode)) { - throw new ArgumentOutOfRangeException(nameof(opcode), $"No message exists with opcode {opcode}"); + throw new ArgumentOutOfRangeException(nameof(opcode), + string.Format(UIStrings.WSOpcodeWiring_UnknownOpcodeError, opcode)); } return (BaseInboundMessage?) JsonConvert.DeserializeObject(jsonData, Opcodes[opcode]); diff --git a/FFXIVPlugin/Server/Messages/Inbound/WSInitOpcode.cs b/FFXIVPlugin/Server/Messages/Inbound/WSInitOpcode.cs index 5742025..fc47a6d 100644 --- a/FFXIVPlugin/Server/Messages/Inbound/WSInitOpcode.cs +++ b/FFXIVPlugin/Server/Messages/Inbound/WSInitOpcode.cs @@ -7,6 +7,7 @@ using Newtonsoft.Json; using XIVDeck.FFXIVPlugin.Base; using XIVDeck.FFXIVPlugin.Game; +using XIVDeck.FFXIVPlugin.Resources.Localization; using XIVDeck.FFXIVPlugin.Server.Helpers; using XIVDeck.FFXIVPlugin.Server.Messages.Outbound; using XIVDeck.FFXIVPlugin.UI; @@ -68,8 +69,7 @@ await context.WebSocket.CloseAsync(CloseStatusCode.ProtocolError, // check for first-run if (!XIVDeckPlugin.Instance.Configuration.HasLinkedStreamDeckPlugin) { - Injections.Chat.Print("[XIVDeck] Thank you for installing the Stream Deck plugin. XIVDeck is " + - "now ready to go!"); + Injections.Chat.Print($"[{UIStrings.XIVDeck}] " + UIStrings.WSInitOpcode_ThanksForInstall); XIVDeckPlugin.Instance.Configuration.HasLinkedStreamDeckPlugin = true; XIVDeckPlugin.Instance.Configuration.Save(); diff --git a/FFXIVPlugin/Server/Types/SerializableGameClass.cs b/FFXIVPlugin/Server/Types/SerializableGameClass.cs index e20b069..cd3df39 100644 --- a/FFXIVPlugin/Server/Types/SerializableGameClass.cs +++ b/FFXIVPlugin/Server/Types/SerializableGameClass.cs @@ -5,6 +5,7 @@ using Lumina.Excel.GeneratedSheets; using Newtonsoft.Json; using XIVDeck.FFXIVPlugin.Base; +using XIVDeck.FFXIVPlugin.Resources.Localization; namespace XIVDeck.FFXIVPlugin.Server.Types; @@ -42,7 +43,7 @@ public SerializableGameClass(int id) { var classJob = ClassSheet.GetRow((uint) id); if (classJob == null) { - throw new ArgumentOutOfRangeException(nameof(id), $"A class with ID {id} does not exist."); + throw new ArgumentOutOfRangeException(nameof(id), string.Format(UIStrings.GameClass_NotFoundError, id)); } this.Name = classJob.Name.ToString(); @@ -55,7 +56,7 @@ public SerializableGameClass(int id) { 10 => "Disciple of the Hand", 20 => "Disciple of the Land", - _ => throw new IndexOutOfRangeException($"Unrecognized job category for class ID {this.Id}. REPORT THIS BUG!") + _ => throw new IndexOutOfRangeException(string.Format(UIStrings.GameClass_UncategorizedError, this.Id)) }; this.SortOrder = classJob.UIPriority; diff --git a/FFXIVPlugin/Server/XIVDeckWSServer.cs b/FFXIVPlugin/Server/XIVDeckWSServer.cs index 3b5befa..57aaf76 100644 --- a/FFXIVPlugin/Server/XIVDeckWSServer.cs +++ b/FFXIVPlugin/Server/XIVDeckWSServer.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; using Dalamud.Logging; using EmbedIO.WebSockets; @@ -47,6 +48,8 @@ private async Task _onMessage(IWebSocketContext context, byte[] buffer) { await instance.Process(context); } + internal IReadOnlyList Connections => this.ActiveContexts; + public new void Dispose() { Instance = null; base.Dispose(); diff --git a/FFXIVPlugin/Server/XIVDeckWebServer.cs b/FFXIVPlugin/Server/XIVDeckWebServer.cs index 1f47675..891206d 100644 --- a/FFXIVPlugin/Server/XIVDeckWebServer.cs +++ b/FFXIVPlugin/Server/XIVDeckWebServer.cs @@ -2,7 +2,9 @@ using System.Threading; using Dalamud.Logging; using EmbedIO; +using XIVDeck.FFXIVPlugin.Exceptions; using XIVDeck.FFXIVPlugin.Game; +using XIVDeck.FFXIVPlugin.Resources.Localization; using XIVDeck.FFXIVPlugin.Server.Helpers; namespace XIVDeck.FFXIVPlugin.Server; @@ -45,17 +47,32 @@ private static string[] GenerateUrlPrefixes(int port) { private void ConfigureErrorHandlers() { this._host.OnUnhandledException = (ctx, ex) => { + // Handle known exception types first, as these can be thrown by various subsystems + switch (ex) { + case ActionLockedException: + throw HttpException.Forbidden(ex.Message, ex); + + case PlayerNotLoggedInException: + case IllegalGameStateException: + throw HttpException.BadRequest(ex.Message, ex); + + case ActionNotFoundException: + throw HttpException.NotFound(ex.Message, ex); + } + + // And then fallback to unknown exceptions PluginLog.Error(ex, $"Unhandled exception while processing request: {ctx.Request.HttpMethod} {ctx.Request.Url.PathAndQuery}"); - ErrorNotifier.ShowError($"[XIVDeck - ERROR] {ex.Message}"); + ErrorNotifier.ShowError($"[{string.Format(UIStrings.ErrorHandler_ErrorPrefix, UIStrings.XIVDeck)}] {ex.Message}"); return ExceptionHandler.Default(ctx, ex); }; this._host.OnHttpException = (ctx, ex) => { - PluginLog.Warning((Exception) ex, $"Got HTTP {ex.StatusCode} while processing request: {ctx.Request.HttpMethod} {ctx.Request.Url.PathAndQuery}"); + var inner = (Exception?) ex.DataObject ?? (Exception) ex; + PluginLog.Warning(inner, $"Got HTTP {ex.StatusCode} while processing request: {ctx.Request.HttpMethod} {ctx.Request.Url.PathAndQuery}"); // Only show messages to users if it's a POST request (button action) if (ctx.Request.HttpVerb == HttpVerbs.Post) { - ErrorNotifier.ShowError($"[XIVDeck] {ex.Message}", true); + ErrorNotifier.ShowError($"[{UIStrings.XIVDeck}] {ex.Message}", true); } return HttpExceptionHandler.Default(ctx, ex); diff --git a/FFXIVPlugin/UI/Windows/DebugWindow.cs b/FFXIVPlugin/UI/Windows/DebugWindow.cs index abf3c01..c60618d 100644 --- a/FFXIVPlugin/UI/Windows/DebugWindow.cs +++ b/FFXIVPlugin/UI/Windows/DebugWindow.cs @@ -1,8 +1,10 @@ -using System.Numerics; +using System.Globalization; +using System.Numerics; using System.Reflection; using Dalamud.Interface.Colors; using Dalamud.Interface.Windowing; using ImGuiNET; +using XIVDeck.FFXIVPlugin.Resources.Localization; using XIVDeck.FFXIVPlugin.Server; using XIVDeck.FFXIVPlugin.Server.Helpers; using XIVDeck.FFXIVPlugin.Server.Messages.Outbound; @@ -71,5 +73,10 @@ public override void Draw() { if (ImGui.Button("(Re)Start Server")) { XIVDeckPlugin.Instance.InitializeWebServer(); } + + ImGui.Spacing(); + if (ImGui.Button("Pseudo-localize")) { + UIStrings.Culture = new CultureInfo("qps-ploc"); + } } } \ No newline at end of file diff --git a/FFXIVPlugin/UI/Windows/Nags/ForcedUpdateNag.cs b/FFXIVPlugin/UI/Windows/Nags/ForcedUpdateNag.cs index 98093bb..4d887c9 100644 --- a/FFXIVPlugin/UI/Windows/Nags/ForcedUpdateNag.cs +++ b/FFXIVPlugin/UI/Windows/Nags/ForcedUpdateNag.cs @@ -1,6 +1,7 @@ using Dalamud.Interface; using Dalamud.Interface.Colors; using ImGuiNET; +using XIVDeck.FFXIVPlugin.Resources.Localization; using XIVDeck.FFXIVPlugin.Utils; namespace XIVDeck.FFXIVPlugin.UI.Windows.Nags; @@ -27,17 +28,16 @@ protected override void _internalDraw() { var placeholderButtonSize = ImGuiHelpers.GetButtonSize("placeholder"); ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed); - ImGui.Text("The XIVDeck Stream Deck Plugin is critically out of date and has been disabled."); + ImGui.Text(UIStrings.ForcedUpdateNag_Headline); ImGui.PopStyleColor(); ImGui.Separator(); - ImGui.Text("Please download and install the latest version of the Stream Deck plugin from the XIVDeck " + - "GitHub to continue using XIVDeck."); + ImGui.Text(UIStrings.ForcedUpdateNag_ResolutionHelp); ImGui.SetCursorPosY(windowSize.Y - placeholderButtonSize.Y); - if (ImGui.Button($"Open XIVDeck Download Page")) { + if (ImGui.Button(UIStrings.Nag_OpenGithubDownloadButton)) { PluginUI.OpenXIVDeckGitHub($"/releases/tag/v{VersionUtils.GetCurrentMajMinBuild()}"); } } diff --git a/FFXIVPlugin/UI/Windows/Nags/PortInUseNag.cs b/FFXIVPlugin/UI/Windows/Nags/PortInUseNag.cs index 20afc26..88f430b 100644 --- a/FFXIVPlugin/UI/Windows/Nags/PortInUseNag.cs +++ b/FFXIVPlugin/UI/Windows/Nags/PortInUseNag.cs @@ -1,5 +1,6 @@ using Dalamud.Interface.Colors; using ImGuiNET; +using XIVDeck.FFXIVPlugin.Resources.Localization; namespace XIVDeck.FFXIVPlugin.UI.Windows.Nags; @@ -34,7 +35,7 @@ protected override void _internalDraw() { "free one. To do this, please choose a new port number in the XIVDeck Game Plugin settings, and " + "enter that same port number in the XIVDeck Stream Deck Plugin settings."); - if (ImGui.Button("Open XIVDeck Settings")) { + if (ImGui.Button(UIStrings.Nag_OpenSettingsButton)) { XIVDeckPlugin.Instance.DrawConfigUI(); } diff --git a/FFXIVPlugin/UI/Windows/Nags/SetupNag.cs b/FFXIVPlugin/UI/Windows/Nags/SetupNag.cs index 4edf19e..3a8c928 100644 --- a/FFXIVPlugin/UI/Windows/Nags/SetupNag.cs +++ b/FFXIVPlugin/UI/Windows/Nags/SetupNag.cs @@ -1,6 +1,7 @@ using System.Numerics; using Dalamud.Interface.Colors; using ImGuiNET; +using XIVDeck.FFXIVPlugin.Resources.Localization; using XIVDeck.FFXIVPlugin.Utils; namespace XIVDeck.FFXIVPlugin.UI.Windows.Nags; @@ -24,32 +25,30 @@ public SetupNag() : base("sdPluginNotInstalled", 400, 325) { } protected override void _internalDraw() { ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudYellow); - ImGui.Text("A Stream Deck has never connected to the game."); + ImGui.Text(UIStrings.SetupNag_Headline); ImGui.PopStyleColor(); ImGui.Separator(); - ImGui.Text("If you haven't done so already, please make sure you've downloaded and installed the " + - "companion XIVDeck Stream Deck Plugin from GitHub."); + ImGui.Text(UIStrings.SetupNag_ResolutionHelp); - if (ImGui.Button("Open XIVDeck Download Page")) { + if (ImGui.Button(UIStrings.Nag_OpenGithubDownloadButton)) { PluginUI.OpenXIVDeckGitHub($"/releases/tag/v{VersionUtils.GetCurrentMajMinBuild()}"); } // spacer, but bigger ImGui.Dummy(new Vector2(0, 15)); - ImGui.Text("If the XIVDeck Stream Deck Plugin is already installed, please make sure the port is set " + - "correctly in the configuration and that you've created at least one button."); + ImGui.Text(UIStrings.SetupNag_AlreadyInstalledHelp); - ImGui.Text($"Current port: {XIVDeckPlugin.Instance.Configuration.WebSocketPort}"); + ImGui.Text(string.Format(UIStrings.SetupNag_CurrentPort, XIVDeckPlugin.Instance.Configuration.WebSocketPort)); - ImGui.Text("If you need to change the port the server is hosted on, you may do so from XIVDeck's settings."); - if (ImGui.Button("Open XIVDeck Settings")) { + ImGui.Text(UIStrings.SetupNag_PortChangeHelp); + if (ImGui.Button(UIStrings.Nag_OpenSettingsButton)) { XIVDeckPlugin.Instance.DrawConfigUI(); } ImGui.Spacing(); - ImGui.TextColored(ImGuiColors.DalamudGrey, "To dismiss this message, resolve the above problem or uninstall the XIVDeck plugin."); + ImGui.TextColored(ImGuiColors.DalamudGrey, UIStrings.SetupNag_DismissHelp); } } \ No newline at end of file diff --git a/FFXIVPlugin/UI/Windows/SettingsWindow.cs b/FFXIVPlugin/UI/Windows/SettingsWindow.cs index 440d332..11850ec 100644 --- a/FFXIVPlugin/UI/Windows/SettingsWindow.cs +++ b/FFXIVPlugin/UI/Windows/SettingsWindow.cs @@ -4,12 +4,13 @@ using Dalamud.Interface.Components; using Dalamud.Interface.Windowing; using ImGuiNET; +using XIVDeck.FFXIVPlugin.Resources.Localization; using XIVDeck.FFXIVPlugin.UI.Windows.Nags; namespace XIVDeck.FFXIVPlugin.UI.Windows; public class SettingsWindow : Window { - public const string WindowKey = "XIVDeck Game Plugin Settings"; + public static readonly string WindowKey = "###xivDeckSettingsWindow"; private readonly XIVDeckPlugin _plugin = XIVDeckPlugin.Instance; @@ -32,8 +33,10 @@ public override void OnOpen() { this._websocketPort = this._plugin.Configuration.WebSocketPort; this._safeMode = this._plugin.Configuration.SafeMode; - // experimental flags + // setting flags this._usePenumbraIPC = this._plugin.Configuration.UsePenumbraIPC; + + // experimental flags this._useMIconIcons = this._plugin.Configuration.UseMIconIcons; } @@ -43,13 +46,13 @@ public override void OnClose() { public override void Draw() { var windowSize = ImGui.GetWindowContentRegionMax(); + this.WindowName = UIStrings.SettingsWindow_Title + WindowKey; if (!this._safeMode) { ImGui.PushTextWrapPos(); ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed); - ImGui.Text("DANGER: SAFE MODE DISABLED! You may be able to send illegal commands from your " + - "Stream Deck to the game."); + ImGui.Text(UIStrings.SettingsWindow_SafeModeDisabledWarning); ImGui.PopStyleColor(); ImGui.PopTextWrapPos(); @@ -58,28 +61,25 @@ public override void Draw() { ImGui.PushItemWidth(80); - if (ImGui.InputInt("API Port", ref this._websocketPort, 0)) { + if (ImGui.InputInt(UIStrings.SettingsWindow_APIPort, ref this._websocketPort, 0)) { if (this._websocketPort < 1024) this._websocketPort = 1024; if (this._websocketPort > 59999) this._websocketPort = 59999; } ImGui.PopItemWidth(); - ImGuiComponents.HelpMarker("Default port: 37984\n\nRange: 1024-59999"); + ImGuiComponents.HelpMarker(string.Format(UIStrings.SettingsWindow_APIPort_Help, 37984, 1024, 59999)); - ImGui.TextWrapped($"Listen IP: 127.0.0.1"); + ImGui.TextWrapped(string.Format(UIStrings.SettingsWindow_ListenIP, "127.0.0.1")); ImGui.Spacing(); - ImGui.Checkbox("Use Penumbra Icons", ref this._usePenumbraIPC); - ImGuiComponents.HelpMarker("When enabled, this feature will attempt to display icons from Penumbra on " + - "the Stream Deck. Note that Penumbra must be installed for this setting to have " + - "any effect.\n\nIf disabled, original game icons will be used instead.\n\n" + - "Default: Off"); + ImGui.Checkbox(UIStrings.SettingsWindow_EnablePenumbraIPC, ref this._usePenumbraIPC); + ImGuiComponents.HelpMarker(UIStrings.SettingsWindow_EnablePenumbraIPC_Help); ImGui.Dummy(new Vector2(0, 20)); - ImGui.TextColored(ImGuiColors.DalamudYellow, "Experimental Settings"); + ImGui.TextColored(ImGuiColors.DalamudYellow, UIStrings.SettingsWindow_ExperimentalSettings); ImGui.Indent(); - ImGui.Checkbox("Use /micon Icons", ref this._useMIconIcons); + ImGui.Checkbox(UIStrings.SettingsWindow_Experiment_MIcon, ref this._useMIconIcons); ImGui.Unindent(); /* FOOTER */ @@ -88,7 +88,7 @@ public override void Draw() { ImGui.SetCursorPosY(windowSize.Y - placeholderButtonSize.Y - 2); ImGui.Separator(); - if (ImGui.Button("XIVDeck GitHub")) PluginUI.OpenXIVDeckGitHub(); + if (ImGui.Button(UIStrings.SettingsWindow_GitHubLink)) PluginUI.OpenXIVDeckGitHub(); #if DEBUG ImGui.SameLine(); @@ -97,7 +97,7 @@ public override void Draw() { } #endif - var applyText = "Apply Settings"; + var applyText = UIStrings.SettingsWindow_ApplyButton; var applyButtonSize = ImGuiHelpers.GetButtonSize(applyText); ImGui.SameLine(windowSize.X - applyButtonSize.X - 20); diff --git a/FFXIVPlugin/Utils/GameUtils.cs b/FFXIVPlugin/Utils/GameUtils.cs new file mode 100644 index 0000000..4a8545e --- /dev/null +++ b/FFXIVPlugin/Utils/GameUtils.cs @@ -0,0 +1,14 @@ +using System; + +namespace XIVDeck.FFXIVPlugin.Utils; + +public static class GameUtils { + public static bool IsCrossHotbar(int hotbarId) { + return hotbarId switch { + < 0 or > 19 => throw new ArgumentOutOfRangeException(nameof(hotbarId)), + 18 => false, // Standard pet/extra hotbar + 19 => true, // Cross pet/extra hotbar + _ => hotbarId >= 10 + }; + } +} \ No newline at end of file diff --git a/FFXIVPlugin/XIVDeck.FFXIVPlugin.csproj b/FFXIVPlugin/XIVDeck.FFXIVPlugin.csproj index 9e2064f..76f42e3 100644 --- a/FFXIVPlugin/XIVDeck.FFXIVPlugin.csproj +++ b/FFXIVPlugin/XIVDeck.FFXIVPlugin.csproj @@ -3,7 +3,7 @@ XIVDeck Game Plugin Kaz Wolfe Blacksite Technologies - 0.1.14 + 0.2.0 XIVDeck.FFXIVPlugin @@ -87,6 +87,19 @@ Always + + + + + ResXFileCodeGenerator + UIStrings.Designer.cs + + + True + True + UIStrings.resx + + diff --git a/FFXIVPlugin/XIVDeckPlugin.cs b/FFXIVPlugin/XIVDeckPlugin.cs index 8de4490..88f8e9c 100644 --- a/FFXIVPlugin/XIVDeckPlugin.cs +++ b/FFXIVPlugin/XIVDeckPlugin.cs @@ -1,10 +1,12 @@ -using Dalamud.Interface.Windowing; +using System.Globalization; +using Dalamud.Interface.Windowing; using Dalamud.Plugin; using FFXIVClientStructs; using XIVDeck.FFXIVPlugin.ActionExecutor; using XIVDeck.FFXIVPlugin.Base; using XIVDeck.FFXIVPlugin.Game; using XIVDeck.FFXIVPlugin.IPC; +using XIVDeck.FFXIVPlugin.Resources.Localization; using XIVDeck.FFXIVPlugin.Server; using XIVDeck.FFXIVPlugin.Server.Types; using XIVDeck.FFXIVPlugin.UI; @@ -17,7 +19,7 @@ namespace XIVDeck.FFXIVPlugin; public sealed class XIVDeckPlugin : IDalamudPlugin { public static XIVDeckPlugin Instance = null!; - public string Name => Constants.PluginName; + public string Name => UIStrings.XIVDeck_Title; public DalamudPluginInterface PluginInterface { get; init; } public PluginConfig Configuration { get; init; } @@ -65,6 +67,9 @@ public XIVDeckPlugin(DalamudPluginInterface pluginInterface) { this.PluginInterface.UiBuilder.Draw += this.WindowSystem.Draw; this.PluginInterface.UiBuilder.OpenConfigUi += this.DrawConfigUI; + this.PluginInterface.LanguageChanged += this.UpdateLang; + this.UpdateLang(this.PluginInterface.UiLanguage); + Injections.ClientState.Login += DalamudHooks.OnGameLogin; this.InitializeNag(); } @@ -80,6 +85,7 @@ public void Dispose() { this._ipcManager.Dispose(); Injections.ClientState.Login -= DalamudHooks.OnGameLogin; + this.PluginInterface.LanguageChanged -= this.UpdateLang; } internal void DrawConfigUI() { @@ -96,6 +102,10 @@ private void InitializeNag() { } } + private void UpdateLang(string langCode) { + UIStrings.Culture = new CultureInfo(langCode); + } + internal void InitializeWebServer() { if (this._xivDeckWebServer is {IsRunning: true}) { this._xivDeckWebServer.Dispose(); diff --git a/README.md b/README.md index 38c7036..96aabd1 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,12 @@ # XIVDeck +[![Download count](https://img.shields.io/endpoint?url=https://vz32sgcoal.execute-api.us-east-1.amazonaws.com/XIVDeck.FFXIVPlugin)](https://github.com/KazWolfe/XIVDeck) +[![Crowdin](https://badges.crowdin.net/xivdeck/localized.svg)](https://crowdin.com/project/xivdeck) +[![Latest Release](https://img.shields.io/github/v/release/KazWolfe/XIVDeck)](https://github.com/KazWolfe/XIVDeck/releases/latest) +[![Testing Release](https://img.shields.io/github/v/release/KazWolfe/XIVDeck?color=orange&include_prereleases&label=testing)](https://github.com/KazWolfe/XIVDeck/releases) + + XIVDeck is a project that attempts to bridge the gap between [Final Fantasy XIV][ffxiv] and the [Elgato Stream Deck][streamdeck]. XIVDeck uses the Dalamud plugin library to create an interactive and pleasant human interface experience. @@ -59,11 +65,10 @@ versions are also available (and may at times be the only live version), but thi ### Building the Plugin If for some reason you'd rather be on the bleeding edge, you may also manually build the plugins. -Note that support is *not* provided for self-built version of the plugin. +Note that support is *not* provided for self-built versions. The XIVDeck Game Plugin is more or less self-contained and only needs to go through your IDE's -normal build processes. Currently, the only version that's guaranteed to build (and work) is -the Debug build. Place the output files (or symlink them) to +normal build processes. Place the output files (or symlink them) to `%APPDATA%\XIVLauncher\devPlugins\XIVDeck` to get started. The XIVDeck Stream Deck Plugin can be installed by copying or symlinking the diff --git a/SDPlugin/assets/locales/de/actiontypes.json b/SDPlugin/assets/locales/de/actiontypes.json new file mode 100644 index 0000000..b251eb6 --- /dev/null +++ b/SDPlugin/assets/locales/de/actiontypes.json @@ -0,0 +1,14 @@ +{ + "Collection": "Sammlung", + "Emote": "Geste", + "ExtraCommand": "Zusatzbefehle", + "GearSet": "Ausrüstungsset", + "GeneralAction": "Allgemeine Aktionen", + "PerformanceInstrument": "Performance-Instrument", + "MainCommand": "Hauptbefehl", + "Marker": "Markierung", + "Minion": "Begleiter", + "Mount": "Reittier", + "FashionAccessory": "Modeaccessoires", + "FieldMarker": "Bodenmarkierung" +} \ No newline at end of file diff --git a/SDPlugin/assets/locales/de/common.json b/SDPlugin/assets/locales/de/common.json new file mode 100644 index 0000000..816ccd6 --- /dev/null +++ b/SDPlugin/assets/locales/de/common.json @@ -0,0 +1,20 @@ +{ + "headers": { + "action": "Aktionskonfiguration", + "plugin": "Pluginkonfiguration" + }, + "versionInfo": { + "build": "Build Version:", + "runtime": "Runtime Version:", + "game": "Spielplugin Version:" + }, + "connError": { + "headline": "Das XIVDeck Plugin wurde nicht entdeckt!", + "clickMore": "Drücken für mehr Information...", + "checkFor": "Bitte Drücken um sicherzustellen dass:", + "checkGameRunning": "Final Fantasy XIV läuft bereits", + "checkPluginInstalled": "The XIVDeck Plugin ist ordnungsgemäß installiert und konfiguriert,", + "checkSettingsCorrect": "Die folgenden Verbindungseinstellungen sind korrekt.", + "resolveSteps": "Nachdem alle oben genannten Einstellungen überprüft wurden, aktualisieren Sie dieses Einstellungsfenster, um eine erneute Verbindung zum Spiel herzustellen." + } +} \ No newline at end of file diff --git a/SDPlugin/assets/locales/de/frames.json b/SDPlugin/assets/locales/de/frames.json new file mode 100644 index 0000000..b94cc53 --- /dev/null +++ b/SDPlugin/assets/locales/de/frames.json @@ -0,0 +1,35 @@ +{ + "global": { + "port-label": "XIVDeck Port" + }, + "command": { + "command-label": "Befehl" + }, + "hotbar": { + "hotbar": "Kommandomenü", + "slot": "Feld", + "standard-pl": "Standard-Kommandomenü", + "cross-pl": "Kreuz-Kommandomenü", + "standard": "Kommandomenü {{id}}", + "cross": "Kreuz-Kommandomenü {{id}}", + "default": "Kommandomenü auswählen..." + }, + "class": { + "default": "Klasse auswählen...", + "class": "Klasse", + "unknown": "Unbekannt" + }, + "action": { + "type": "Typ", + "action": "Aktion", + "default-type": "Aktionstyp auswählen...", + "default-action": "Aktion auswählen...", + "unknown": "Unbekannt" + }, + "macro": { + "type": "Macro Typ", + "number": "Macro Nummer", + "individual": "Individuell", + "shared": "Geteilt" + } +} \ No newline at end of file diff --git a/SDPlugin/assets/locales/en/actiontypes.json b/SDPlugin/assets/locales/en/actiontypes.json new file mode 100644 index 0000000..87222bc --- /dev/null +++ b/SDPlugin/assets/locales/en/actiontypes.json @@ -0,0 +1,14 @@ +{ + "Collection": "Collection", + "Emote": "Emote", + "ExtraCommand": "Extra Command", + "GearSet": "Gear Set", + "GeneralAction": "General Action", + "PerformanceInstrument": "Performance Instrument", + "MainCommand": "Main Command", + "Marker": "Marker", + "Minion": "Minion", + "Mount": "Mount", + "FashionAccessory": "Fashion Accessory", + "FieldMarker": "Waymark" +} \ No newline at end of file diff --git a/SDPlugin/assets/locales/en/common.json b/SDPlugin/assets/locales/en/common.json new file mode 100644 index 0000000..821bab0 --- /dev/null +++ b/SDPlugin/assets/locales/en/common.json @@ -0,0 +1,20 @@ +{ + "headers": { + "action": "Action Configuration", + "plugin": "Plugin Configuration" + }, + "versionInfo": { + "build": "Build Version:", + "runtime": "Runtime Version:", + "game": "Game Plugin Version:" + }, + "connError": { + "headline": "The XIVDeck Game Plugin wasn't detected!", + "clickMore": "Click for more info...", + "checkFor": "Please check to ensure that:", + "checkGameRunning": "Final Fantasy XIV is running", + "checkPluginInstalled": "The XIVDeck Game Plugin is properly installed and configured,", + "checkSettingsCorrect": "The connection settings below are correct.", + "resolveSteps": "After verifying all of the above, refresh this settings pane to attempt to connect to the game again." + } +} \ No newline at end of file diff --git a/SDPlugin/assets/locales/en/frames.json b/SDPlugin/assets/locales/en/frames.json new file mode 100644 index 0000000..412506b --- /dev/null +++ b/SDPlugin/assets/locales/en/frames.json @@ -0,0 +1,35 @@ +{ + "global": { + "port-label": "XIVDeck Port" + }, + "command": { + "command-label": "Command" + }, + "hotbar": { + "hotbar": "Hotbar", + "slot": "Slot", + "standard-pl": "Standard Hotbars", + "cross-pl": "Cross Hotbars", + "standard": "Hotbar {{id}}", + "cross": "Cross Hotbar {{id}}", + "default": "Select hotbar..." + }, + "class": { + "default": "Select class...", + "class": "Class", + "unknown": "Unknown" + }, + "action": { + "type": "Type", + "action": "Action", + "default-type": "Select action type...", + "default-action": "Select action...", + "unknown": "Unknown" + }, + "macro": { + "type": "Macro Type", + "number": "Macro Number", + "individual": "Individual", + "shared": "Shared" + } +} \ No newline at end of file diff --git a/SDPlugin/assets/locales/fr/actiontypes.json b/SDPlugin/assets/locales/fr/actiontypes.json new file mode 100644 index 0000000..c6debeb --- /dev/null +++ b/SDPlugin/assets/locales/fr/actiontypes.json @@ -0,0 +1,14 @@ +{ + "Collection": "Objet précieux", + "Emote": "Emote", + "ExtraCommand": "Autres Commandes", + "GearSet": "Tenue", + "GeneralAction": "Action", + "PerformanceInstrument": "Instrument d'Interprétation", + "MainCommand": "Commande Générale", + "Marker": "Signe", + "Minion": "Mascotte", + "Mount": "Monture", + "FashionAccessory": "Accessoire de mode", + "FieldMarker": "Marque au sol" +} \ No newline at end of file diff --git a/SDPlugin/assets/locales/fr/common.json b/SDPlugin/assets/locales/fr/common.json new file mode 100644 index 0000000..5ecac92 --- /dev/null +++ b/SDPlugin/assets/locales/fr/common.json @@ -0,0 +1,20 @@ +{ + "headers": { + "action": "Configuration de l'Action", + "plugin": "Configuration du Plugin" + }, + "versionInfo": { + "build": "Version de build :", + "runtime": "Version du Runtime :", + "game": "Version du Plugin en Jeu :" + }, + "connError": { + "headline": "Le Plugin en Jeu de XIVDeck n'a pas été détecté !", + "clickMore": "Cliquez ici pour plus d'informations...", + "checkFor": "Veuillez vérifier que :", + "checkGameRunning": "Final Fantasy XIV est en cours d'exécution", + "checkPluginInstalled": "Le plugin de jeu de XIVDeck est correctement installé et configuré,", + "checkSettingsCorrect": "Les paramètres de connexion ci-dessous sont corrects.", + "resolveSteps": "Après avoir vérifié tout ce qui précède, rafraîchissez ce volet de paramètres pour réessayer la connexion au jeu." + } +} \ No newline at end of file diff --git a/SDPlugin/assets/locales/fr/frames.json b/SDPlugin/assets/locales/fr/frames.json new file mode 100644 index 0000000..c0cae47 --- /dev/null +++ b/SDPlugin/assets/locales/fr/frames.json @@ -0,0 +1,35 @@ +{ + "global": { + "port-label": "Port de XIVDeck" + }, + "command": { + "command-label": "Commande" + }, + "hotbar": { + "hotbar": "Barre de raccourcis", + "slot": "Emplacement", + "standard-pl": "Barres de raccourcis standards", + "cross-pl": "Croix de raccourcis", + "standard": "Barre de raccourcis {{id}}", + "cross": "Croix de raccourcis {{id}}", + "default": "Sélectionnez la barre de raccourci..." + }, + "class": { + "default": "Sélectionner la classe...", + "class": "Classe", + "unknown": "Inconnu" + }, + "action": { + "type": "Type", + "action": "Action", + "default-type": "Sélectionnez le type d'action...", + "default-action": "Sélectionnez l'action...", + "unknown": "Inconnu" + }, + "macro": { + "type": "Type de Macro", + "number": "Numéro de Macro", + "individual": "Personnage", + "shared": "Communes" + } +} \ No newline at end of file diff --git a/SDPlugin/assets/locales/index.d.ts b/SDPlugin/assets/locales/index.d.ts new file mode 100644 index 0000000..e96f9d1 --- /dev/null +++ b/SDPlugin/assets/locales/index.d.ts @@ -0,0 +1,2 @@ +// empty file for i18next loader +export default {} \ No newline at end of file diff --git a/SDPlugin/assets/locales/index.js b/SDPlugin/assets/locales/index.js new file mode 100644 index 0000000..e96f9d1 --- /dev/null +++ b/SDPlugin/assets/locales/index.js @@ -0,0 +1,2 @@ +// empty file for i18next loader +export default {} \ No newline at end of file diff --git a/SDPlugin/assets/locales/ja/actiontypes.json b/SDPlugin/assets/locales/ja/actiontypes.json new file mode 100644 index 0000000..6f5a523 --- /dev/null +++ b/SDPlugin/assets/locales/ja/actiontypes.json @@ -0,0 +1,14 @@ +{ + "Collection": "コレクション", + "Emote": "エモート", + "ExtraCommand": "エクストラコマンド", + "GearSet": "ギアセット", + "GeneralAction": "ジェネラルアクション", + "PerformanceInstrument": "楽器演奏", + "MainCommand": "メイン コマンド", + "Marker": "マーカー", + "Minion": "ミニオン", + "Mount": "マウント", + "FashionAccessory": "ファッションアクセサリー", + "FieldMarker": "フィールドマーカー" +} \ No newline at end of file diff --git a/SDPlugin/assets/locales/ja/common.json b/SDPlugin/assets/locales/ja/common.json new file mode 100644 index 0000000..79e2ea6 --- /dev/null +++ b/SDPlugin/assets/locales/ja/common.json @@ -0,0 +1,20 @@ +{ + "headers": { + "action": "アクション設定", + "plugin": "プラグイン設定" + }, + "versionInfo": { + "build": "ビルドバージョン:", + "runtime": "ランタイムバージョン:", + "game": "ゲームプラグインバージョン:" + }, + "connError": { + "headline": "XIVDeck Game Pluginは検出されませんでした。", + "clickMore": "クリックして詳細を見る", + "checkFor": "以下を確認してください:", + "checkGameRunning": "ファイナルファンタジーXIVは実行中です", + "checkPluginInstalled": "XIVDeck Game Pluginは正常にインストール、および設定されています。", + "checkSettingsCorrect": "下記の接続設定は正常です。", + "resolveSteps": "上記のすべてを確認した後、この設定ペインを更新して、再度ゲームへの接続を試みます。" + } +} \ No newline at end of file diff --git a/SDPlugin/assets/locales/ja/frames.json b/SDPlugin/assets/locales/ja/frames.json new file mode 100644 index 0000000..c42ddbc --- /dev/null +++ b/SDPlugin/assets/locales/ja/frames.json @@ -0,0 +1,35 @@ +{ + "global": { + "port-label": "XIVDeck ポート" + }, + "command": { + "command-label": "コマンド" + }, + "hotbar": { + "hotbar": "ホットバー", + "slot": "スロット", + "standard-pl": "標準ホットバー", + "cross-pl": "クロスホットバー", + "standard": "ホットバー {{id}}", + "cross": "クロスホットバー {{id}}", + "default": "ホットバーを選択..." + }, + "class": { + "default": "クラスを選択...", + "class": "クラス", + "unknown": "不明" + }, + "action": { + "type": "タイプ", + "action": "アクション", + "default-type": "アクションタイプを選択...", + "default-action": "アクションを選択...", + "unknown": "不明" + }, + "macro": { + "type": "マクロタイプ", + "number": "マクロ No.", + "individual": "個別", + "shared": "共有" + } +} \ No newline at end of file diff --git a/SDPlugin/assets/locales/pseudo/actiontypes.json b/SDPlugin/assets/locales/pseudo/actiontypes.json new file mode 100644 index 0000000..006586d --- /dev/null +++ b/SDPlugin/assets/locales/pseudo/actiontypes.json @@ -0,0 +1,14 @@ +{ + "Collection": "Çôļļéçţîôññ", + "Emote": "Éɱôţé", + "ExtraCommand": "Éẋţŕà Çôɱɱɱàñð", + "GearSet": "Ĝéàŕ Šéţ", + "GeneralAction": "Ĝéñéŕàļ Àççţîôñ", + "PerformanceInstrument": "Þéŕƒôŕɱàñççé Îñšţŕûɱééñţ", + "MainCommand": "Ṁàîñ Çôɱɱààñð", + "Marker": "Ṁàŕķéŕ", + "Minion": "Ṁîñîôñ", + "Mount": "Ṁôûñţ", + "FashionAccessory": "Ƒàšĥîôñ Àçççéššôŕý", + "FieldMarker": "Ŵàýɱàŕķ" +} \ No newline at end of file diff --git a/SDPlugin/assets/locales/pseudo/common.json b/SDPlugin/assets/locales/pseudo/common.json new file mode 100644 index 0000000..cd22718 --- /dev/null +++ b/SDPlugin/assets/locales/pseudo/common.json @@ -0,0 +1,20 @@ +{ + "headers": { + "action": "Àçţîôñ Çôññƒîĝûŕàţîôññ", + "plugin": "Þļûĝîñ Çôññƒîĝûŕàţîôññ" + }, + "versionInfo": { + "build": "Ɓûîļð Ṽéŕššîôñ∶", + "runtime": "Ŕûñţîɱé Ṽééŕšîôñ∶", + "game": "Ĝàɱé Þļûĝîîñ Ṽéŕšîôñ∶∶" + }, + "connError": { + "headline": "Ţĥé ẊÎṼÐéççķ Ĝàɱé Þļûûĝîñ ŵàšñ'ţţ ðéţéçţéð¡¡", + "clickMore": "Çļîçķ ƒôŕ ɱôŕé îñƒô····", + "checkFor": "Þļéàšé çĥééçķ ţô éñšûûŕé ţĥàţ∶", + "checkGameRunning": "Ƒîñàļ Ƒàñţţàšý ẊÎṼ îšš ŕûññîñĝ", + "checkPluginInstalled": "Ţĥé ẊÎṼÐéççķ Ĝàɱé Þļûûĝîñ îš þŕôôþéŕļý îñšţţàļļéð àñð çôñƒîĝûŕéðð،", + "checkSettingsCorrect": "Ţĥé çôññéççţîôñ šéţţîîñĝš ƀéļôŵ àŕé çôŕŕéççţ·", + "resolveSteps": "Àƒţéŕ ṽéŕîîƒýîñĝ àļļ ôƒ ţĥé àƀôôṽé، ŕéƒŕéššĥ ţĥîš šéţţţîñĝš þàñéé ţô àţţéɱþþţ ţô çôññééçţ ţô ţĥé ĝàɱé àĝàîññ·" + } +} \ No newline at end of file diff --git a/SDPlugin/assets/locales/pseudo/frames.json b/SDPlugin/assets/locales/pseudo/frames.json new file mode 100644 index 0000000..6199588 --- /dev/null +++ b/SDPlugin/assets/locales/pseudo/frames.json @@ -0,0 +1,35 @@ +{ + "global": { + "port-label": "ẊÎṼÐéçķ Þôôŕţ" + }, + "command": { + "command-label": "Çôɱɱàñð" + }, + "hotbar": { + "hotbar": "Ĥôţƀàŕ", + "slot": "Šļôţ", + "standard-pl": "Šţàñðàŕð ĤĤôţƀàŕš", + "cross-pl": "Çŕôšš Ĥôţƀƀàŕš", + "standard": "Ĥôţƀàŕ {id}", + "cross": "Çŕôšš Ĥôţƀƀàŕ {id}", + "default": "Šéļéçţ ĥôţţƀàŕ···" + }, + "class": { + "default": "Šéļéçţ çļààšš···", + "class": "Çļàšš", + "unknown": "Ûñķñôŵñ" + }, + "action": { + "type": "Ţýþé", + "action": "Àçţîôñ", + "default-type": "Šéļéçţ àçţţîôñ ţýþé····", + "default-action": "Šéļéçţ àçţţîôñ···", + "unknown": "Ûñķñôŵñ" + }, + "macro": { + "type": "Ṁàçŕô Ţýþéé", + "number": "Ṁàçŕô Ñûɱƀƀéŕ", + "individual": "Îñðîṽîðûàļļ", + "shared": "Šĥàŕéð" + } +} \ No newline at end of file diff --git a/SDPlugin/assets/propertyInspector.html b/SDPlugin/assets/propertyInspector.html index f831170..01966ea 100644 --- a/SDPlugin/assets/propertyInspector.html +++ b/SDPlugin/assets/propertyInspector.html @@ -10,18 +10,18 @@ -
Action Configuration
+
ACTION_CONFIG
-
Plugin Configuration
+
PLUGIN_CONFIG
- Build Version: {{ PLUGIN_VERSION }} • - Runtime Version: ???
- Game Plugin Version: ??? • + BUILD_VER: {{ PLUGIN_VERSION }} • + RUNTIME_VER: ???
+ GAMEPL_VER: ???ID: {{ PLUGIN_NS }}
diff --git a/SDPlugin/package-lock.json b/SDPlugin/package-lock.json index 7c68c26..a22c814 100644 --- a/SDPlugin/package-lock.json +++ b/SDPlugin/package-lock.json @@ -1,20 +1,22 @@ { "name": "@kazwolfe/xivdeck-sdplugin", - "version": "0.1.0", + "version": "0.1.12", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@kazwolfe/xivdeck-sdplugin", - "version": "0.1.0", + "version": "0.1.12", "dependencies": { "@rweich/streamdeck-ts": "^4.0.0", "jquery": "^3.6.0", + "jquery-i18next": "^1.2.1", + "react-i18next": "^11.16.9", "uuid": "^8.3.2", "websocket-as-promised": "^2.0.1" }, "devDependencies": { - "@amille/semantic-release-plugins": "^3.3.10", + "@alienfast/i18next-loader": "^1.1.4", "@types/jquery": "^3.5.14", "@types/uuid": "^8.3.4", "copy-webpack-plugin": "^10.2.4", @@ -26,15 +28,27 @@ "webpack-cli": "^4.9.2" } }, - "node_modules/@amille/semantic-release-plugins": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/@amille/semantic-release-plugins/-/semantic-release-plugins-3.3.12.tgz", - "integrity": "sha512-gP68HB+p1j8xflgUS0RQ1IftsaboUz5x12KfPCTxm4wo9+ZnQKRuKUc72KqC7jVBzQxcEepgsK8Xmnc815yJNA==", + "node_modules/@alienfast/i18next-loader": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@alienfast/i18next-loader/-/i18next-loader-1.1.4.tgz", + "integrity": "sha512-8H+pIHIPwsjr1ip4bpCHnZtmR1z/K4KPpmD/fUL+kLug/2usATVmRi3IcZogy70Olqo3eH+qoKvWf+ROJbwoUA==", "dev": true, "dependencies": { - "glob": "^7.1.6", + "glob-all": "^3.1.0", "js-yaml": "^3.13.1", - "jszip": "^3.4.0" + "loader-utils": "^1.2.3", + "lodash": "^4.17.15" + } + }, + "node_modules/@babel/runtime": { + "version": "7.17.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.9.tgz", + "integrity": "sha512-lSiBBvodq29uShpWGNbgFdKYNiFDo5/HIYsaCEY9ff4sb10x9jizo2+pRrSyF4jKZCXqgzuqBOQKbUm90gQwJg==", + "dependencies": { + "regenerator-runtime": "^0.13.4" + }, + "engines": { + "node": ">=6.9.0" } }, "node_modules/@cspotcode/source-map-consumer": { @@ -498,6 +512,15 @@ "ajv": "^8.8.2" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -546,6 +569,15 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -609,6 +641,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001299", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001299.tgz", @@ -649,6 +690,17 @@ "node": ">=6.0" } }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, "node_modules/clone-deep": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", @@ -723,12 +775,6 @@ "webpack": "^5.1.0" } }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true - }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -749,6 +795,15 @@ "node": ">= 8" } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/define-properties": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", @@ -787,6 +842,21 @@ "integrity": "sha512-UtV0xUA/dibCKKP2JMxOpDtXR74zABevuUEH4K0tvduFSIoxRVcYmQsbB51kXsFTX8MmOyWMt8tuZAlmDOqkrQ==", "dev": true }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, "node_modules/enhanced-resolve": { "version": "5.9.2", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.2.tgz", @@ -1059,6 +1129,15 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", @@ -1119,6 +1198,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/glob-all": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/glob-all/-/glob-all-3.3.0.tgz", + "integrity": "sha512-30gCh9beSb+YSAh0vsoIlBRm4bSlyMa+5nayax1EJhjwYrCohX0aDxcxvWVe3heOrJikbHgRs75Af6kPLcumew==", + "dev": true, + "dependencies": { + "glob": "^7.1.2", + "yargs": "^15.3.1" + }, + "bin": { + "glob-all": "bin/glob-all" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -1216,6 +1308,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==" + }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "dependencies": { + "void-elements": "3.1.0" + } + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -1225,6 +1330,29 @@ "node": ">=10.17.0" } }, + "node_modules/i18next": { + "version": "21.7.1", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-21.7.1.tgz", + "integrity": "sha512-uNt0BM+HV7wgWrrTE+Z4qinQFTONB21hK3I5V/LayhquPv78WchnO4hjF2PWY/OZocahHBDbzbe0/o1rp/RcWQ==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "peer": true, + "dependencies": { + "@babel/runtime": "^7.17.2" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -1246,12 +1374,6 @@ "node": ">= 4" } }, - "node_modules/immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=", - "dev": true - }, "node_modules/import-local": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", @@ -1381,6 +1503,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -1513,12 +1644,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -1576,6 +1701,17 @@ "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz", "integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==" }, + "node_modules/jquery-i18next": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/jquery-i18next/-/jquery-i18next-1.2.1.tgz", + "integrity": "sha512-UNcw3rgxoKjGEg4w23FEn2h3OlPJU7rPzsgDuXDBZktIzeiVbJohs9Cv9hj8oP8KNfBRKOoErL/OVxg2FaAR4g==" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "peer": true + }, "node_modules/js-yaml": { "version": "3.14.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", @@ -1600,16 +1736,16 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, - "node_modules/jszip": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.7.1.tgz", - "integrity": "sha512-ghL0tz1XG9ZEmRMcEN2vt7xabrDdqHHeykgARpmZ0BiIctWxM47Vt63ZO2dnp4QYt/xJVLLy5Zv1l/xRdh2byg==", + "node_modules/json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", "dev": true, "dependencies": { - "lie": "~3.3.0", - "pako": "~1.0.2", - "readable-stream": "~2.3.6", - "set-immediate-shim": "~1.0.1" + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" } }, "node_modules/kind-of": { @@ -1621,15 +1757,6 @@ "node": ">=0.10.0" } }, - "node_modules/lie": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "dev": true, - "dependencies": { - "immediate": "~3.0.5" - } - }, "node_modules/loader-runner": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz", @@ -1639,6 +1766,20 @@ "node": ">=6.11.5" } }, + "node_modules/loader-utils": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", + "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -1651,6 +1792,24 @@ "node": ">=8" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "peer": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -1739,6 +1898,12 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "dev": true + }, "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", @@ -1865,12 +2030,6 @@ "node": ">=6" } }, - "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "dev": true - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -1943,12 +2102,6 @@ "node": ">=8" } }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true - }, "node_modules/promise-controller": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/promise-controller/-/promise-controller-1.0.0.tgz", @@ -2018,26 +2171,39 @@ "safe-buffer": "^5.1.0" } }, - "node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, + "node_modules/react": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.1.0.tgz", + "integrity": "sha512-4oL8ivCz5ZEPyclFQXaNksK3adutVS8l2xzZU0cqEFrE9Sb7fC0EFK5uEk74wIreL1DERyjvsU915j1pcT2uEQ==", + "peer": true, "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/readable-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "node_modules/react-i18next": { + "version": "11.16.9", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.16.9.tgz", + "integrity": "sha512-euXxWvcEAvsY7ZVkwx9ztCq4butqtsGHEkpkuo0RMj8Ru09IF9o2KxCyN+zyv51Nr0aBh/elaTIiR6fMb8YfVg==", + "dependencies": { + "@babel/runtime": "^7.14.5", + "html-escaper": "^2.0.2", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 19.0.0", + "react": ">= 16.8.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } }, "node_modules/rechoir": { "version": "0.7.1", @@ -2051,6 +2217,20 @@ "node": ">= 0.10" } }, + "node_modules/regenerator-runtime": { + "version": "0.13.9", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", + "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -2059,6 +2239,12 @@ "node": ">=0.10.0" } }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, "node_modules/resolve": { "version": "1.21.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.21.0.tgz", @@ -2199,14 +2385,11 @@ "randombytes": "^2.1.0" } }, - "node_modules/set-immediate-shim": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", - "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true }, "node_modules/shallow-clone": { "version": "3.0.1", @@ -2327,21 +2510,20 @@ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "dev": true }, - "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "dependencies": { - "safe-buffer": "~5.1.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" } }, - "node_modules/string_decoder/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, "node_modules/string.prototype.trimend": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", @@ -2366,6 +2548,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", @@ -2639,12 +2833,6 @@ "punycode": "^2.1.0" } }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true - }, "node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -2659,6 +2847,14 @@ "integrity": "sha512-mpSYqfsFvASnSn5qMiwrr4VKfumbPyONLCOPmsR3A6pTY/r0+tSaVbgPWSAIuzbk3lCTa+FForeTiO+wBQGkjA==", "dev": true }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha1-YU9/v42AHwu18GYfWy9XhXUOTwk=", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/watchpack": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.3.1.tgz", @@ -2886,12 +3082,32 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, "node_modules/wildcard": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", "dev": true }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -2918,12 +3134,53 @@ } } }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", @@ -2935,15 +3192,24 @@ } }, "dependencies": { - "@amille/semantic-release-plugins": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/@amille/semantic-release-plugins/-/semantic-release-plugins-3.3.12.tgz", - "integrity": "sha512-gP68HB+p1j8xflgUS0RQ1IftsaboUz5x12KfPCTxm4wo9+ZnQKRuKUc72KqC7jVBzQxcEepgsK8Xmnc815yJNA==", + "@alienfast/i18next-loader": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@alienfast/i18next-loader/-/i18next-loader-1.1.4.tgz", + "integrity": "sha512-8H+pIHIPwsjr1ip4bpCHnZtmR1z/K4KPpmD/fUL+kLug/2usATVmRi3IcZogy70Olqo3eH+qoKvWf+ROJbwoUA==", "dev": true, "requires": { - "glob": "^7.1.6", + "glob-all": "^3.1.0", "js-yaml": "^3.13.1", - "jszip": "^3.4.0" + "loader-utils": "^1.2.3", + "lodash": "^4.17.15" + } + }, + "@babel/runtime": { + "version": "7.17.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.9.tgz", + "integrity": "sha512-lSiBBvodq29uShpWGNbgFdKYNiFDo5/HIYsaCEY9ff4sb10x9jizo2+pRrSyF4jKZCXqgzuqBOQKbUm90gQwJg==", + "requires": { + "regenerator-runtime": "^0.13.4" } }, "@cspotcode/source-map-consumer": { @@ -3346,6 +3612,12 @@ "fast-deep-equal": "^3.1.3" } }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -3382,6 +3654,12 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -3429,6 +3707,12 @@ "get-intrinsic": "^1.0.2" } }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, "caniuse-lite": { "version": "1.0.30001299", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001299.tgz", @@ -3456,6 +3740,17 @@ "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", "dev": true }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, "clone-deep": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", @@ -3514,12 +3809,6 @@ "serialize-javascript": "^6.0.0" } }, - "core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true - }, "create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -3537,6 +3826,12 @@ "which": "^2.0.1" } }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, "define-properties": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", @@ -3566,6 +3861,18 @@ "integrity": "sha512-UtV0xUA/dibCKKP2JMxOpDtXR74zABevuUEH4K0tvduFSIoxRVcYmQsbB51kXsFTX8MmOyWMt8tuZAlmDOqkrQ==", "dev": true }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true + }, "enhanced-resolve": { "version": "5.9.2", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.2.tgz", @@ -3778,6 +4085,12 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, "get-intrinsic": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", @@ -3817,6 +4130,16 @@ "path-is-absolute": "^1.0.0" } }, + "glob-all": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/glob-all/-/glob-all-3.3.0.tgz", + "integrity": "sha512-30gCh9beSb+YSAh0vsoIlBRm4bSlyMa+5nayax1EJhjwYrCohX0aDxcxvWVe3heOrJikbHgRs75Af6kPLcumew==", + "dev": true, + "requires": { + "glob": "^7.1.2", + "yargs": "^15.3.1" + } + }, "glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -3884,12 +4207,34 @@ "has-symbols": "^1.0.2" } }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==" + }, + "html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "requires": { + "void-elements": "3.1.0" + } + }, "human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true }, + "i18next": { + "version": "21.7.1", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-21.7.1.tgz", + "integrity": "sha512-uNt0BM+HV7wgWrrTE+Z4qinQFTONB21hK3I5V/LayhquPv78WchnO4hjF2PWY/OZocahHBDbzbe0/o1rp/RcWQ==", + "peer": true, + "requires": { + "@babel/runtime": "^7.17.2" + } + }, "iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -3905,12 +4250,6 @@ "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", "dev": true }, - "immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=", - "dev": true - }, "import-local": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", @@ -3998,6 +4337,12 @@ "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", "dev": true }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, "is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -4079,12 +4424,6 @@ "call-bind": "^1.0.2" } }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -4130,6 +4469,17 @@ "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz", "integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==" }, + "jquery-i18next": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/jquery-i18next/-/jquery-i18next-1.2.1.tgz", + "integrity": "sha512-UNcw3rgxoKjGEg4w23FEn2h3OlPJU7rPzsgDuXDBZktIzeiVbJohs9Cv9hj8oP8KNfBRKOoErL/OVxg2FaAR4g==" + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "peer": true + }, "js-yaml": { "version": "3.14.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", @@ -4151,16 +4501,13 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, - "jszip": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.7.1.tgz", - "integrity": "sha512-ghL0tz1XG9ZEmRMcEN2vt7xabrDdqHHeykgARpmZ0BiIctWxM47Vt63ZO2dnp4QYt/xJVLLy5Zv1l/xRdh2byg==", + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", "dev": true, "requires": { - "lie": "~3.3.0", - "pako": "~1.0.2", - "readable-stream": "~2.3.6", - "set-immediate-shim": "~1.0.1" + "minimist": "^1.2.0" } }, "kind-of": { @@ -4169,21 +4516,23 @@ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true }, - "lie": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "dev": true, - "requires": { - "immediate": "~3.0.5" - } - }, "loader-runner": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz", "integrity": "sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==", "dev": true }, + "loader-utils": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", + "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + } + }, "locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -4193,6 +4542,21 @@ "p-locate": "^4.1.0" } }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "peer": true, + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -4260,6 +4624,12 @@ "brace-expansion": "^1.1.7" } }, + "minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "dev": true + }, "neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", @@ -4350,12 +4720,6 @@ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true }, - "pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "dev": true - }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -4407,12 +4771,6 @@ "find-up": "^4.0.0" } }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true - }, "promise-controller": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/promise-controller/-/promise-controller-1.0.0.tgz", @@ -4453,27 +4811,23 @@ "safe-buffer": "^5.1.0" } }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, + "react": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.1.0.tgz", + "integrity": "sha512-4oL8ivCz5ZEPyclFQXaNksK3adutVS8l2xzZU0cqEFrE9Sb7fC0EFK5uEk74wIreL1DERyjvsU915j1pcT2uEQ==", + "peer": true, "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - } + "loose-envify": "^1.1.0" + } + }, + "react-i18next": { + "version": "11.16.9", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.16.9.tgz", + "integrity": "sha512-euXxWvcEAvsY7ZVkwx9ztCq4butqtsGHEkpkuo0RMj8Ru09IF9o2KxCyN+zyv51Nr0aBh/elaTIiR6fMb8YfVg==", + "requires": { + "@babel/runtime": "^7.14.5", + "html-escaper": "^2.0.2", + "html-parse-stringify": "^3.0.1" } }, "rechoir": { @@ -4485,11 +4839,28 @@ "resolve": "^1.9.0" } }, + "regenerator-runtime": { + "version": "0.13.9", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", + "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, "require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, "resolve": { "version": "1.21.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.21.0.tgz", @@ -4573,10 +4944,10 @@ "randombytes": "^2.1.0" } }, - "set-immediate-shim": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", - "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "dev": true }, "shallow-clone": { @@ -4664,21 +5035,15 @@ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "dev": true }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "requires": { - "safe-buffer": "~5.1.0" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - } + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" } }, "string.prototype.trimend": { @@ -4699,6 +5064,15 @@ "define-properties": "^1.1.3" } }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, "strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", @@ -4868,12 +5242,6 @@ "punycode": "^2.1.0" } }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true - }, "uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -4885,6 +5253,11 @@ "integrity": "sha512-mpSYqfsFvASnSn5qMiwrr4VKfumbPyONLCOPmsR3A6pTY/r0+tSaVbgPWSAIuzbk3lCTa+FForeTiO+wBQGkjA==", "dev": true }, + "void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha1-YU9/v42AHwu18GYfWy9XhXUOTwk=" + }, "watchpack": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.3.1.tgz", @@ -5041,12 +5414,29 @@ "is-symbol": "^1.0.3" } }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, "wildcard": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", "dev": true }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -5059,12 +5449,47 @@ "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", "requires": {} }, + "y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true + }, "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, "yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/SDPlugin/package.json b/SDPlugin/package.json index b0615a7..fcd1c5c 100644 --- a/SDPlugin/package.json +++ b/SDPlugin/package.json @@ -1,6 +1,6 @@ { "name": "@kazwolfe/xivdeck-sdplugin", - "version": "0.1.14", + "version": "0.2.0", "private": true, "scripts": { "build-release": "webpack --mode production", @@ -11,7 +11,8 @@ "@rweich/streamdeck-ts": "^4.0.0", "websocket-as-promised": "^2.0.1", "uuid": "^8.3.2", - "jquery": "^3.6.0" + "jquery": "^3.6.0", + "react-i18next": "^11.16.9" }, "devDependencies": { "copy-webpack-plugin": "^10.2.4", @@ -22,6 +23,7 @@ "webpack": "^5.70.0", "webpack-cli": "^4.9.2", "@types/uuid": "^8.3.4", - "@types/jquery": "^3.5.14" + "@types/jquery": "^3.5.14", + "@alienfast/i18next-loader": "^1.1.4" } } diff --git a/SDPlugin/src/i18n/i18n.ts b/SDPlugin/src/i18n/i18n.ts new file mode 100644 index 0000000..166eae2 --- /dev/null +++ b/SDPlugin/src/i18n/i18n.ts @@ -0,0 +1,12 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import * as resources from '../../assets/locales' + +i18n + .use(initReactI18next) + .init({ + resources: resources, + fallbackLng: "en", + }); + +export default i18n; \ No newline at end of file diff --git a/SDPlugin/src/i18n/react-i18next.d.ts b/SDPlugin/src/i18n/react-i18next.d.ts new file mode 100644 index 0000000..f6e18a5 --- /dev/null +++ b/SDPlugin/src/i18n/react-i18next.d.ts @@ -0,0 +1,9 @@ +import 'react-i18next'; +import * as resources from '../../assets/locales' + +declare module 'react-i18next' { + interface CustomTypeOptions { + defaultNS: 'common'; + resources: resources + } +} \ No newline at end of file diff --git a/SDPlugin/src/inspector.ts b/SDPlugin/src/inspector.ts index 59e58ea..db6aa1a 100644 --- a/SDPlugin/src/inspector.ts +++ b/SDPlugin/src/inspector.ts @@ -7,6 +7,8 @@ import {GlobalFrame} from "./inspector/frames/GlobalFrame"; import {FFXIVInitReply} from "./link/ffxivplugin/GameTypes"; import {PIUtils} from "./util/PIUtils"; +import i18n from "./i18n/i18n"; + class XIVDeckInspector { sdPluginLink: SDInspector = new Streamdeck().propertyinspector(); xivPluginLink: FFXIVPluginLink = new FFXIVPluginLink(this.sdPluginLink); @@ -14,27 +16,33 @@ class XIVDeckInspector { // state for this inspector uuid: string = ""; dispatcher: PIDispatcher = new PIDispatcher(); - globalInspector: GlobalFrame = new GlobalFrame(); + globalInspector!: GlobalFrame; constructor() { console.log(this); - - this.sdPluginLink.on('didReceiveGlobalSettings', (ev: DidReceiveGlobalSettingsEvent) => this.handleDidReceiveGlobalSettings(ev)); - this.sdPluginLink.on('didReceiveSettings', (ev: DidReceiveSettingsEvent) => this.handleDidReceiveSettings(ev)) this.sdPluginLink.on('websocketOpen', (event => { - this.dispatcher.initialize(); - this.uuid = event.uuid; this.sdPluginLink.getGlobalSettings(event.uuid); // this.sdPluginLink.getSettings(event.uuid); + // localization work + let aInfo = this.sdPluginLink.info.application as Record; + i18n.changeLanguage(aInfo.language); + console.debug(`Language set to ${aInfo.language}`) + PIUtils.localizeDomTree(); + // load version into DOM (hacky) let pInfo = this.sdPluginLink.info.plugin as Record; let rtVersion = document.getElementById('runtime-version'); if (rtVersion) { rtVersion.innerText = pInfo.version; } + + this.globalInspector = new GlobalFrame(); + this.sdPluginLink.on('didReceiveGlobalSettings', (ev: DidReceiveGlobalSettingsEvent) => this.handleDidReceiveGlobalSettings(ev)); + this.sdPluginLink.on('didReceiveSettings', (ev: DidReceiveSettingsEvent) => this.handleDidReceiveSettings(ev)); + this.dispatcher.initialize(); })); } diff --git a/SDPlugin/src/inspector/frames/ActionFrame.ts b/SDPlugin/src/inspector/frames/ActionFrame.ts index 2aeb672..3fa5e8f 100644 --- a/SDPlugin/src/inspector/frames/ActionFrame.ts +++ b/SDPlugin/src/inspector/frames/ActionFrame.ts @@ -5,6 +5,7 @@ import piInstance from "../../inspector"; import {PIUtils} from "../../util/PIUtils"; import {StringUtils} from "../../util/StringUtils"; import {FFXIVApi} from "../../link/ffxivplugin/FFXIVApi"; +import i18n from "../../i18n/i18n"; const NAME_SUBSTITUTIONS: Record = { "FieldMarker": "Waymark", @@ -19,7 +20,7 @@ export class ActionFrame extends BaseFrame { selectedType: string = "default"; selectedAction: number = -1; - selectedActionName: string = "unknown"; + selectedActionName: string = i18n.t("frames:action.unknown"); constructor() { super(); @@ -32,7 +33,7 @@ export class ActionFrame extends BaseFrame { this.actionSelector.id = "actionSelector"; this.actionSelector.onchange = this._onItemChange.bind(this); - this.typeSelector.add(PIUtils.createDefaultSelection("action type")); + this.typeSelector.add(PIUtils.createDefaultSelection(i18n.t("frames:action.default-type"))); piInstance.xivPluginLink.on("_ready", this.loadGameData.bind(this)); } @@ -46,8 +47,8 @@ export class ActionFrame extends BaseFrame { } renderHTML(): void { - this.domParent.append(PIUtils.createPILabeledElement("Type", this.typeSelector)); - this.domParent.append(PIUtils.createPILabeledElement("Action", this.actionSelector)); + this.domParent.append(PIUtils.createPILabeledElement(i18n.t("frames:action.type"), this.typeSelector)); + this.domParent.append(PIUtils.createPILabeledElement(i18n.t("frames:action.action"), this.actionSelector)); } async loadGameData(): Promise { @@ -64,7 +65,8 @@ export class ActionFrame extends BaseFrame { // their settings are lost. Hacky as hell, but... if (!onlyItem) { let genericType = PIUtils.createDefaultSelection(""); - genericType.text = StringUtils.expandCaps(NAME_SUBSTITUTIONS[this.selectedType] || this.selectedType); + let typeKey = `actiontypes:${this.selectedType}` + genericType.text = i18n.t(typeKey); this.typeSelector.add(genericType); } @@ -101,14 +103,15 @@ export class ActionFrame extends BaseFrame { private _renderTypes() { this.typeSelector.options.length = 0; - this.typeSelector.add(PIUtils.createDefaultSelection("action type")); + this.typeSelector.add(PIUtils.createDefaultSelection(i18n.t("frames:action.default-type"))); console.log(this.actionData, typeof(this.actionData)) this.actionData.forEach((_, k) => { let option = document.createElement("option"); + let typeKey = `actiontypes:${k}` option.value = k; - option.innerText = StringUtils.expandCaps(NAME_SUBSTITUTIONS[k] || k); + option.innerText = i18n.t(typeKey); this.typeSelector.add(option); }) } @@ -127,7 +130,7 @@ export class ActionFrame extends BaseFrame { let items = this.actionData.get(this.selectedType) || []; console.debug("Loaded current selectable items for this PI instance", items) - this.actionSelector.add(PIUtils.createDefaultSelection("action")); + this.actionSelector.add(PIUtils.createDefaultSelection(i18n.t("frames:action.default-action"))); items.forEach((ac) => { let parent: HTMLSelectElement | HTMLOptGroupElement = this.actionSelector; @@ -145,7 +148,7 @@ export class ActionFrame extends BaseFrame { let option = document.createElement("option"); option.value = ac.id.toString(); - option.title = StringUtils.toTitleCase(ac.name || "unknown"); + option.title = StringUtils.toTitleCase(ac.name || i18n.t("frames:action.unknown")); option.innerText = `[#${ac.id}] ${option.title}`; parent.append(option); @@ -169,7 +172,7 @@ export class ActionFrame extends BaseFrame { } this.selectedAction = parseInt(newSelection); - this.selectedActionName = this.actionSelector.selectedOptions[0].title || "unknown"; + this.selectedActionName = this.actionSelector.selectedOptions[0].title || i18n.t("frames:action.unknown"); this.setSettings({ actionId: this.selectedAction, diff --git a/SDPlugin/src/inspector/frames/ClassFrame.ts b/SDPlugin/src/inspector/frames/ClassFrame.ts index 2546b29..dbc6bde 100644 --- a/SDPlugin/src/inspector/frames/ClassFrame.ts +++ b/SDPlugin/src/inspector/frames/ClassFrame.ts @@ -4,11 +4,12 @@ import piInstance from "../../inspector"; import {StringUtils} from "../../util/StringUtils"; import {PIUtils} from "../../util/PIUtils"; import {FFXIVApi} from "../../link/ffxivplugin/FFXIVApi"; +import i18n from "../../i18n/i18n"; export class ClassFrame extends BaseFrame { classSelector: HTMLSelectElement; selected: number = -1; // prevent against race to load in visible settings - selectedName: string = "unknown"; + selectedName: string = i18n.t("frames:class.unknown"); constructor() { super(); @@ -27,7 +28,7 @@ export class ClassFrame extends BaseFrame { } renderHTML(): void { - let sdItem = PIUtils.createPILabeledElement("Class", this.classSelector); + let sdItem = PIUtils.createPILabeledElement(i18n.t("frames:class.class"), this.classSelector); this.classSelector.options.length = 0; this.classSelector.add(PIUtils.createDefaultSelection("class")); @@ -59,7 +60,7 @@ export class ClassFrame extends BaseFrame { }); this.classSelector.options.length = 0; - this.classSelector.add(PIUtils.createDefaultSelection("class")); + this.classSelector.add(PIUtils.createDefaultSelection(i18n.t("frames:class.default"))); console.log(this, groupCache); groupCache.forEach((v) => { this.classSelector.add(v); diff --git a/SDPlugin/src/inspector/frames/CommandFrame.ts b/SDPlugin/src/inspector/frames/CommandFrame.ts index 976d52d..4402dd6 100644 --- a/SDPlugin/src/inspector/frames/CommandFrame.ts +++ b/SDPlugin/src/inspector/frames/CommandFrame.ts @@ -1,6 +1,7 @@ import {BaseFrame} from "../BaseFrame"; import {CommandButtonSettings} from "../../button/buttons/CommandButton"; import {PIUtils} from "../../util/PIUtils"; +import i18n from "../../i18n/i18n"; export class CommandFrame extends BaseFrame { commandField: HTMLTextAreaElement; @@ -20,7 +21,7 @@ export class CommandFrame extends BaseFrame { } renderHTML(): void { - this.domParent.append(PIUtils.createPILabeledElement("Command", this.commandField)); + this.domParent.append(PIUtils.createPILabeledElement(i18n.t("frames:command.command-label"), this.commandField)); this.commandField.oninput = this._onChange.bind(this); } diff --git a/SDPlugin/src/inspector/frames/GlobalFrame.ts b/SDPlugin/src/inspector/frames/GlobalFrame.ts index 7fd9d5b..e555e49 100644 --- a/SDPlugin/src/inspector/frames/GlobalFrame.ts +++ b/SDPlugin/src/inspector/frames/GlobalFrame.ts @@ -2,6 +2,7 @@ import {GlobalSettings} from "../../util/GlobalSettings"; import {PIUtils} from "../../util/PIUtils"; import piInstance from "../../inspector"; +import i18n from "../../i18n/i18n"; export class GlobalFrame extends BaseFrame { domParent: HTMLElement; @@ -21,7 +22,7 @@ export class GlobalFrame extends BaseFrame { } renderHTML(): void { - this.domParent.append(PIUtils.createPILabeledElement("XIVDeck Port", this.portField)); + this.domParent.append(PIUtils.createPILabeledElement(i18n.t("frames:global.port-label"), this.portField)); this.portField.onchange = this._onPortChange.bind(this); } diff --git a/SDPlugin/src/inspector/frames/HotbarFrame.ts b/SDPlugin/src/inspector/frames/HotbarFrame.ts index 461eba5..e9d9def 100644 --- a/SDPlugin/src/inspector/frames/HotbarFrame.ts +++ b/SDPlugin/src/inspector/frames/HotbarFrame.ts @@ -1,6 +1,7 @@ import {BaseFrame} from "../BaseFrame"; import {HotbarButtonSettings} from "../../button/buttons/HotbarButton"; import {PIUtils} from "../../util/PIUtils"; +import i18n from "../../i18n/i18n"; export class HotbarFrame extends BaseFrame { // html elements @@ -38,8 +39,8 @@ export class HotbarFrame extends BaseFrame { } renderHTML(): void { - this.domParent.append(PIUtils.createPILabeledElement("Hotbar", this.hotbarSelector)); - this.domParent.append(PIUtils.createPILabeledElement("Slot", this.slotField)); + this.domParent.append(PIUtils.createPILabeledElement(i18n.t("frames:hotbar.hotbar"), this.hotbarSelector)); + this.domParent.append(PIUtils.createPILabeledElement(i18n.t("frames:hotbar.slot"), this.slotField)); this._renderHotbarSelector(); this.hotbarSelector.value = (this.hotbarId >= 0) ? this.hotbarId.toString() : "default"; @@ -51,10 +52,10 @@ export class HotbarFrame extends BaseFrame { private _renderHotbarSelector() { let standardGroup = document.createElement("optgroup"); - standardGroup.label = "Standard Hotbars"; + standardGroup.label = i18n.t("frames:hotbar.standard-pl") let crossGroup = document.createElement("optgroup"); - crossGroup.label = "Cross Hotbars"; + crossGroup.label = i18n.t("frames:hotbar.cross-pl") for (let i = 0; i <= 17; i++) { let isCrossHotbar = (i >= 10); @@ -62,7 +63,11 @@ export class HotbarFrame extends BaseFrame { let entry = document.createElement("option"); entry.value = i.toString(); - entry.innerText = `${isCrossHotbar ? "Cross ": ""} Hotbar ${humanIndex}` + if (isCrossHotbar) { + entry.innerText = i18n.t("frames:hotbar.cross", {"id": humanIndex}) + } else { + entry.innerText = i18n.t("frames:hotbar.standard", {"id": humanIndex}) + } if (isCrossHotbar) { crossGroup.append(entry); @@ -72,7 +77,7 @@ export class HotbarFrame extends BaseFrame { } console.log(standardGroup, crossGroup); - this.hotbarSelector.add(PIUtils.createDefaultSelection("hotbar")); + this.hotbarSelector.add(PIUtils.createDefaultSelection(i18n.t("frames:hotbar.default"))); this.hotbarSelector.add(standardGroup); this.hotbarSelector.add(crossGroup); } diff --git a/SDPlugin/src/inspector/frames/MacroFrame.ts b/SDPlugin/src/inspector/frames/MacroFrame.ts index c0c2d35..0a88e7e 100644 --- a/SDPlugin/src/inspector/frames/MacroFrame.ts +++ b/SDPlugin/src/inspector/frames/MacroFrame.ts @@ -1,6 +1,7 @@ import {BaseFrame} from "../BaseFrame"; import {MacroButtonSettings} from "../../button/buttons/MacroButton"; import {PIUtils} from "../../util/PIUtils"; +import i18n from "../../i18n/i18n"; export class MacroFrame extends BaseFrame { // settings @@ -18,7 +19,10 @@ export class MacroFrame extends BaseFrame { this.macroNumberField.min = "0"; this.macroNumberField.max = "99"; - this.macroRadioField = PIUtils.generateRadioSelection("Macro Type", "macroType", "Individual", "Shared"); + this.macroRadioField = PIUtils.generateRadioSelection(i18n.t("frames:macro.type"), "macroType", ...[ + {value: "indiv", name: i18n.t("frames:macro.individual")}, + {value: "shared", name: i18n.t("frames:macro.shared")} + ]); } private get isSharedMacro(): boolean { @@ -38,7 +42,7 @@ export class MacroFrame extends BaseFrame { renderHTML(): void { this.domParent.append(this.macroRadioField); - this.domParent.append(PIUtils.createPILabeledElement("Macro Number", this.macroNumberField)); + this.domParent.append(PIUtils.createPILabeledElement(i18n.t("frames:macro.number"), this.macroNumberField)); this._renderRadio() this.macroNumberField.value = this.humanMacroNumber.toString(); @@ -51,9 +55,9 @@ export class MacroFrame extends BaseFrame { let choice: HTMLInputElement | null; if (this.isSharedMacro) { - choice = document.querySelector(`input[name="macroType"][value="Shared"]`); + choice = document.querySelector(`input[name="macroType"][value="shared"]`); } else { - choice = document.querySelector(`input[name="macroType"][value="Individual"]`); + choice = document.querySelector(`input[name="macroType"][value="indiv"]`); } if (choice === null) { @@ -78,7 +82,7 @@ export class MacroFrame extends BaseFrame { this.macroNumberField.setAttribute("style", ""); } - let isShared = (typeSelector.value === "Shared") + let isShared = (typeSelector.value === "shared") this.macroId = parseInt(this.macroNumberField.value) + (isShared ? 100 : 0) diff --git a/SDPlugin/src/link/ffxivplugin/FFXIVApi.ts b/SDPlugin/src/link/ffxivplugin/FFXIVApi.ts index 45642eb..fa0bddb 100644 --- a/SDPlugin/src/link/ffxivplugin/FFXIVApi.ts +++ b/SDPlugin/src/link/ffxivplugin/FFXIVApi.ts @@ -9,7 +9,7 @@ export class FFXIVApi { return FFXIVPluginLink.instance.baseUrl; } - private static async _requestWrapper(url: string, method: HTTPVerb = "GET", body: unknown = null) : Promise { + private static async _requestWrapper(url: string, method: HTTPVerb = "GET", queryParams?: Record, body?: unknown) : Promise { if (this.getBaseUrl() == null || !FFXIVPluginLink.instance.isReady() || FFXIVPluginLink.instance.apiKey == "") { throw new Error("XIV API not initialized yet! Requests should not be getting made...") } @@ -18,11 +18,17 @@ export class FFXIVApi { "Authorization": `Bearer ${FFXIVPluginLink.instance.apiKey}` } + let urlObject = new URL(url); + + if (queryParams != null) { + urlObject.search = new URLSearchParams(queryParams).toString(); + } + let response: Response if (method == "GET" || method == "HEAD" ) { - response = await fetch(url, {method: method, headers: headers}) + response = await fetch(urlObject.toString(), {method: method, headers: headers}) } else { - response = await fetch(url, {method: method, body: JSON.stringify(body), headers: headers}) + response = await fetch(urlObject.toString(), {method: method, body: JSON.stringify(body), headers: headers, }) } if (!response.ok) { @@ -38,7 +44,7 @@ export class FFXIVApi { } public static async getIcon(iconId: number, hq: boolean = false): Promise { - let response = await this._requestWrapper(this.getBaseUrl() + `/icon/${iconId}?hq=${hq}`); + let response = await this._requestWrapper(this.getBaseUrl() + `/icon/${iconId}`, "GET", {"hq": `${hq}`}); let blob = await response.blob(); return new Promise( callback =>{ @@ -49,7 +55,7 @@ export class FFXIVApi { } public static async runTextCommand(command: string) : Promise { - await FFXIVApi._requestWrapper(this.getBaseUrl() + `/command`, "POST", { + await FFXIVApi._requestWrapper(this.getBaseUrl() + `/command`, "POST", undefined, { "command": command }); } diff --git a/SDPlugin/src/util/PIUtils.ts b/SDPlugin/src/util/PIUtils.ts index cf882d7..894d334 100644 --- a/SDPlugin/src/util/PIUtils.ts +++ b/SDPlugin/src/util/PIUtils.ts @@ -1,17 +1,19 @@ -export class PIUtils { +import i18n from "../i18n/i18n"; + +export class PIUtils { static generateConnectionErrorDom(): HTMLElement { let element: HTMLElement = document.createElement("details") element.className = "message caution" element.innerHTML = ` - The XIVDeck Game Plugin wasn't detected! Click for more info... -

Please check to ensure that:

+ ${i18n.t("common:connError.headline")} ${i18n.t("common:connError.clickMore")} +

${i18n.t("common:connError.checkFor")}

    -
  • Final Fantasy XIV is running,
  • -
  • The XIVDeck Game Plugin is properly installed and configured,
  • -
  • The connection settings below are correct.
  • +
  • ${i18n.t("common:connError.checkGameRunning")}
  • +
  • ${i18n.t("common:connError.checkPluginInstalled")}
  • +
  • ${i18n.t("common:connError.checkSettingsCorrect")}
-

After verifying all of the above, refresh this settings pane to attempt to connect to the game again.

+

${i18n.t("common:connError.resolveSteps")}

` return element; @@ -33,13 +35,13 @@ return item; } - static createDefaultSelection(type: string = "item"): HTMLOptionElement { + static createDefaultSelection(innerText: string = "item"): HTMLOptionElement { let el = document.createElement("option"); el.value = "default"; el.disabled = true; el.selected = true; - el.innerText = `Select ${type}...`; + el.innerText = innerText; return el; } @@ -60,11 +62,11 @@ return true; } - static generateRadioSelection(title: string, id: string, ...choices: string[]): HTMLElement { + static generateRadioSelection(title: string, id: string, ...choices: RadioSelection[]): HTMLElement { let radioInner = document.createElement("div"); radioInner.classList.add("sdpi-item-value"); - choices.forEach((name, index) => { + choices.forEach((rs, index) => { let choiceSpan = document.createElement("span"); choiceSpan.classList.add('sdpi-item-child'); @@ -72,12 +74,12 @@ choiceInput.id = `r_${id}_choice${index}`; choiceInput.type = "radio"; choiceInput.name = id; - choiceInput.value = name; + choiceInput.value = rs.value; let choiceLabel = document.createElement("label"); choiceLabel.setAttribute("for", `r_${id}_choice${index}`); choiceLabel.classList.add("sdpi-item-label"); - choiceLabel.innerHTML = ` ${name}` + choiceLabel.innerHTML = ` ${rs.name}` choiceSpan.append(choiceInput); choiceSpan.append(choiceLabel); @@ -91,4 +93,18 @@ return labeledElement; } + + static localizeDomTree() { + document.querySelectorAll("*").forEach((node) => { + let i18nKey = node.getAttribute("data-i18n"); + if (!i18nKey) return; + + node.innerHTML = i18n.t(i18nKey); + }) + } +} + +export interface RadioSelection { + value: string, + name: string } \ No newline at end of file diff --git a/SDPlugin/tsconfig.json b/SDPlugin/tsconfig.json index 2ffeaf1..807eb50 100644 --- a/SDPlugin/tsconfig.json +++ b/SDPlugin/tsconfig.json @@ -20,6 +20,5 @@ "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, - } } diff --git a/SDPlugin/webpack.config.ts b/SDPlugin/webpack.config.ts index 9d868b5..67a0f09 100644 --- a/SDPlugin/webpack.config.ts +++ b/SDPlugin/webpack.config.ts @@ -75,6 +75,14 @@ const config = (environment: unknown, options: { mode: string; env: unknown }): }, ], }, + { + test: /locales/, + loader: "@alienfast/i18next-loader", + options: { + basenameAsNamespace: true, + debug: true, + } + }, ], }, resolve: { diff --git a/crowdin.yml b/crowdin.yml new file mode 100644 index 0000000..b159bc2 --- /dev/null +++ b/crowdin.yml @@ -0,0 +1,11 @@ +project_id_env: CROWDIN_PROJECT_ID +api_token_env: CROWDIN_PERSONAL_TOKEN +preserve_hierarchy: true +files: + - source: /FFXIVPlugin/Resources/Localization/*.resx + ignore: + - /FFXIVPlugin/Resources/Localization/UIStrings.*.resx + translation: /%original_path%/%file_name%.%two_letters_code%.%file_extension% + - source: /SDPlugin/assets/locales/en/*.json + translation: /SDPlugin/assets/locales/%two_letters_code%/%original_file_name% + type: i18next_json