Browse Source

Merge pull request #499 from PixiEditor/localization

Localization
Krzysztof Krysiński 2 years ago
parent
commit
9e6d3cb718
100 changed files with 2393 additions and 373 deletions
  1. 11 2
      src/PixiEditor.ChangeableDocument/Enums/SelectionMode.cs
  2. 4 1
      src/PixiEditor/App.xaml.cs
  3. 2 0
      src/PixiEditor/Data/Localization/Languages/ar.json
  4. 2 0
      src/PixiEditor/Data/Localization/Languages/cs.json
  5. 2 0
      src/PixiEditor/Data/Localization/Languages/de.json
  6. 600 0
      src/PixiEditor/Data/Localization/Languages/en.json
  7. 2 0
      src/PixiEditor/Data/Localization/Languages/es.json
  8. 504 0
      src/PixiEditor/Data/Localization/Languages/pl.json
  9. 2 0
      src/PixiEditor/Data/Localization/Languages/ru.json
  10. 2 0
      src/PixiEditor/Data/Localization/Languages/uk.json
  11. 54 0
      src/PixiEditor/Data/Localization/LocalizationData.json
  12. 54 0
      src/PixiEditor/Data/Localization/LocalizationDataSchema.json
  13. 2 1
      src/PixiEditor/Helpers/Converters/BlendModeToStringConverter.cs
  14. 14 3
      src/PixiEditor/Helpers/Converters/BoolToValueConverter.cs
  15. 3 2
      src/PixiEditor/Helpers/Converters/EnumToStringConverter.cs
  16. 13 14
      src/PixiEditor/Helpers/Converters/KeyToStringConverter.cs
  17. 17 0
      src/PixiEditor/Helpers/Converters/LangConverter.cs
  18. 24 0
      src/PixiEditor/Helpers/Converters/SubtractConverter.cs
  19. 36 0
      src/PixiEditor/Helpers/EnumExtension.cs
  20. 20 19
      src/PixiEditor/Helpers/Extensions/BlendModeEx.cs
  21. 3 1
      src/PixiEditor/Helpers/Extensions/ServiceCollectionHelpers.cs
  22. 43 13
      src/PixiEditor/Helpers/InputKeyHelpers.cs
  23. 45 0
      src/PixiEditor/Helpers/LocalizationExtension.cs
  24. 39 0
      src/PixiEditor/Helpers/RegistryHelpers.cs
  25. 24 1
      src/PixiEditor/Helpers/Win32.cs
  26. BIN
      src/PixiEditor/Images/LanguageFlags/ar.png
  27. BIN
      src/PixiEditor/Images/LanguageFlags/cs.png
  28. BIN
      src/PixiEditor/Images/LanguageFlags/de.png
  29. BIN
      src/PixiEditor/Images/LanguageFlags/en.png
  30. BIN
      src/PixiEditor/Images/LanguageFlags/es.png
  31. BIN
      src/PixiEditor/Images/LanguageFlags/pl.png
  32. BIN
      src/PixiEditor/Images/LanguageFlags/ru.png
  33. BIN
      src/PixiEditor/Images/LanguageFlags/uk.png
  34. 22 0
      src/PixiEditor/Localization/ILocalizationProvider.cs
  35. 37 0
      src/PixiEditor/Localization/Language.cs
  36. 18 0
      src/PixiEditor/Localization/LanguageData.cs
  37. 9 0
      src/PixiEditor/Localization/LocalizationData.cs
  38. 114 0
      src/PixiEditor/Localization/LocalizationProvider.cs
  39. 112 0
      src/PixiEditor/Localization/LocalizedString.cs
  40. 4 4
      src/PixiEditor/Models/Commands/Attributes/Commands/BasicAttribute.cs
  41. 3 2
      src/PixiEditor/Models/Commands/Attributes/Commands/CommandAttribute.cs
  42. 4 2
      src/PixiEditor/Models/Commands/Attributes/Commands/FilterAttribute.cs
  43. 4 2
      src/PixiEditor/Models/Commands/Attributes/Commands/GroupAttribute.cs
  44. 15 12
      src/PixiEditor/Models/Commands/CommandController.cs
  45. 19 6
      src/PixiEditor/Models/Commands/CommandGroup.cs
  46. 18 3
      src/PixiEditor/Models/Commands/Commands/Command.cs
  47. 5 4
      src/PixiEditor/Models/Commands/XAML/ShortcutBinding.cs
  48. 1 1
      src/PixiEditor/Models/Controllers/ClipboardController.cs
  49. 11 7
      src/PixiEditor/Models/DataHolders/KeyCombination.cs
  50. 3 2
      src/PixiEditor/Models/Dialogs/ConfirmationDialog.cs
  51. 3 2
      src/PixiEditor/Models/Dialogs/NoticeDialog.cs
  52. 3 2
      src/PixiEditor/Models/Dialogs/OptionDialog.cs
  53. 2 1
      src/PixiEditor/Models/Dialogs/OptionsDialog.cs
  54. 4 3
      src/PixiEditor/Models/DocumentModels/DocumentStructureHelper.cs
  55. 3 3
      src/PixiEditor/Models/Enums/DocumentScope.cs
  56. 24 0
      src/PixiEditor/Models/Enums/LocalizationKeyShowMode.cs
  57. 4 1
      src/PixiEditor/Models/Enums/SelectionShape.cs
  58. 1 1
      src/PixiEditor/Models/ExternalServices/LospecPaletteFetcher.cs
  59. 22 0
      src/PixiEditor/PixiEditor.csproj
  60. 1 1
      src/PixiEditor/Styles/DarkCheckboxStyle.xaml
  61. 1 0
      src/PixiEditor/Styles/LabelStyles.xaml
  62. 3 2
      src/PixiEditor/Styles/PixiListBoxItemStyle.xaml
  63. 8 2
      src/PixiEditor/Styles/ThemeStyle.xaml
  64. 58 13
      src/PixiEditor/ViewModels/SettingsWindowViewModel.cs
  65. 24 24
      src/PixiEditor/ViewModels/SubViewModels/Document/DocumentManagerViewModel.cs
  66. 2 1
      src/PixiEditor/ViewModels/SubViewModels/Document/DocumentViewModel.cs
  67. 12 12
      src/PixiEditor/ViewModels/SubViewModels/Main/ClipboardViewModel.cs
  68. 20 18
      src/PixiEditor/ViewModels/SubViewModels/Main/ColorsViewModel.cs
  69. 68 19
      src/PixiEditor/ViewModels/SubViewModels/Main/DebugViewModel.cs
  70. 5 3
      src/PixiEditor/ViewModels/SubViewModels/Main/DiscordViewModel.cs
  71. 16 15
      src/PixiEditor/ViewModels/SubViewModels/Main/FileViewModel.cs
  72. 24 23
      src/PixiEditor/ViewModels/SubViewModels/Main/LayersViewModel.cs
  73. 6 6
      src/PixiEditor/ViewModels/SubViewModels/Main/MiscViewModel.cs
  74. 21 15
      src/PixiEditor/ViewModels/SubViewModels/Main/RegistryViewModel.cs
  75. 2 2
      src/PixiEditor/ViewModels/SubViewModels/Main/SearchViewModel.cs
  76. 14 14
      src/PixiEditor/ViewModels/SubViewModels/Main/SelectionViewModel.cs
  77. 2 2
      src/PixiEditor/ViewModels/SubViewModels/Main/StylusViewModel.cs
  78. 4 4
      src/PixiEditor/ViewModels/SubViewModels/Main/ToolsViewModel.cs
  79. 3 3
      src/PixiEditor/ViewModels/SubViewModels/Main/UndoViewModel.cs
  80. 9 8
      src/PixiEditor/ViewModels/SubViewModels/Main/UpdateViewModel.cs
  81. 3 3
      src/PixiEditor/ViewModels/SubViewModels/Main/ViewOptionsViewModel.cs
  82. 13 13
      src/PixiEditor/ViewModels/SubViewModels/Main/WindowViewModel.cs
  83. 1 0
      src/PixiEditor/ViewModels/SubViewModels/Tools/ToolSettings/Settings/BoolSetting.cs
  84. 1 0
      src/PixiEditor/ViewModels/SubViewModels/Tools/ToolSettings/Settings/ColorSetting.cs
  85. 4 1
      src/PixiEditor/ViewModels/SubViewModels/Tools/ToolSettings/Settings/EnumSetting.cs
  86. 1 0
      src/PixiEditor/ViewModels/SubViewModels/Tools/ToolSettings/Settings/FloatSetting.cs
  87. 2 1
      src/PixiEditor/ViewModels/SubViewModels/Tools/ToolSettings/Settings/Setting.cs
  88. 0 1
      src/PixiEditor/ViewModels/SubViewModels/Tools/ToolSettings/Settings/SizeSetting.cs
  89. 2 2
      src/PixiEditor/ViewModels/SubViewModels/Tools/ToolSettings/Toolbars/BasicShapeToolbar.cs
  90. 1 1
      src/PixiEditor/ViewModels/SubViewModels/Tools/ToolSettings/Toolbars/BasicToolbar.cs
  91. 15 15
      src/PixiEditor/ViewModels/SubViewModels/Tools/ToolSettings/Toolbars/SettingAttributes.cs
  92. 3 3
      src/PixiEditor/ViewModels/SubViewModels/Tools/ToolSettings/Toolbars/ToolbarFactory.cs
  93. 16 4
      src/PixiEditor/ViewModels/SubViewModels/Tools/ToolViewModel.cs
  94. 7 5
      src/PixiEditor/ViewModels/SubViewModels/Tools/Tools/BrightnessToolViewModel.cs
  95. 7 5
      src/PixiEditor/ViewModels/SubViewModels/Tools/Tools/ColorPickerToolViewModel.cs
  96. 5 3
      src/PixiEditor/ViewModels/SubViewModels/Tools/Tools/EllipseToolViewModel.cs
  97. 4 2
      src/PixiEditor/ViewModels/SubViewModels/Tools/Tools/EraserToolViewModel.cs
  98. 5 3
      src/PixiEditor/ViewModels/SubViewModels/Tools/Tools/FloodFillToolViewModel.cs
  99. 8 6
      src/PixiEditor/ViewModels/SubViewModels/Tools/Tools/LassoToolViewModel.cs
  100. 5 3
      src/PixiEditor/ViewModels/SubViewModels/Tools/Tools/LineToolViewModel.cs

+ 11 - 2
src/PixiEditor.ChangeableDocument/Enums/SelectionMode.cs

@@ -1,5 +1,14 @@
-namespace PixiEditor.ChangeableDocument.Enums;
+using System.ComponentModel;
+
+namespace PixiEditor.ChangeableDocument.Enums;
 public enum SelectionMode
 {
-    New, Add, Subtract, Intersect
+    [Description("NEW")]
+    New,
+    [Description("ADD")]
+    Add,
+    [Description("SUBTRACT")]
+    Subtract,
+    [Description("INTERSECT")]
+    Intersect
 }

+ 4 - 1
src/PixiEditor/App.xaml.cs

@@ -1,6 +1,7 @@
 using System.IO;
 using System.Text.RegularExpressions;
 using System.Windows;
+using PixiEditor.Localization;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Dialogs;
@@ -110,7 +111,9 @@ internal partial class App : Application
 
         if (vm.DocumentManagerSubViewModel.Documents.Any(x => !x.AllChangesSaved))
         {
-            ConfirmationType confirmation = ConfirmationDialog.Show($"{e.ReasonSessionEnding} with unsaved data. Are you sure?", $"{e.ReasonSessionEnding}");
+            ConfirmationType confirmation = ConfirmationDialog.Show(
+                new LocalizedString("SESSION_UNSAVED_DATA", e.ReasonSessionEnding),
+                $"{e.ReasonSessionEnding}");
             e.Cancel = confirmation != ConfirmationType.Yes;
         }
     }

+ 2 - 0
src/PixiEditor/Data/Localization/Languages/ar.json

@@ -0,0 +1,2 @@
+{
+}

+ 2 - 0
src/PixiEditor/Data/Localization/Languages/cs.json

@@ -0,0 +1,2 @@
+{
+}

+ 2 - 0
src/PixiEditor/Data/Localization/Languages/de.json

@@ -0,0 +1,2 @@
+{
+}

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

@@ -0,0 +1,600 @@
+{
+  "FILE": "File",
+  "RECENT": "Recent",
+  "RECENT_FILES": "Recent Files",
+  "OPEN": "Open",
+  "NEW_FILE": "New",
+  "SAVE_PIXI": "Save (.pixi)",
+  "SAVE_AS_PIXI": "Save as... (.pixi)",
+  "EXPORT_IMG": "Export (.png, .jpg, etc.)",
+  "EDIT": "Edit",
+  "EXIT": "Exit",
+  "RECENT_EMPTY_TEXT": "So much empty space",
+
+  "YES": "Yes",
+  "NO": "No",
+  "CANCEL": "Cancel",
+  "UNNAMED": "Unnamed",
+  
+  "LANGUAGE": "Language",
+  "GENERAL": "General",
+
+  "PERCENTAGE": "Percentage",
+  "ABSOLUTE": "Absolute",
+  "PRESERVE_ASPECT_RATIO": "Preserve aspect ratio",
+  "ANCHOR_POINT": "Anchor point",
+
+  "RESIZE_IMAGE": "Resize image",
+  "RESIZE": "Resize",
+
+  "DISCORD": "Discord",
+
+  "KEY_BINDINGS": "Key Bindings",
+
+  "MISC": "Misc",
+  "DOCUMENTATION": "Documentation",
+  "WEBSITE": "Website",
+  "OPEN_WEBSITE": "Open website",
+  "REPOSITORY": "Repository",
+  "OPEN_REPOSITORY": "Open repository",
+  "LICENSE": "License",
+  "OPEN_LICENSE": "Open license",
+  "THIRD_PARTY_LICENSES": "Third party licenses",
+  "OPEN_THIRD_PARTY_LICENSES": "Open third party licenses",
+
+  "SHOW_STARTUP_WINDOW": "Show Startup Window",
+  "SHOW_IMAGE_PREVIEW_TASKBAR": "Show image preview in taskbar",
+  "RECENT_FILE_LENGTH": "Recent file list length",
+  "RECENT_FILE_LENGTH_TOOLTIP": "How many documents are shown under File > Recent. Default: 8",
+  "DEFAULT_NEW_SIZE": "Default new file size",
+  "WIDTH": "Width",
+  "HEIGHT": "Height",
+  "TOOLS": "Tools",
+  "APPLY_TRANSFORM": "Apply transform",
+  "INCREASE_TOOL_SIZE": "Increase tool size",
+  "DECREASE_TOOL_SIZE": "Decrease tool size",
+  "ENABLE_SHARED_TOOLBAR": "Enable shared toolbar",
+  "AUTOMATIC_UPDATES": "Automatic Updates",
+  "CHECK_FOR_UPDATES": "Check updates on startup",
+  "UPDATE_STREAM": "Update stream",
+  "TO_INSTALL_UPDATE": "to install update {0}",
+  "UPDATE_CHANNEL_HELP_TOOLTIP": "Update channels can only be changed in standalone version (downloaded from https://pixieditor.net).\nSteam and Microsoft Store versions handle updates separately.",
+  "DOWNLOADING_UPDATE": "Downloading update...",
+  "UPDATE_READY": "Update is ready to be installed. Do you want to install it now?",
+  "NEW_UPDATE": "New update",
+  "COULD_NOT_UPDATE_WITHOUT_ADMIN": "Couldn't update without admin privileges. Please run PixiEditor as administrator.",
+  "INSUFFICIENT_PERMISSIONS": "Insufficient permissions",
+  "UPDATE_CHECK_FAILED": "Update check failed",
+  "COULD_NOT_CHECK_FOR_UPDATES": "Could not check if there is an update available.",
+  "VERSION": "Version {0}",
+
+  "DEBUG": "Debug",
+  "OPEN_COMMAND_DEBUG_WINDOW": "Open command debug window",
+  "ENABLE_DEBUG_MODE": "Enable Debug mode",
+  "OPEN_CRASH_REPORTS_DIR": "Open crash reports directory",
+  "OPEN_TEMP_DIR": "Open temp directory",
+  "OPEN_LOCAL_APPDATA_DIR": "Open Local AppData directory",
+  "OPEN_ROAMING_APPDATA_DIR": "Open Roaming AppData directory",
+  "OPEN_INSTALLATION_DIR": "Open installation directory",
+  "DUMP_ALL_COMMANDS": "Dump all commands",
+  "DUMP_ALL_COMMANDS_DESCRIPTIVE": "Dump all commands to a text file",
+  "CRASH": "Crash",
+  "CRASH_APP": "Crash application",
+  "DELETE": "Delete",
+  "USER_PREFS": "User preferences (Roaming)",
+  "DELETE_USR_PREFS": "Delete user preferences (Roaming AppData)",
+  "SHORTCUT_FILE": "Shortcut file (Roaming)",
+  "DELETE_SHORTCUT_FILE": "Delete shortcut file (Roaming AppData)",
+  "EDITOR_DATA": "Editor data (Local)",
+  "DELETE_EDITOR_DATA": "Delete editor data (Local AppData)",
+
+  "GENERATE_KEY_BINDINGS_TEMPLATE": "Generate key bindings template",
+  "GENERATE_KEY_BINDINGS_TEMPLATE_DESCRIPTIVE": "Generate key bindings json template",
+  "VALIDATE_SHORTCUT_MAP": "Validate shortcut map",
+  "VALIDATE_SHORTCUT_MAP_DESCRIPTIVE": "Validates shortcut map",
+  "VALIDATION_KEYS_NOTICE_DIALOG": "Empty keys: {0}\nUnknown Commands: {1}",
+  "RESULT": "Result",
+  "CLEAR_RECENT_DOCUMENTS": "Clear recent documents",
+  "CLEAR_RECENTLY_OPENED_DOCUMENTS": "Clear recently opened documents",
+  "OPEN_CMD_DEBUG_WINDOW": "Open command debug window",
+
+  "MOVE_VIEWPORT_TOOLTIP": "Moves viewport. ({0})",
+  "MOVE_VIEWPORT_ACTION_DISPLAY": "Click and move to pan the viewport",
+  "MOVE_TOOL_TOOLTIP": "Moves selected pixels ({0}). Hold Ctrl to move all layers.",
+  "MOVE_TOOL_ACTION_DISPLAY": "Hold mouse to move selected pixels. Hold Ctrl to move all layers.",
+  "MOVE_TOOL_ACTION_DISPLAY_TRANSFORMING": "Click and hold mouse to move pixels in selected layers.",
+  "MOVE_TOOL_ACTION_DISPLAY_CTRL": "Hold mouse to move all layers.",
+  "PEN_TOOL_TOOLTIP": "Pen. ({0})",
+  "PEN_TOOL_ACTION_DISPLAY": "Click and move to draw.",
+  "PIXEL_PERFECT_SETTING": "Pixel perfect",
+  "RECTANGLE_TOOL_TOOLTIP": "Draws rectangle on canvas ({0}). Hold Shift to draw a square.",
+  "RECTANGLE_TOOL_ACTION_DISPLAY_DEFAULT": "Click and move to draw a rectangle. Hold Shift to draw a square.",
+  "RECTANGLE_TOOL_ACTION_DISPLAY_SHIFT": "Click and move to draw a square.",
+  "KEEP_ORIGINAL_IMAGE_SETTING": "Keep original image",
+  "ROTATE_VIEWPORT_TOOLTIP": "Rotates viewport. ({0})",
+  "ROTATE_VIEWPORT_ACTION_DISPLAY": "Click and move to rotate the viewport",
+  "SELECT_TOOL_TOOLTIP": "Selects area. ({0})",
+  "SELECT_TOOL_ACTION_DISPLAY_DEFAULT": "Click and move to select an area. Hold Shift to add to existing selection. Hold Ctrl to subtract from it.",
+  "SELECT_TOOL_ACTION_DISPLAY_SHIFT": "Click and move to add to the current selection.",
+  "SELECT_TOOL_ACTION_DISPLAY_CTRL": "Click and move to subtract from the current selection.",
+  "ZOOM_TOOL_TOOLTIP": "Zooms viewport ({0}). Click to zoom in, hold alt and click to zoom out.",
+  "ZOOM_TOOL_ACTION_DISPLAY_DEFAULT": "Click and move to zoom. Click to zoom in, hold ctrl and click to zoom out.",
+  "ZOOM_TOOL_ACTION_DISPLAY_CTRL": "Click and move to zoom. Click to zoom out, release ctrl and click to zoom in.",
+  "BRIGHTNESS_TOOL_TOOLTIP": "Makes pixels brighter or darker ({0}). Hold Ctrl to make pixels darker.",
+  "BRIGHTNESS_TOOL_ACTION_DISPLAY_DEFAULT": "Draw on pixels to make them brighter. Hold Ctrl to darken.",
+  "BRIGHTNESS_TOOL_ACTION_DISPLAY_CTRL": "Draw on pixels to make them darker. Release Ctrl to brighten.",
+  "COLOR_PICKER_TOOLTIP": "Picks the primary color from the canvas. ({0})",
+  "COLOR_PICKER_ACTION_DISPLAY_DEFAULT": "Click to pick colors. Hold Ctrl to hide the canvas. Hold Shift to hide the reference layer",
+  "COLOR_PICKER_ACTION_DISPLAY_CTRL": "Click to pick colors from the reference layer.",
+  "COLOR_PICKER_ACTION_DISPLAY_SHIFT": "Click to pick colors from the canvas.",
+  "ELLIPSE_TOOL_TOOLTIP": "Draws an ellipse on canvas ({0}). Hold Shift to draw a circle.",
+  "ELLIPSE_TOOL_ACTION_DISPLAY_DEFAULT": "Click and move mouse to draw an ellipse. Hold Shift to draw a circle.",
+  "ELLIPSE_TOOL_ACTION_DISPLAY_SHIFT": "Click and move mouse to draw a circle.",
+  "ERASER_TOOL_TOOLTIP": "Erasers color from pixel. ({0})",
+  "ERASER_TOOL_ACTION_DISPLAY": "Click and move to erase.",
+  "FLOOD_FILL_TOOL_TOOLTIP": "Fills area with color. ({0})",
+  "FLOOD_FILL_TOOL_ACTION_DISPLAY_DEFAULT": "Press on an area to fill it. Hold down Ctrl to consider all layers.",
+  "FLOOD_FILL_TOOL_ACTION_DISPLAY_CTRL": "Press on an area to fill it. Release Ctrl to only consider the current layers.",
+  "LASSO_TOOL_TOOLTIP": "Lasso. ({0})",
+  "LASSO_TOOL_ACTION_DISPLAY_DEFAULT": "Click and move to select pixels inside of the lasso. Hold Shift to add to existing selection. Hold Ctrl to subtract from it.",
+  "LASSO_TOOL_ACTION_DISPLAY_SHIFT": "Click and move to add pixels inside of the lasso to the selection.",
+  "LASSO_TOOL_ACTION_DISPLAY_CTRL": "Click and move to subtract pixels inside of the lasso from the selection.",
+  "LINE_TOOL_TOOLTIP": "Draws line on canvas ({0}). Hold Shift to enable snapping.",
+  "LINE_TOOL_ACTION_DISPLAY_DEFAULT": "Click and move to draw a line. Hold Shift to enable snapping.",
+  "LINE_TOOL_ACTION_DISPLAY_SHIFT": "Click and move mouse to draw a line with snapping enabled.",
+  "MAGIC_WAND_TOOL_TOOLTIP": "Magic Wand ({0}). Flood's the selection",
+  "MAGIC_WAND_ACTION_DISPLAY": "Click to flood the selection.",
+
+  "PEN_TOOL": "Pen",
+  "BRIGHTNESS_TOOL": "Brightness",
+  "COLOR_PICKER_TOOL": "Color Picker",
+  "ELLIPSE_TOOL": "Ellipse",
+  "ERASER_TOOL": "Eraser",
+  "FLOOD_FILL_TOOL": "Flood Fill",
+  "LASSO_TOOL": "Lasso",
+  "LINE_TOOL": "Line",
+  "MAGIC_WAND_TOOL": "Magic Wand",
+  "MOVE_TOOL": "Move",
+  "MOVE_VIEWPORT_TOOL": "Move Viewport",
+  "RECTANGLE_TOOL": "Rectangle",
+  "ROTATE_VIEWPORT_TOOL": "Rotate Viewport",
+  "SELECT_TOOL_NAME": "Select",
+  "ZOOM_TOOL": "Zoom",
+
+  "SHAPE_LABEL": "Shape",
+  "MODE_LABEL": "Mode",
+  "SCOPE_LABEL": "Scope",
+  "FILL_SHAPE_LABEL": "Fill shape",
+  "FILL_COLOR_LABEL": "Fill color",
+  "TOOL_SIZE_LABEL": "Tool size",
+  "STRENGTH_LABEL": "Strength",
+  "NEW": "New",
+  "ADD": "Add",
+  "SUBTRACT": "Subtract",
+  "INTERSECT": "Intersect",
+  "RECTANGLE": "Rectangle",
+  "CIRCLE": "Circle",
+  "ALL_LAYERS": "All Layers",
+  "SINGLE_LAYER": "Single Layer",
+
+  "PATH_DOES_NOT_EXIST": "{0} does not exist.",
+  "LOCATION_DOES_NOT_EXIST": "Location does not exist.",
+  "FILE_NOT_FOUND": "File not found.",
+  "ARE_YOU_SURE": "Are you sure?",
+  "ARE_YOU_SURE_PATH_FULL_PATH": "Are you sure you want to delete {0}?\nThis data will be lost for all installations.\n(Full Path: {1})",
+
+  "OPEN_FILE": "Open file",
+  "FAILED_TO_OPEN_FILE": "Failed to open the file",
+
+  "OLD_FILE_FORMAT": "Old file format",
+  "OLD_FILE_FORMAT_DESCRIPTION": "This .pixi file uses the old format,\n which is no longer supported and can't be opened.",
+
+  "DISCORD_RICH_PRESENCE": "Rich Presence",
+  "ENABLED": "Enabled",
+  "SHOW_IMAGE_NAME": "Show image name",
+  "SHOW_IMAGE_SIZE": "Show image size",
+  "SHOW_LAYER_COUNT": "Show layer count",
+
+  "ABOUT": "About",
+
+  "MINIMIZE": "Minimize",
+  "RESTORE": "Restore",
+  "MAXIMIZE": "Maximize",
+  "CLOSE": "Close",
+
+  "NOTHING_FOUND": "Nothing found",
+  "EXPORT": "Export",
+  "EXPORT_IMAGE": "Export image",
+  "EXPORT_SIZE_HINT": "If you want to share the image, try {0}% for the best clarity",
+  "IMPORT": "Import",
+  "SHORTCUT_TEMPLATES": "Shortcut templates",
+  "RESET_ALL": "Reset all",
+
+  "LAYER": "Layer",
+  "LAYER_DELETE_SELECTED": "Delete active layer/folder",
+  "LAYER_DELETE_SELECTED_DESCRIPTIVE": "Delete active layer or folder",
+  
+  "LAYER_DELETE_ALL_SELECTED": "Delete all selected layers/folders",
+  "LAYER_DELETE_ALL_SELECTED_DESCRIPTIVE": "Delete all selected layers and/or folders",
+
+  "DELETE_SELECTED_PIXELS": "Delete selected pixels",
+
+  "NEW_FOLDER": "New folder",
+  "CREATE": "Create",
+  "CREATE_NEW_FOLDER": "Create new folder",
+  "NEW_LAYER": "New layer",
+  "CREATE_NEW_LAYER": "Create new layer",
+  "BASE_LAYER_NAME": "Base layer",
+  "NEW_IMAGE": "New image",
+  "CREATE_NEW_IMAGE": "Create new image",
+
+  "SAVE": "Save",
+  "SAVE_AS": "Save as...",
+  "IMAGE": "Image",
+  "SAVE_IMAGE": "Save image",
+  "SAVE_IMAGE_AS": "Save image as new",
+
+  "DUPLICATE": "Duplicate",
+  "DUPLICATE_SELECTED_LAYER": "Duplicate selected layer",
+  
+  "CREATE_MASK": "Create mask",
+  "DELETE_MASK": "Delete mask",
+  "TOGGLE_MASK": "Toggle mask",
+  "ENABLE_MASK": "Enable mask",
+  "APPLY_MASK": "Apply mask",
+  "TOGGLE_VISIBILITY": "Toggle visibility",
+  
+  "MOVE_MEMBER_UP": "Move member upwards",
+  "MOVE_MEMBER_UP_DESCRIPTIVE": "Move selected layer or folder upwards",
+
+  "MOVE_MEMBER_DOWN": "Move member downwards",
+  "MOVE_MEMBER_DOWN_DESCRIPTIVE": "Move selected layer or folder downwards",
+  
+  "MERGE_ALL_SELECTED_LAYERS": "Merge all selected layers",
+  "MERGE_WITH_ABOVE": "Merge selected layer with above",
+  "MERGE_WITH_ABOVE_DESCRIPTIVE": "Merge selected layer with the one above it",
+  "MERGE_WITH_BELOW": "Merge selected layer with below",
+  "MERGE_WITH_BELOW_DESCRIPTIVE": "Merge selected layer with the one below it",
+
+  "SELECTED_AREA_EMPTY": "Selected area is empty",
+  "NOTHING_TO_COPY": "Nothing to copy",
+  
+  "ADD_REFERENCE_LAYER": "Add Reference Layer",
+  "REFERENCE": "Reference",
+  "DELETE_REFERENCE_LAYER": "Delete reference layer",
+  "TRANSFORM_REFERENCE_LAYER": "Transform reference layer",
+  "TOGGLE_REFERENCE_LAYER_POS": "Toggle reference layer position",
+  "TOGGLE_REFERENCE_LAYER_POS_DESCRIPTIVE": "Toggle reference layer between topmost or most below",
+  "RESET_REFERENCE_LAYER_POS": "Reset reference layer position",
+  "REFERENCE_LAYER_PATH": "Reference layer path",
+  "PUT_REFERENCE_LAYER_ABOVE": "Put reference layer above",
+  "PUT_REFERENCE_LAYER_BELOW": "Put reference layer below",
+
+  "CLIP_CANVAS": "Clip Canvas",
+  "FLIP": "Flip",
+  "FLIP_IMG_VERTICALLY": "Flip Image Vertically",
+  "FLIP_IMG_HORIZONTALLY": "Flip Image Horizontally",
+  "FLIP_LAYERS_VERTICALLY": "Flip Selected Layers Vertically",
+  "FLIP_LAYERS_HORIZONTALLY": "Flip Selected Layers Horizontally",
+
+  "ROTATION": "Rotation",
+  "ROT_IMG_90": "Rotate Image 90 degrees",
+  "ROT_IMG_90_D": "Rotate Image 90°",
+  "ROT_IMG_180": "Rotate Image 180 degrees",
+  "ROT_IMG_180_D": "Rotate Image 180°",
+  "ROT_IMG_-90": "Rotate Image -90 degrees",
+  "ROT_IMG_-90_D": "Rotate Image -90°",
+  "ROT_LAYERS_90": "Rotate Selected Layers 90 degrees",
+  "ROT_LAYERS_90_D": "Rotate Selected Layers 90°",
+  "ROT_LAYERS_180": "Rotate Selected Layers 180 degrees",
+  "ROT_LAYERS_180_D": "Rotate Selected Layers 180°",
+  "ROT_LAYERS_-90": "Rotate Selected Layers -90 degrees",
+  "ROT_LAYERS_-90_D": "Rotate Selected Layers -90°",
+
+  "TOGGLE_VERT_SYMMETRY_AXIS": "Toggle vertical symmetry axis",
+  "TOGGLE_HOR_SYMMETRY_AXIS": "Toggle horizontal symmetry axis",
+
+  "DELETE_PIXELS": "Delete pixels",
+  "DELETE_PIXELS_DESCRIPTIVE": "Delete selected pixels",
+
+  "RESIZE_DOCUMENT": "Resize document",
+  "RESIZE_CANVAS": "Resize canvas",
+  "CENTER_CONTENT": "Center content",
+
+  "CUT": "Cut",
+  "CUT_DESCRIPTIVE": "Cut selected area/layers",
+  "PASTE": "Paste",
+  "PASTE_DESCRIPTIVE": "Paste clipboard contents",
+  "PASTE_AS_NEW_LAYER": "Paste as new layer",
+  "PASTE_AS_NEW_LAYER_DESCRIPTIVE": "Paste from clipboard as new layer",
+  "PASTE_REFERENCE_LAYER": "Paste reference layer",
+  "PASTE_REFERENCE_LAYER_DESCRIPTIVE": "Paste clipboard contents as reference layer",
+  "PASTE_COLOR": "Paste color",
+  "PASTE_COLOR_DESCRIPTIVE": "Paste color from clipboard",
+  "PASTE_COLOR_SECONDARY": "Paste color as secondary",
+  "PASTE_COLOR_SECONDARY_DESCRIPTIVE": "Paste color from clipboard as secondary color",
+
+  "CLIPBOARD": "Clipboard",
+  "COPY": "Copy",
+  "COPY_DESCRIPTIVE": "Copy to clipboard",
+  "COPY_COLOR_HEX": "Copy primary color (HEX)",
+  "COPY_COLOR_HEX_DESCRIPTIVE": "Copy primary color as HEX code",
+  "COPY_COLOR_RGB": "Copy primary color (RGB)",
+  "COPY_COLOR_RGB_DESCRIPTIVE": "Copy primary color as RGB code",
+  "COPY_COLOR_SECONDARY_HEX": "Copy secondary color (HEX)",
+  "COPY_COLOR_SECONDARY_HEX_DESCRIPTIVE": "Copy secondary color as HEX code",
+  "COPY_COLOR_SECONDARY_RGB": "Copy secondary color (RGB)",
+  "COPY_COLOR_SECONDARY_RGB_DESCRIPTIVE": "Copy secondary color as RGB code",
+
+  "PALETTE_COLORS": "Palette Colors",
+  "DROP_PALETTE": "Drop palette here",
+  "REPLACE_SECONDARY_BY_PRIMARY": "Replace secondary color by primary",
+  "REPLACE_SECONDARY_BY_PRIMARY_DESCRIPTIVE": "Replace the secondary color by the primary color",
+  "REPLACE_PRIMARY_BY_SECONDARY": "Replace primary color by secondary",
+  "REPLACE_PRIMARY_BY_SECONDARY_DESCRIPTIVE": "Replace the primary color by the secondary color",
+  "OPEN_PALETTE_BROWSER": "Open palette browser",
+  "OVERWRITE_PALETTE_CONSENT": "Palette '{0}' already exists, do you want to overwrite it?",
+  "PALETTE_EXISTS": "Palette already exists",
+  "REPLACE_PALETTE_CONSENT": "Replace current palette with selected one?",
+  "REPLACE_PALETTE": "Replace current palette",
+  "UNNAMED_PALETTE": "Unnamed Palette",
+
+  "SELECT_COLOR_1": "Select color 1",
+  "SELECT_COLOR_2": "Select color 2",
+  "SELECT_COLOR_3": "Select color 3",
+  "SELECT_COLOR_4": "Select color 4",
+  "SELECT_COLOR_5": "Select color 5",
+  "SELECT_COLOR_6": "Select color 6",
+  "SELECT_COLOR_7": "Select color 7",
+  "SELECT_COLOR_8": "Select color 8",
+  "SELECT_COLOR_9": "Select color 9",
+  "SELECT_COLOR_10": "Select color 10",
+
+  "SELECT_TOOL": "Select {0} Tool",
+
+  "SELECT_COLOR_1_DESCRIPTIVE": "Select the first color in the palette",
+  "SELECT_COLOR_2_DESCRIPTIVE": "Select the second color in the palette",
+  "SELECT_COLOR_3_DESCRIPTIVE": "Select the third color in the palette",
+  "SELECT_COLOR_4_DESCRIPTIVE": "Select the fourth color in the palette",
+  "SELECT_COLOR_5_DESCRIPTIVE": "Select the fifth color in the palette",
+  "SELECT_COLOR_6_DESCRIPTIVE": "Select the sixth color in the palette",
+  "SELECT_COLOR_7_DESCRIPTIVE": "Select the seventh color in the palette",
+  "SELECT_COLOR_8_DESCRIPTIVE": "Select the eighth color in the palette",
+  "SELECT_COLOR_9_DESCRIPTIVE": "Select the ninth color in the palette",
+  "SELECT_COLOR_10_DESCRIPTIVE": "Select the tenth color in the palette",
+
+  "SWAP_COLORS": "Swap colors",
+  "SWAP_COLORS_DESCRIPTIVE": "Swap primary and secondary colors",
+
+  "CLICK_SELECT_PRIMARY": "Click to select as main color.",
+
+  "SEARCH": "Search",
+  "COMMAND_SEARCH": "Command search",
+  "OPEN_COMMAND_SEARCH": "Open command search window",
+
+  "SELECT": "Select",
+  "DESELECT": "Deselect",
+  "INVERT": "Invert",
+  "SELECTION": "Selection",
+  "SELECT_ALL": "Select all",
+  "SELECT_ALL_DESCRIPTIVE": "Select everything",
+  "CLEAR_SELECTION": "Clear selection",
+  "INVERT_SELECTION": "Invert selection",
+  "INVERT_SELECTION_DESCRIPTIVE": "Invert the selected area",
+  "TRANSFORM_SELECTED_AREA": "Transform selected area",
+  "NUDGE_SELECTED_LEFT": "Nudge selected object left",
+  "NUDGE_SELECTED_RIGHT": "Nudge selected object right",
+  "NUDGE_SELECTED_UP": "Nudge selected object up",
+  "NUDGE_SELECTED_DOWN": "Nudge selected object down",
+  "MASK_FROM_SELECTION": "New mask from selection",
+  "MASK_FROM_SELECTION_DESCRIPTIVE": "Selection to new mask",
+  "ADD_SELECTION_TO_MASK": "Add selection to mask",
+  "SUBTRACT_SELECTION_FROM_MASK": "Subtract selection from mask",
+  "INTERSECT_SELECTION_MASK": "Intersect selection with mask",
+  "SELECTION_TO_MASK": "Selection to mask",
+  "TO_NEW_MASK": "to new mask",
+  "ADD_TO_MASK": "add to mask",
+  "SUBTRACT_FROM_MASK": "subtract from mask",
+  "INTERSECT_WITH_MASK": "intersect with mask",
+
+  "STYLUS": "Stylus",
+  "PEN_MODE": "Pen mode",
+  "TOGGLE_PEN_MODE": "Toggle pen mode",
+
+  "UNDO": "Undo",
+  "UNDO_DESCRIPTIVE": "Undo last action",
+  "REDO": "Redo",
+  "REDO_DESCRIPTIVE": "Redo last action",
+  "WINDOWS": "Windows",
+
+  "VIEW": "View",
+  "TOGGLE_GRIDLINES": "Toggle gridlines",
+  "ZOOM_IN": "Zoom in",
+  "ZOOM_OUT": "Zoom out",
+  "NEW_WINDOW_FOR_IMG": "New window for current image",
+  "CENTER_ACTIVE_VIEWPORT": "Center active viewport",
+  "FLIP_VIEWPORT_HORIZONTALLY": "Flip viewport horizontally",
+  "HORIZONTAL_LINE_SYMMETRY": "Horizontal line symmetry",
+  "VERTICAL_LINE_SYMMETRY": "Vertical line symmetry",
+  "TOGGLE_VERTICAL_SYMMETRY": "Toggle vertical symmetry",
+  "TOGGLE_HORIZONTAL_SYMMETRY": "Toggle horizontal symmetry",
+  "FLIP_VIEWPORT_VERTICALLY": "Flip viewport vertically",
+  "RESET_VIEWPORT": "Reset viewport",
+  "VIEWPORT_SETTINGS": "Viewport settings",
+  "SETTINGS": "Settings",
+  "OPEN_SETTINGS": "Open settings",
+  "OPEN_SETTINGS_DESCRIPTIVE": "Open settings window",
+
+  "OPEN_STARTUP_WINDOW": "Open startup window",
+  "OPEN_SHORTCUT_WINDOW": "Open shortcuts window",
+  "OPEN_ABOUT_WINDOW": "Open about window",
+  "OPEN_NAVIGATION_WINDOW": "Open navigation window",
+
+  "COLOR_PICKER_TITLE": "Color Picker",
+  "COLOR_SLIDERS_TITLE": "Color Sliders",
+  "PALETTE_TITLE": "Palette",
+  "SWATCHES_TITLE": "Swatches",
+  "LAYERS_TITLE": "Layers",
+  "NAVIGATION_TITLE": "Navigation",
+
+  "NORMAL_BLEND_MODE": "Normal",
+  "DARKEN_BLEND_MODE": "Darken",
+  "MULTIPLY_BLEND_MODE": "Multiply",
+  "COLOR_BURN_BLEND_MODE": "Color burn",
+  "LIGHTEN_BLEND_MODE": "Lighten",
+  "SCREEN_BLEND_MODE": "Screen",
+  "COLOR_DODGE_BLEND_MODE": "Color dodge",
+  "LINEAR_DODGE_BLEND_MODE": "Linear dodge (Add)",
+  "OVERLAY_BLEND_MODE": "Overlay",
+  "SOFT_LIGHT_BLEND_MODE": "Soft light",
+  "HARD_LIGHT_BLEND_MODE": "Hard light",
+  "DIFFERENCE_BLEND_MODE": "Difference",
+  "EXCLUSION_BLEND_MODE": "Exclusion",
+  "HUE_BLEND_MODE": "Hue",
+  "SATURATION_BLEND_MODE": "Saturation",
+  "LUMINOSITY_BLEND_MODE": "Luminosity",
+  "COLOR_BLEND_MODE": "Color",
+  "NOT_SUPPORTED_BLEND_MODE": "Not supported",
+
+  "RESTART": "Restart",
+
+  "SORT_BY": "Sort by",
+  "NAME": "Name",
+  "COLORS": "Colors",
+  "DEFAULT": "Default",
+  "ALPHABETICAL": "Alphabetical",
+  "COLOR_COUNT": "Color count",
+
+  "ANY": "Any",
+  "MAX": "Max",
+  "MIN": "Min",
+  "EXACT": "Exact",
+
+  "ASCENDING": "Ascending",
+  "DESCENDING": "Descending",
+
+  "NAME_IS_TOO_LONG": "The name is too long",
+  "STOP_IT_TEXT1": "That's enough. Tidy up your file names.",
+  "STOP_IT_TEXT2": "Can you stop copying these names please?",
+  "STOP_IT_TEXT3": "No, really, stop it.",
+  "STOP_IT_TEXT4": "Don't you have anything better to do?",
+
+  "CHOOSE": "Choose",
+  "REMOVE": "Remove",
+  "REPLACER_TOOLTIP": "Right click on palette color and choose 'Replace' or drop it here.",
+  "CLICK_TO_CHOOSE_COLOR": "Click to choose the color",
+  "REPLACE_COLOR": "Replace color",
+  "PALETTE_COLOR_TOOLTIP": "Click to select as main color. Drag and drop onto another palette color to swap them.",
+  "ADD_FROM_SWATCHES": "Add from swatches",
+  "ADD_COLOR_TO_PALETTE": "Add color to palette",
+
+  "USE_IN_CURRENT_IMAGE": "Use in current image",
+  "ADD_TO_FAVORITES": "Add to favorites",
+
+  "BROWSE_PALETTES": "Browse palettes",
+  "LOAD_PALETTE": "Load palette",
+  "SAVE_PALETTE": "Save palette",
+
+  "FAVORITES": "Favorites",
+  "ADD_FROM_CURRENT_PALETTE": "Add from current palette",
+  "OPEN_PALETTES_DIR_TOOLTIP": "Open palettes directory in explorer",
+  "BROWSE_ON_LOSPEC_TOOLTIP": "Browse palettes on Lospec",
+  "IMPORT_FROM_FILE_TOOLTIP": "Import from file",
+
+  "TOP_LEFT": "Top left",
+  "TOP_CENTER": "Top center",
+  "TOP_RIGHT": "Top right",
+  "MIDDLE_LEFT": "Middle left",
+  "MIDDLE_CENTER": "Middle center",
+  "MIDDLE_RIGHT": "Middle right",
+  "BOTTOM_LEFT": "Bottom left",
+  "BOTTOM_CENTER": "Bottom center",
+  "BOTTOM_RIGHT": "Bottom right",
+
+  "CLIP_TO_BELOW": "Clip to layer below",
+  "MOVE_UPWARDS": "Move upwards",
+  "MOVE_DOWNWARDS": "Move downwards",
+  "MERGE_SELECTED": "Merge selected",
+  "LOCK_TRANSPARENCY": "Lock transparency",
+  "RENAME": "Rename",
+
+  "COULD_NOT_LOAD_PALETTE": "Couldn't fetch palettes",
+  "NO_PALETTES_FOUND": "No palettes found.",
+  "LOSPEC_LINK_TEXT": "I heard you can find some here: lospec.com/palette-list",
+  "PALETTE_BROWSER": "Palette Browser",
+  "DELETE_PALETTE_CONFIRMATION": "Are you sure you want to delete this palette? This cannot be undone.",
+
+  "SHORTCUTS_IMPORTED": "Shortcuts from {0} were imported successfully.",
+  "SHORTCUT_PROVIDER_DETECTED": "We've detected, that you have {0} installed. Do you want to import shortcuts from it?",
+  "IMPORT_FROM_INSTALLATION": "Import from installation",
+  "IMPORT_INSTALLATION_OPTION1": "Import from installation",
+  "IMPORT_INSTALLATION_OPTION2": "Use defaults",
+  "IMPORT_FROM_TEMPLATE": "Import from template",
+  "SHORTCUTS_IMPORTED_SUCCESS": "Shortcuts were imported successfully.",
+  "WARNING_RESET_SHORTCUTS_DEFAULT": "Are you sure you want to reset all shortcuts to their default value?",
+  "PRESS_ANY_KEY": "Press any key",
+  "NONE_SHORTCUT": "None",
+  "CTRL_KEY": "Ctrl",
+  "SHIFT_KEY": "Shift",
+  "ALT_KEY": "Alt",
+
+  "SUCCESS": "Success",
+  "ERROR": "Error",
+  "WARNING": "Warning",
+  "INTERNAL_ERROR": "Internal error",
+  "ERROR_IMPORTING_IMAGE": "An error occured while importing the image.",
+  "ERROR_SAVE_LOCATION": "Couldn't save the file to the specified location",
+  "ERROR_WHILE_SAVING": "An internal error occured while saving. Please try again.",
+  "UNKNOWN_ERROR_SAVING": "An error occured while saving.",
+  "SECURITY_ERROR": "Security error",
+  "SECURITY_ERROR_MSG": "No rights to write to the specified location.",
+  "IO_ERROR": "IO error",
+  "IO_ERROR_MSG": "Error while writing to disk.",
+
+  "FAILED_ASSOCIATE_LOSPEC": "Failed to associate Lospec Palette protocol.",
+  "FAILED_ASSOCIATE_PIXI": "Failed to associate .pixi file with PixiEditor.",
+  "SHORTCUTS_CORRUPTED_TITLE": "Corrupted shortcuts file",
+  "SHORTCUTS_CORRUPTED": "Shortcuts file was corrupted, resetting to default.",
+  "FAILED_DOWNLOAD_PALETTE": "Failed to download palette",
+  "COULD_NOT_SAVE_PALETTE": "There was an error while saving the palette.",
+  "NO_COLORS_TO_SAVE": "There are no colors to save.",
+  "FILE_INCORRECT_FORMAT": "The file was not in a correct format",
+  "INVALID_FILE": "Invalid file",
+  "SHORTCUTS_FILE_INCORRECT_FORMAT": "Shortcuts file was not in a correct format",
+  "UNSUPPORTED_FILE_FORMAT": "This file format is unsupported",
+  "ALREADY_ASSIGNED": "Already assigned",
+  "REPLACE": "Replace",
+  "SWAP": "Swap",
+  "SHORTCUT_ALREADY_ASSIGNED_SWAP": "This shortcut is already assigned to '{0}'\nDo you want to replace the existing shortcut or swap the two?",
+  "SHORTCUT_ALREADY_ASSIGNED_OVERWRITE": "This shortcut is already assigned to '{0}'\nDo you want to replace the existing shortcut?",
+
+  "UNSAVED_CHANGES": "Unsaved changes",
+  "DOCUMENT_MODIFIED_SAVE": "The document has been modified. Do you want to save changes?",
+  "SESSION_UNSAVED_DATA": "{0} with unsaved data. Are you sure?",
+
+  "PROJECT_MAINTAINERS": "Project Maintainers",
+  "OTHER_AWESOME_CONTRIBUTORS": "And other awesome contributors",
+
+  "HELP": "Help",
+  "REDDIT": "Reddit",
+  "GITHUB": "GitHub",
+  "YOUTUBE": "YouTube",
+  "DONATE": "Donate",
+  
+  "OPEN_LOCALIZATION_DEBUG_WINDOW": "Open Localization Debug Window",
+  "FORCE_OTHER_FLOW_DIRECTION": "Force other flow direction",
+  "API_KEY": "API Key",
+  "LOCALIZATION_VIEW_TYPE": "Localization View Type",
+  "LOAD_LANGUAGE_FROM_FILE": "Load language from file",
+  "LOG_IN": "Log in",
+  "SYNC": "Sync",
+  
+  "NOT_LOGGED_IN": "Not logged in",
+  "POE_EDITOR_ERROR": "POEditor Error: {0} {1}",
+  "HTTP_ERROR_MESSAGE": "HTTP Error: {0} {1}",
+  "LOGGED_IN": "Logged in",
+  "SYNCED_SUCCESSFULLY": "Synced successfully",
+  "EXCEPTION_ERROR": "Exception: {0}",
+  
+  "PIXEL_UNIT": "px"
+}

+ 2 - 0
src/PixiEditor/Data/Localization/Languages/es.json

@@ -0,0 +1,2 @@
+{
+}

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

@@ -0,0 +1,504 @@
+{
+  "RECENT_FILES": "Ostatnie pliki",
+  "OPEN_FILE": "Otwórz plik",
+  "NEW_FILE": "Nowy",
+  "RECENT_EMPTY_TEXT": "Ale tu pusto",
+  "LANGUAGE": "Język",
+  "GENERAL": "Ogólne",
+  "DISCORD": "Discord",
+  "KEY_BINDINGS": "Skróty klawiszowe",
+  "MISC": "Inne",
+  "SHOW_STARTUP_WINDOW": "Otwórz okno powitalne na starcie",
+  "SHOW_IMAGE_PREVIEW_TASKBAR": "Pokaż podgląd obrazu w pasku zadań",
+  "RECENT_FILE_LENGTH": "Ilość ostatnich plików",
+  "RECENT_FILE_LENGTH_TOOLTIP": "Ile plików jest wyświetlanych w menu Plik -> Ostatnie. Domyślnie 8",
+  "DEFAULT_NEW_SIZE": "Domyslny rozmiar nowego obrazu",
+  "WIDTH": "Szerokość",
+  "HEIGHT": "Wysokość",
+  "TOOLS": "Narzędzia",
+  "ENABLE_SHARED_TOOLBAR": "Włącz wspólny pasek narzędzi",
+  "AUTOMATIC_UPDATES": "Automatyczne aktualizacje",
+  "CHECK_FOR_UPDATES": "Sprawdzaj aktualizacje na starcie",
+  "UPDATE_STREAM": "Kanał aktualizacji",
+  "UPDATE_CHANNEL_HELP_TOOLTIP": "Kanały aktualizacji mogą być tylko ustawione w wersji samodzielnej (pobranej z https://pixieditor.net).\nSteam i Microsoft Store zajmuje się aktualizacjami osobno.",
+  "DEBUG": "Debugowanie",
+  "ENABLE_DEBUG_MODE": "Włącz tryb debugowania",
+  "OPEN_CRASH_REPORTS_DIR": "Otwórz folder z raportami o awariach",
+  "DISCORD_RICH_PRESENCE": "Rich Presence",
+  "ENABLED": "Włączony",
+  "SHOW_IMAGE_NAME": "Pokaż nazwę obrazu",
+  "SHOW_IMAGE_SIZE": "Pokaż wielkość obrazu",
+  "SHOW_LAYER_COUNT": "Pokaż ilość warstw",
+  "FILE": "Plik",
+  "RECENT": "Ostatnie",
+  "OPEN": "Otwórz",
+  "SAVE_PIXI": "Zapisz (.pixi)",
+  "SAVE_AS_PIXI": "Zapisz jako... (.pixi)",
+  "EXPORT_IMG": "Eksportuj (.png, .jpg, itp.)",
+  "EDIT": "Edytuj",
+  "EXIT": "Wyjdź",
+  "PERCENTAGE": "Procentowo",
+  "ABSOLUTE": "Absolutnie",
+  "PRESERVE_ASPECT_RATIO": "Zachowaj proporcje",
+  "ANCHOR_POINT": "Punkt zaczepienia",
+  "RESIZE_IMAGE": "Zmień rozmiar obrazu",
+  "RESIZE": "Zmień rozmiar",
+  "EDITING_IMG": "Edytuje obraz",
+  "EDITING_IMG_DETAIL": "Edytuje {0}",
+  "ONE_LAYER": "1 warstwa",
+  "LAYERS": "{0} warstw",
+  "DOCUMENTATION": "Dokumentacja",
+  "WEBSITE": "Strona internetowa",
+  "OPEN_WEBSITE": "Otwórz stronę internetową",
+  "REPOSITORY": "Repozytorium",
+  "OPEN_REPOSITORY": "Otwórz repozytorium",
+  "LICENSE": "Licencja",
+  "OPEN_LICENSE": "Otwórz licencję",
+  "THIRD_PARTY_LICENSES": "Licencje stron trzecich",
+  "OPEN_THIRD_PARTY_LICENSES": "Otwórz licencje stron trzecich",
+  "APPLY_TRANSFORM": "Zastosuj transformację",
+  "INCREASE_TOOL_SIZE": "Zwiększ rozmiar narzędzia",
+  "DECREASE_TOOL_SIZE": "Zmniejsz rozmiar narzędzia",
+  "TO_INSTALL_UPDATE": "aby zainstalować aktualizacje {0}",
+  "DOWNLOADING_UPDATE": "Pobieranie aktualizacji...",
+  "UPDATE_READY": "Aktualizacja jest gotowa do instalacji. Czy chcesz ją zainstalować teraz?",
+  "NEW_UPDATE": "Nowa aktualizacja",
+  "COULD_NOT_UPDATE_WITHOUT_ADMIN": "Nie można zaaktualizować bez uprawnien administratora. Włącz PixiEditor jako administrator.",
+  "INSUFFICIENT_PERMISSIONS": "Niewystarczające uprawnienia",
+  "UPDATE_CHECK_FAILED": "Sprawdzanie aktualizacji nie powiodło się",
+  "COULD_NOT_CHECK_FOR_UPDATES": "Nie udało się sprawdzić aktualizacji",
+  "VERSION": "Wersja {0}",
+  "OPEN_TEMP_DIR": "Otwórz folder temp",
+  "OPEN_LOCAL_APPDATA_DIR": "Otwórz folder Local AppData",
+  "OPEN_ROAMING_APPDATA_DIR": "Otwórz folder Roaming AppData",
+  "OPEN_INSTALLATION_DIR": "Otwórz folder instalacji",
+  "DUMP_ALL_COMMANDS": "Wypisz wszystkie komendy",
+  "DUMP_ALL_COMMANDS_DESCRIPTIVE": "Wypisz wszystkie komendy do pliku",
+  "CRASH": "Crash",
+  "CRASH_APP": "Zcrashuj aplikację",
+  "DELETE_USR_PREFS": "Usuń preferencje użytkownika (Roaming AppData)",
+  "DELETE_SHORTCUT_FILE": "Usuń plik skrótów klawiszowych (Roaming AppData)",
+  "DELETE_EDITOR_DATA": "Usuń dane edytora (Local AppData)",
+  "GENERATE_KEY_BINDINGS_TEMPLATE": "Stwórz szablon skrótów klawiszowych",
+  "GENERATE_KEY_BINDINGS_TEMPLATE_DESCRIPTIVE": "Stwórz szablon JSON skrótów klawiszowych",
+  "VALIDATE_SHORTCUT_MAP": "Sprawdź poprawność mapy skrótów",
+  "VALIDATE_SHORTCUT_MAP_DESCRIPTIVE": "Sprawdza poprawność mapy skrótów",
+  "VALIDATION_KEYS_NOTICE_DIALOG": "Puste klucze: {0}\nNieznane komendy: {1}",
+  "RESULT": "Wynik",
+  "CLEAR_RECENT_DOCUMENTS": "Wyczyść ostatnie dokumenty",
+  "CLEAR_RECENTLY_OPENED_DOCUMENTS": "Wyczyść ostatnio otwarte dokumenty",
+  "OPEN_CMD_DEBUG_WINDOW": "Otwórz okno do debugowania komend",
+  "PATH_DOES_NOT_EXIST": "{0} nie istnieje.",
+  "LOCATION_DOES_NOT_EXIST": "Lokalizacja nie istnieje.",
+  "FILE_NOT_FOUND": "Plik nie znaleziony.",
+  "FILE_NOT_FOUND_PATH_FULL_PATH": "Plik {0} nie istnieje\n(Pełna ścieżka: {1})",
+  "ARE_YOU_SURE": "Jesteś pewien?",
+  "ARE_YOU_SURE_PATH_FULL_PATH": "Czy na pewno chcesz usunąć {0}?\nDane zostaną utracone dla wszystkich instalacji.\n(Pełna ścieżka: {1})",
+  "FAILED_TO_OPEN_FILE": "Nie udało się otworzyć pliku",
+  "OLD_FILE_FORMAT": "Stary format pliku",
+  "OLD_FILE_FORMAT_DESCRIPTION": "Ten plik .pixi używa starego formatu, który nie jest już wspierany i nie można go otworzyć.",
+  "NOTHING_FOUND": "Nic nie znaleziono",
+  "EXPORT": "Eksportuj",
+  "EXPORT_IMAGE": "Eksportuj obraz",
+  "IMPORT": "Importuj",
+  "SHORTCUT_TEMPLATES": "Szablony",
+  "RESET_ALL": "Resetuj wszystko",
+  "LAYER": "Warstwa",
+  "LAYER_DELETE_SELECTED": "Usuń aktywną warstwę/folder",
+  "LAYER_DELETE_SELECTED_DESCRIPTIVE": "Usuń aktywną warstwę lub folder",
+  "LAYER_DELETE_ALL_SELECTED": "Usuń wszystkie zaznaczone warstwy/foldery",
+  "LAYER_DELETE_ALL_SELECTED_DESCRIPTIVE": "Usuń wszystkie zaznaczone warstwy i/lub foldery",
+  "DELETE_SELECTED_PIXELS": "Usuń zaznaczone piksele",
+  "NEW_FOLDER": "Nowy folder",
+  "CREATE_NEW_FOLDER": "Stwórz nowy folder",
+  "NEW_LAYER": "Nowa warstwa",
+  "CREATE_NEW_LAYER": "Stwórz nową warstwę",
+  "NEW_IMAGE": "Nowy obraz",
+  "CREATE_NEW_IMAGE": "Stwórz nowy obraz",
+  "SAVE": "Zapisz",
+  "SAVE_AS": "Zapisz jako...",
+  "IMAGE": "Obraz",
+  "SAVE_IMAGE": "Zapisz obraz",
+  "SAVE_IMAGE_AS": "Zapisz obraz jako",
+  "DUPLICATE": "Duplikuj",
+  "DUPLICATE_SELECTED_LAYER": "Duplikuj zaznaczoną warstwę",
+  "CREATE_MASK": "Stwórz maskę",
+  "DELETE_MASK": "Usuń maskę",
+  "TOGGLE_MASK": "Przełącz maskę",
+  "APPLY_MASK": "Zastosuj maskę",
+  "TOGGLE_VISIBILITY": "Przełącz widoczność",
+  "MOVE_MEMBER_UP": "Przesuń obiekt wyżej",
+  "MOVE_MEMBER_UP_DESCRIPTIVE": "Przesuń zaznaczoną warstwę lub folder wyżej",
+  "MOVE_MEMBER_DOWN": "Przesuń obiekt niżej",
+  "MOVE_MEMBER_DOWN_DESCRIPTIVE": "Przesuń zaznaczoną warstwę lub folder niżej",
+  "MERGE_ALL_SELECTED_LAYERS": "Scal wszystkie warstwy",
+  "MERGE_WITH_ABOVE": "Scal zaznaczoną warstwę z obiektem wyżej",
+  "MERGE_WITH_ABOVE_DESCRIPTIVE": "Scal zaznaczoną warstwę z warstwą wyżej",
+  "MERGE_WITH_BELOW": "Scal zaznaczoną warstwę z obiektem niżej",
+  "MERGE_WITH_BELOW_DESCRIPTIVE": "Scal zaznaczoną warstwę z warstwą niżej",
+  "ADD_REFERENCE_LAYER": "Dodaj warstwę referencyjną",
+  "DELETE_REFERENCE_LAYER": "Usuń warstwę referencyjną",
+  "TRANSFORM_REFERENCE_LAYER": "Modyfikuj warstwę referencyjną",
+  "TOGGLE_REFERENCE_LAYER_POS": "Przełącz pozycję warstwy referencyjnej",
+  "TOGGLE_REFERENCE_LAYER_POS_DESCRIPTIVE": "Przełącz warstwę referencyjną pomiędzy górą a dołem",
+  "RESET_REFERENCE_LAYER_POS": "Resetuj pozycję warstwy referencyjnej",
+  "CLIP_CANVAS": "Dopasuj płótno do zawartości",
+  "FLIP_IMG_VERTICALLY": "Przerzuć obraz w pionie",
+  "FLIP_IMG_HORIZONTALLY": "Przerzuć obraz w poziomie",
+  "FLIP_LAYERS_VERTICALLY": "Przerzuć zaznaczone warstwy w pionie",
+  "FLIP_LAYERS_HORIZONTALLY": "Przerzuć zaznaczone warstwy w poziomie",
+  "ROT_IMG_90": "Obróć obraz 90 stopni",
+  "ROT_IMG_180": "Obróć obraz 180 stopni",
+  "ROT_IMG_-90": "Obróć obraz -90 stopni",
+  "ROT_LAYERS_90": "Obróć zaznaczone warstwy 90 stopni",
+  "ROT_LAYERS_180": "Obróć zaznaczone warstwy 180 stopni",
+  "ROT_LAYERS_-90": "Obróć zaznaczone warstwy -90 stopni",
+  "TOGGLE_VERT_SYMMETRY_AXIS": "Przełącz oś symetrii w pionie",
+  "TOGGLE_HOR_SYMMETRY_AXIS": "Przełącz oś symetrii w poziomie",
+  "DELETE_PIXELS": "Usuń piksele",
+  "DELETE_PIXELS_DESCRIPTIVE": "Usuń zaznaczone piksele",
+  "RESIZE_DOCUMENT": "Zmień rozmiar dokumentu",
+  "RESIZE_CANVAS": "Zmien rozmiar płótna",
+  "CENTER_CONTENT": "Wyśrodkuj zawartość",
+  "CUT": "Wytnij",
+  "CUT_DESCRIPTIVE": "Wytnij zaznaczony obszar/warstwę",
+  "PASTE": "Wklej",
+  "PASTE_DESCRIPTIVE": "Wklej zawartość schowka",
+  "PASTE_AS_NEW_LAYER": "Wklej jako nowa warstwa",
+  "PASTE_AS_NEW_LAYER_DESCRIPTIVE": "Wklej ze schowka jako nowa warstwa",
+  "PASTE_REFERENCE_LAYER": "Wklej warstwę referencyjną",
+  "PASTE_REFERENCE_LAYER_DESCRIPTIVE": "Wklej zawartość schowka jako warstwa referencyjna",
+  "PASTE_COLOR": "Wklej kolor",
+  "PASTE_COLOR_DESCRIPTIVE": "Wklej kolor ze schowka",
+  "PASTE_COLOR_SECONDARY": "Wklej kolor jako drugorzędny",
+  "PASTE_COLOR_SECONDARY_DESCRIPTIVE": "Wklej kolor ze schowka jako drugorzędny",
+  "CLIPBOARD": "Schowek",
+  "COPY": "Kopiuj",
+  "COPY_DESCRIPTIVE": "Kopiuj do schowka",
+  "COPY_COLOR_HEX": "Kopiuj pierwszorzędny kolor (HEX)",
+  "COPY_COLOR_HEX_DESCRIPTIVE": "Kopiuj pierwszorzędny kolor jako kod HEX",
+  "COPY_COLOR_RGB": "Kopiuj pierwszorzędny kolor (RGB)",
+  "COPY_COLOR_RGB_DESCRIPTIVE": "Kopiuj pierwszorzędny kolor jako kod RGB",
+  "COPY_COLOR_SECONDARY_HEX": "Kopiuj drugorzędny kolor (HEX)",
+  "COPY_COLOR_SECONDARY_HEX_DESCRIPTIVE": "Kopiuj drugorzędny kolor jako kod HEX",
+  "COPY_COLOR_SECONDARY_RGB": "Kopiuj drugorzędny kolor (RGB)",
+  "COPY_COLOR_SECONDARY_RGB_DESCRIPTIVE": "Kopiuj drugorzędny kolor jako kod RGB",
+  "PALETTE_COLORS": "Paleta Kolorów",
+  "REPLACE_SECONDARY_BY_PRIMARY": "Zastąp drugorzędny kolor pierwszorzędnym",
+  "REPLACE_SECONDARY_BY_PRIMARY_DESCRIPTIVE": "Zastąp drugorzędny kolor pierwszorzędnym",
+  "REPLACE_PRIMARY_BY_SECONDARY": "Zastąp pierwszorzędny kolor drugorzędnym",
+  "REPLACE_PRIMARY_BY_SECONDARY_DESCRIPTIVE": "Zastąp pierwszorzędny kolor drugorzędnym",
+  "OPEN_PALETTE_BROWSER": "Otwórz przeglądarkę palet",
+  "OVERWRITE_PALETTE_CONSENT": "Paleta '{0}' już istnieje, czy chcesz ją nadpisać?",
+  "PALETTE_EXISTS": "Paleta już istnieje",
+  "REPLACE_PALETTE_CONSENT": "Zastąpić aktywną paletę zaznaczoną?",
+  "REPLACE_PALETTE": "Zastąp aktywną paletę",
+  "SELECT_COLOR_1": "Wybierz kolor 1",
+  "SELECT_COLOR_2": "Wybierz kolor 2",
+  "SELECT_COLOR_3": "Wybierz kolor 3",
+  "SELECT_COLOR_4": "Wybierz kolor 4",
+  "SELECT_COLOR_5": "Wybierz kolor 5",
+  "SELECT_COLOR_6": "Wybierz kolor 6",
+  "SELECT_COLOR_7": "Wybierz kolor 7",
+  "SELECT_COLOR_8": "Wybierz kolor 8",
+  "SELECT_COLOR_9": "Wybierz kolor 9",
+  "SELECT_COLOR_10": "Wybierz kolor 10",
+  "SELECT_TOOL": "Zaznacz narzędzie {0}",
+  "SELECT_COLOR_1_DESCRIPTIVE": "Zaznacz pierwszy kolor w palecie",
+  "SELECT_COLOR_2_DESCRIPTIVE": "Zaznacz drugi kolor w palecie",
+  "SELECT_COLOR_3_DESCRIPTIVE": "Zaznacz trzeci kolor w palecie",
+  "SELECT_COLOR_4_DESCRIPTIVE": "Zaznacz czwarty kolor w palecie",
+  "SELECT_COLOR_5_DESCRIPTIVE": "Zaznacz piąty kolor w palecie",
+  "SELECT_COLOR_6_DESCRIPTIVE": "Zaznacz szósty kolor w palecie",
+  "SELECT_COLOR_7_DESCRIPTIVE": "Zaznacz siódmy kolor w palecie",
+  "SELECT_COLOR_8_DESCRIPTIVE": "Zaznacz ósmy kolor w palecie",
+  "SELECT_COLOR_9_DESCRIPTIVE": "Zaznacz dziewiąty kolor w palecie",
+  "SELECT_COLOR_10_DESCRIPTIVE": "Zaznacz dziesiąty kolor w palecie",
+  "SWAP_COLORS": "Zamień kolory",
+  "SWAP_COLORS_DESCRIPTIVE": "Zamień pierwszorzędny kolor z drugorzędnym",
+  "SEARCH": "Szukaj",
+  "COMMAND_SEARCH": "Wyszukiwanie komend",
+  "OPEN_COMMAND_SEARCH": "Otwórz okno wyszukiwania komend",
+  "SELECT": "Zaznacz",
+  "DESELECT": "Odznacz",
+  "INVERT": "Odwróć",
+  "SELECTION": "Zaznaczenie",
+  "SELECT_ALL": "Zaznacz wszystko",
+  "SELECT_ALL_DESCRIPTIVE": "Zaznacz wszystko",
+  "CLEAR_SELECTION": "Wyczyść zaznaczenie",
+  "INVERT_SELECTION": "Odwróć zaznaczenie",
+  "INVERT_SELECTION_DESCRIPTIVE": "Odwróć zaznaczony obszar",
+  "TRANSFORM_SELECTED_AREA": "Transformuj zaznaczony obszar",
+  "NUDGE_SELECTED_LEFT": "Pstryknij zaznaczony obiekt w lewo",
+  "NUDGE_SELECTED_RIGHT": "Pstryknij zaznaczony obiekt w prawo",
+  "NUDGE_SELECTED_UP": "Pstryknij zaznaczony obiekt w górę",
+  "NUDGE_SELECTED_DOWN": "Pstryknij zaznaczony obiekt w dół",
+  "MASK_FROM_SELECTION": "Nowa maska z zaznaczenia",
+  "MASK_FROM_SELECTION_DESCRIPTIVE": "Zrób nową maskę z zaznaczenia",
+  "ADD_SELECTION_TO_MASK": "Dodaj zaznaczenie do maski",
+  "SUBTRACT_SELECTION_FROM_MASK": "Odejmij zaznaczenie z maski",
+  "INTERSECT_SELECTION_MASK": "Wykonaj intersekcję zaznaczenia z maską",
+  "SELECTION_TO_MASK": "Zaznaczenie do maski",
+  "TO_NEW_MASK": "do nowej maski",
+  "ADD_TO_MASK": "dodaj do maski",
+  "SUBTRACT_FROM_MASK": "odejmij z maski",
+  "INTERSECT_WITH_MASK": "wykonaj intersekcję z maską",
+  "STYLUS": "Rysik",
+  "TOGGLE_PEN_MODE": "Przełącz tryb długopisu",
+  "UNDO": "Cofnij",
+  "UNDO_DESCRIPTIVE": "Cofnij ostatnią akcję",
+  "REDO": "Ponów",
+  "REDO_DESCRIPTIVE": "Ponów ostatnią akcję",
+  "WINDOWS": "Okna",
+  "TOGGLE_GRIDLINES": "Przełącz linie siatki",
+  "ZOOM_IN": "Przybliż",
+  "ZOOM_OUT": "Oddal",
+  "NEW_WINDOW_FOR_IMG": "Nowe okno dla aktywnego obrazu",
+  "CENTER_ACTIVE_VIEWPORT": "Wyśrodkuj aktywny widok",
+  "FLIP_VIEWPORT_HORIZONTALLY": "Przerzuć widok w poziomie",
+  "FLIP_VIEWPORT_VERTICALLY": "Przerzuć widok w pionie",
+  "SETTINGS": "Ustawienia",
+  "OPEN_SETTINGS": "Otwórz ustawienia",
+  "OPEN_SETTINGS_DESCRIPTIVE": "Otwórz okno ustawień",
+  "OPEN_STARTUP_WINDOW": "Otwórz okno startowe",
+  "OPEN_SHORTCUT_WINDOW": "Otwórz okno ze skrótami klawiszowymi",
+  "OPEN_ABOUT_WINDOW": "Otwórz okno informacji",
+  "OPEN_NAVIGATION_WINDOW": "Otwórz okno nawigacji",
+  "ERROR": "Błąd",
+  "INTERNAL_ERROR": "Wewnętrzny błąd",
+  "ERROR_SAVE_LOCATION": "Nie udało się zapisać pliku do wskazanej lokalizacji",
+  "ERROR_WHILE_SAVING": "Wystąpił wewnętrzny błąd podczas zapisu. Spróbuj ponownie.",
+  "UNKNOWN_ERROR_SAVING": "Wystąpił błąd poczas zapisu.",
+  "FAILED_ASSOCIATE_LOSPEC": "Nie udało się powiązać protokołu palet serwisu Lospec.",
+  "REDDIT": "Reddit",
+  "GITHUB": "GitHub",
+  "YOUTUBE": "YouTube",
+  "DONATE": "Dotacja",
+  "YES": "Tak",
+  "NO": "Nie",
+  "CANCEL": "Anuluj",
+  "UNNAMED": "Bez nazwy",
+  "OPEN_COMMAND_DEBUG_WINDOW": "Otwórz okno debugowania komend",
+  "DELETE": "Usuń",
+  "USER_PREFS": "Preferencje użytkownika (Roaming)",
+  "SHORTCUT_FILE": "Plik skrótów klawiszowych (Roaming)",
+  "EDITOR_DATA": "Dane edytora (Local)",
+  "MOVE_VIEWPORT_TOOLTIP": "Przesuwa widok. ({0})",
+  "MOVE_VIEWPORT_ACTION_DISPLAY": "Kliknij i porusz myszką, aby przesunąć widok",
+  "MOVE_TOOL_TOOLTIP": "Przesuwa zaznaczone piksele ({0}). Przytrzymaj Ctrl aby ruszyć wszystkimi warstwami.",
+  "MOVE_TOOL_ACTION_DISPLAY": "Przytrzymaj myszkę, aby poruszyć zaznaczone piksele. Przytrzymaj Ctrl aby ruszyć wszystkimi warstwami.",
+  "PEN_TOOL_TOOLTIP": "Pióro. ({0})",
+  "PEN_TOOL_ACTION_DISPLAY": "Kliknij i porusz aby rysować.",
+  "PIXEL_PERFECT_SETTING": "Pixel perfect",
+  "RECTANGLE_TOOL_TOOLTIP": "Tworzy prostokąt na płótnie ({0}). Przytrzymaj Shift, żeby narysować kwadrat.",
+  "RECTANGLE_TOOL_ACTION_DISPLAY_DEFAULT": "Tworzy prostokąt na płótnie. Przytrzymaj Shift, żeby narysować kwadrat.",
+  "RECTANGLE_TOOL_ACTION_DISPLAY_SHIFT": "Kliknij i porusz aby narysować kwadrat.",
+  "KEEP_ORIGINAL_IMAGE_SETTING": "Zachowaj oryginalny obraz",
+  "ROTATE_VIEWPORT_TOOLTIP": "Obraca widok. ({0})",
+  "ROTATE_VIEWPORT_ACTION_DISPLAY": "Kliknij i porusz aby obrócić widok",
+  "SELECT_TOOL_TOOLTIP": "Zaznacza obszar. ({0})",
+  "SELECT_TOOL_ACTION_DISPLAY_DEFAULT": "Kliknij i porusz aby zaznaczyć obszar. Przytrzymaj Shift aby dodać do istniejącego zaznaczenia. Przytrzymaj Ctrl aby usunąć z niego.",
+  "SELECT_TOOL_ACTION_DISPLAY_SHIFT": "Kliknij i porusz aby dodać do aktywnego zaznaczenia.",
+  "SELECT_TOOL_ACTION_DISPLAY_CTRL": "Kliknij i porusz myszką aby usunąć z aktywnego zaznaczenia.",
+  "ZOOM_TOOL_TOOLTIP": "Przybliża lub oddala widok ({0}). Kliknij aby przybliżyć, przytrzymaj ctrl i kliknij aby oddalić.",
+  "ZOOM_TOOL_ACTION_DISPLAY_DEFAULT": "Kliknij i porusz myszką, aby zmienić oddalenie. Kliknij aby przybliżyć, przytrzymaj ctrl i kliknij aby oddalić.",
+  "ZOOM_TOOL_ACTION_DISPLAY_CTRL": "Kliknij i porusz myszką aby zmienić oddalenie. Kliknij aby oddalić, puść ctrl i kliknij aby przybliżyć.",
+  "BRIGHTNESS_TOOL_TOOLTIP": "Rozjaśnia lub przyciemnia piksele ({0}). Przytrzymaj Ctrl aby przyciemniać.",
+  "BRIGHTNESS_TOOL_ACTION_DISPLAY_DEFAULT": "Rysuj po pikselach aby je rozjaśnić. Przytrzymaj Ctrl aby przyciemnić.",
+  "BRIGHTNESS_TOOL_ACTION_DISPLAY_CTRL": "Rysuj po pikselach aby je przyciemnić. Puść Ctrl aby rozjaśnić.",
+  "COLOR_PICKER_TOOLTIP": "Pobiera kolor z płótna i ustawia go jako pierwszorzędny. ({0})",
+  "COLOR_PICKER_ACTION_DISPLAY_DEFAULT": "Kliknij aby pobrać kolor. Przytrzymaj Ctrl aby ukryć płótno. Przytrzymaj Shift aby ukryć warstwę referencyjną.",
+  "COLOR_PICKER_ACTION_DISPLAY_CTRL": "Kliknij aby pobrać kolory z warstwy referencyjnej.",
+  "COLOR_PICKER_ACTION_DISPLAY_SHIFT": "Kliknij aby pobrać kolory z płótna.",
+  "ELLIPSE_TOOL_TOOLTIP": "Rysuje elipsę na płótnie ({0}). Przytrzymaj Shift aby narysować koło.",
+  "ELLIPSE_TOOL_ACTION_DISPLAY_DEFAULT": "Kliknij i porusz myszką aby narysować elipsę. Przytrzymaj Shift aby narysować koło.",
+  "ELLIPSE_TOOL_ACTION_DISPLAY_SHIFT": "Kliknij i porusz myszką aby narysować koło.",
+  "ERASER_TOOL_TOOLTIP": "Wymazuje piksele z płótna. ({0})",
+  "ERASER_TOOL_ACTION_DISPLAY": "Kliknij i porusz myszką aby zmazywać.",
+  "FLOOD_FILL_TOOL_TOOLTIP": "Zapełnia obszar kolorem. ({0})",
+  "FLOOD_FILL_TOOL_ACTION_DISPLAY_DEFAULT": "Kliknij na obszar, aby go zapełnić. Przytrzymaj Ctrl aby zapełniać z uwzględnieniem wszystkich warstw.",
+  "FLOOD_FILL_TOOL_ACTION_DISPLAY_CTRL": "Kliknij na obszar aby go zapełnić. Puść Ctrl żeby aby zapełniać tylko z uwzględnieniem aktywnej warstwy.",
+  "LASSO_TOOL_TOOLTIP": "Lasso. ({0})",
+  "LASSO_TOOL_ACTION_DISPLAY_DEFAULT": "Kliknij i porusz myszką aby zaznaczyć piksele wewnątrz lassa. Przytrzymaj Shift aby dodać do istniejącego zaznaczenia. Przytrzymaj Ctrl aby usunąć z niego.",
+  "LASSO_TOOL_ACTION_DISPLAY_SHIFT": "Kliknij i porusz myszką aby dodać piksele wewnątrz lassa do zaznaczenia.",
+  "LASSO_TOOL_ACTION_DISPLAY_CTRL": "Kliknij i porusz myszką, aby usunąć piksele wewnątrz lassa z aktywnego zaznaczenia.",
+  "LINE_TOOL_TOOLTIP": "Rysuje linie na płótnie ({0}). Przytrzymaj Shift aby rysować równe linie.",
+  "LINE_TOOL_ACTION_DISPLAY_DEFAULT": "Kliknij i porusz myszką aby narysować linie. Przytrzymaj Shify aby rysować równe linie.",
+  "LINE_TOOL_ACTION_DISPLAY_SHIFT": "Kliknij i porusz myszką aby rysować równe linie.",
+  "MAGIC_WAND_TOOL_TOOLTIP": "Magiczna Różdżka ({0}). Zapełnia obszar zaznaczeniem",
+  "MAGIC_WAND_ACTION_DISPLAY": "Kliknij aby stworzyć zaznaczenie z obszaru.",
+  "PEN_TOOL": "Pióro",
+  "BRIGHTNESS_TOOL": "Rozświetlenie",
+  "COLOR_PICKER_TOOL": "Próbnik Kolorów",
+  "ELLIPSE_TOOL": "Elipsa",
+  "ERASER_TOOL": "Gumka",
+  "FLOOD_FILL_TOOL": "Wiadro",
+  "LASSO_TOOL": "Lasso",
+  "LINE_TOOL": "Linia",
+  "MAGIC_WAND_TOOL": "Magiczna Różdżka",
+  "MOVE_TOOL": "Przesuwanie",
+  "MOVE_VIEWPORT_TOOL": "Przesuwanie widoku",
+  "RECTANGLE_TOOL": "Prostokąt",
+  "ROTATE_VIEWPORT_TOOL": "Obracanie widoku",
+  "SELECT_TOOL_NAME": "Zaznaczenie",
+  "ZOOM_TOOL": "Powiększenie",
+  "SHAPE_LABEL": "Kształt",
+  "MODE_LABEL": "Tryb",
+  "SCOPE_LABEL": "Zakres",
+  "FILL_SHAPE_LABEL": "Zapełnij kształt",
+  "FILL_COLOR_LABEL": "Kolor zapełnienia",
+  "TOOL_SIZE_LABEL": "Wielkość narzędzia",
+  "STRENGTH_LABEL": "Siła",
+  "NEW": "Nowy",
+  "ADD": "Dodaj",
+  "SUBTRACT": "Odejmij",
+  "INTERSECT": "Intersekcja",
+  "RECTANGLE": "Prostokąt",
+  "CIRCLE": "Koło",
+  "ABOUT": "O nas",
+  "MINIMIZE": "Minimializuj",
+  "RESTORE": "Przywróć",
+  "MAXIMIZE": "Maksymalizuj",
+  "CLOSE": "Zamknij",
+  "EXPORT_SIZE_HINT": "Jeżeli chcesz udostępnić obraz, spróbuj {0}% dla najlepszej przejrzystości",
+  "CREATE": "Stwórz",
+  "BASE_LAYER_NAME": "Podstawowa warstwa",
+  "ENABLE_MASK": "Włącz maskę",
+  "SELECTED_AREA_EMPTY": "Zaznaczony obszar jest pusty",
+  "NOTHING_TO_COPY": "Nie ma nic do skopiowania",
+  "REFERENCE_LAYER_PATH": "Ścieżka warstwy referencyjnej",
+  "CLIP_LAYER_BELOW": "Dopasuj do warstwy niżej",
+  "FLIP": "Przerzuć",
+  "ROTATION": "Obrót",
+  "ROT_IMG_90_D": "Obróć obraz 90°",
+  "ROT_IMG_180_D": "Obróć obraz 180°",
+  "ROT_IMG_-90_D": "Obróć obraz -90°",
+  "ROT_LAYERS_90_D": "Obróć zaznaczone warstwy 90°",
+  "ROT_LAYERS_180_D": "Obróć zaznaczone warstwy 180°",
+  "ROT_LAYERS_-90_D": "Obróć zaznaczone warstwy -90°",
+  "UNNAMED_PALETTE": "Paleta bez nazwy",
+  "CLICK_SELECT_PRIMARY": "Kilknij aby ustawić jako kolor główny.",
+  "PEN_MODE": "Tryb pióra",
+  "VIEW": "Widok",
+  "HORIZONTAL_LINE_SYMMETRY": "Pozioma oś symetrii",
+  "VERTICAL_LINE_SYMMETRY": "Pionowa oś symetrii",
+  "COLOR_PICKER_TITLE": "Próbnik Kolorów",
+  "COLOR_SLIDERS_TITLE": "Suwaki Kolorów",
+  "PALETTE_TITLE": "Paleta Kolorów",
+  "SWATCHES_TITLE": "Swatche",
+  "LAYERS_TITLE": "Warstwy",
+  "NAVIGATION_TITLE": "Nawigacja",
+  "NORMAL_BLEND_MODE": "Normalny",
+  "DARKEN_BLEND_MODE": "Przyciemnianie",
+  "MULTIPLY_BLEND_MODE": "Mnożenie",
+  "COLOR_BURN_BLEND_MODE": "Wypalenie koloru",
+  "LIGHTEN_BLEND_MODE": "Rozjaśnianie",
+  "SCREEN_BLEND_MODE": "Przesiewanie",
+  "COLOR_DODGE_BLEND_MODE": "Unikanie",
+  "OVERLAY_BLEND_MODE": "Narzuta",
+  "SOFT_LIGHT_BLEND_MODE": "Miękkie światło",
+  "HARD_LIGHT_BLEND_MODE": "Twarde światło",
+  "DIFFERENCE_BLEND_MODE": "Różnica",
+  "EXCLUSION_BLEND_MODE": "Wykluczenie",
+  "HUE_BLEND_MODE": "Odcień",
+  "SATURATION_BLEND_MODE": "Nasycenie",
+  "LUMINOSITY_BLEND_MODE": "Jasność",
+  "COLOR_BLEND_MODE": "Kolor",
+  "NOT_SUPPORTED_BLEND_MODE": "Nie wspierany",
+  "RESTART": "Restartuj",
+  "SORT_BY": "Sortuj po",
+  "NAME": "Nazwa",
+  "COLORS": "Kolory",
+  "DEFAULT": "Domyślnie",
+  "ALPHABETICAL": "Alfabetycznie",
+  "COLOR_COUNT": "Ilość kolorów",
+  "ANY": "Obojętnie",
+  "MAX": "Maksymalnie",
+  "MIN": "Minimalnie",
+  "EXACT": "Dokładnie",
+  "ASCENDING": "Rosnąco",
+  "DESCENDING": "Malejąco",
+  "NAME_IS_TOO_LONG": "Nazwa jest za długa",
+  "STOP_IT_TEXT1": "Wystarczy. Posprzątaj nazwy swoich plików.",
+  "STOP_IT_TEXT2": "Czy możesz proszę przestać kopiować te nazwy?",
+  "REPLACER_TOOLTIP": "Kliknij prawym przyciskiem myszy na kolor i wybierz 'Zamień' lub przeciągnij go tutaj.",
+  "CLICK_TO_CHOOSE_COLOR": "Kliknij aby wybrać kolor",
+  "REPLACE_COLOR": "Zastąp kolor",
+  "PALETTE_COLOR_TOOLTIP": "Kliknij aby ustawić jako główny kolor. Przeciągnij i upuść na inny kolor z palety, aby zamienić je miejscami.",
+  "ADD_FROM_SWATCHES": "Dodaj ze swatchy",
+  "ADD_COLOR_TO_PALETTE": "Dodaj kolor do palety",
+  "USE_IN_CURRENT_IMAGE": "Użyj w aktywnym obrazie",
+  "ADD_TO_FAVORITES": "Dodaj do ulubionych",
+  "BROWSE_PALETTES": "Przeglądaj palety",
+  "LOAD_PALETTE": "Załaduj paletę",
+  "SAVE_PALETTE": "Zapisz paletę",
+  "FAVORITES": "Ulubione",
+  "ADD_FROM_CURRENT_PALETTE": "Dodaj z aktywnej palety",
+  "OPEN_PALETTES_DIR_TOOLTIP": "Otwórz folder z paletami w explorerze",
+  "BROWSE_ON_LOSPEC_TOOLTIP": "Przeglądaj palety w serwisie Lospec",
+  "IMPORT_FROM_FILE_TOOLTIP": "Importuj z pliku",
+  "TOP_LEFT": "Lewy górny",
+  "TOP_CENTER": "Środkowy górny",
+  "TOP_RIGHT": "Prawy górny",
+  "MIDDLE_LEFT": "Lewy środkowy",
+  "MIDDLE_CENTER": "Środek",
+  "MIDDLE_RIGHT": "Prawy środkowy",
+  "BOTTOM_LEFT": "Lewy dolny",
+  "BOTTOM_CENTER": "Środkowy dolny",
+  "BOTTOM_RIGHT": "Prawy dolny",
+  "CLIP_TO_BELOW": "Dopasuj do warstwy niżej",
+  "MOVE_UPWARDS": "Przesuń w górę",
+  "MOVE_DOWNWARDS": "Przesuń w dół",
+  "MERGE_SELECTED": "Scal zaznaczone",
+  "LOCK_TRANSPARENCY": "Zablokuj przeźroczystość",
+  "COULD_NOT_LOAD_PALETTE": "Nie udało się pobrać palet kolorów",
+  "NO_PALETTES_FOUND": "Nie znaleziono żadnych palet.",
+  "LOSPEC_LINK_TEXT": "Słyszałem, że trochę możesz znaleźć tutaj: lospec.com/palette-list",
+  "PALETTE_BROWSER": "Przeglądarka Palet",
+  "DELETE_PALETTE_CONFIRMATION": "Czy jesteś pewien, że chcesz usunąć tę paletę? Tej akcji nie można cofnąć.",
+  "SHORTCUTS_IMPORTED": "Skróty klawiszowe z {0} zostały zaimportowane poprawnie.",
+  "SHORTCUT_PROVIDER_DETECTED": "Zauważyliśmy, że masz zainstalowane {0}. Czy chcesz zaimportować skróty z tego programu?",
+  "IMPORT_FROM_INSTALLATION": "Importuj z instalacji",
+  "IMPORT_INSTALLATION_OPTION1": "Importuj z instalacji",
+  "IMPORT_INSTALLATION_OPTION2": "Użyj domyślnych",
+  "IMPORT_FROM_TEMPLATE": "Importuj z szablonu",
+  "SHORTCUTS_IMPORTED_SUCCESS": "Skróty klawiszowe zostały zaimportowane poprawnie.",
+  "WARNING_RESET_SHORTCUTS_DEFAULT": "Czy na pewno chcesz zresetować skróty klawiszowe do ich domyślnych wartości?",
+  "SUCCESS": "Sukces",
+  "WARNING": "Ostrzeżenie",
+  "ERROR_IMPORTING_IMAGE": "Wystąpił błąd podczas importowania obrazu.",
+  "SHORTCUTS_CORRUPTED_TITLE": "Popsuty plik ze skrótami klawiszowymi",
+  "SHORTCUTS_CORRUPTED": "Plik skrótów klawiszowych jest popsuty, zresetowano do domyślnych wartości.",
+  "FAILED_DOWNLOAD_PALETTE": "Nie udało się pobrać palety",
+  "FILE_INCORRECT_FORMAT": "Plik nie jest w poprawnym formacie",
+  "INVALID_FILE": "Nieprawidłowy plik",
+  "SHORTCUTS_FILE_INCORRECT_FORMAT": "Plik ze skrótami klawiszowymi jest w niepoprawnym formacie",
+  "UNSUPPORTED_FILE_FORMAT": "Ten format plików nie jest wspierany",
+  "ALREADY_ASSIGNED": "Już przypisano",
+  "REPLACE": "Zastąp",
+  "SWAP": "Zamień",
+  "SHORTCUT_ALREADY_ASSIGNED_SWAP": "Ten skrót został już przypisany do '{0}'\nChcesz go zastąpić czy je zamienić?",
+  "SHORTCUT_ALREADY_ASSIGNED_OVERWRITE": "Ten skrót jest już przypisany do '{0}' \nCzy chcesz go zastąpić?",
+  "UNSAVED_CHANGES": "Niezapisane zmiany.",
+  "DOCUMENT_MODIFIED_SAVE": "Ten dokument został zmodyfikowany. Czy chcesz go zapisać?",
+  "SESSION_UNSAVED_DATA": "{0} ma niezapisane zmiany. Jesteś pewien?",
+  "PROJECT_MAINTAINERS": "Opiekunowie Projektu",
+  "OTHER_AWESOME_CONTRIBUTORS": "I inni świetni współtwórcy",
+  "HELP": "Pomoc",
+  "ALL LAYERS": "Wszystkie warstwy",
+  "SINGLE LAYER": "Pojedyńcza warstwa",
+  "STOP_IT_TEXT3": "Nie no, serio, przestań.",
+  "STOP_IT_TEXT4": "Nie masz nic lepszego do roboty?",
+  "LINEAR_DODGE_BLEND_MODE": "Liniowe unikanie (Dodatnie)",
+  "PRESS_ANY_KEY": "Wciśnij klawisz",
+  "NONE_SHORTCUT": "Brak",
+  "REFERENCE": "Referencja",
+  "PUT_REFERENCE_LAYER_ABOVE": "Daj warstwę referencyjną do góry",
+  "PUT_REFERENCE_LAYER_BELOW": "Daj warstwę referencyjną w dół",
+  "TOGGLE_VERTICAL_SYMMETRY": "Przełącz pionową oś symetrii",
+  "TOGGLE_HORIZONTAL_SYMMETRY": "Przełącz poziomą oś symetrii",
+  "RESET_VIEWPORT": "Resetuj widok",
+  "VIEWPORT_SETTINGS": "Ustawienia widoku",
+  "MOVE_TOOL_ACTION_DISPLAY_TRANSFORMING": "Kliknij i przytrzymaj myszkę, aby poruszyć piksele w zaznaczonej warstwie.",
+  "MOVE_TOOL_ACTION_DISPLAY_CTRL": "Przytrzymaj myszkę aby poruszyć wszystkie warstwy."
+}

+ 2 - 0
src/PixiEditor/Data/Localization/Languages/ru.json

@@ -0,0 +1,2 @@
+{
+}

+ 2 - 0
src/PixiEditor/Data/Localization/Languages/uk.json

@@ -0,0 +1,2 @@
+{
+}

+ 54 - 0
src/PixiEditor/Data/Localization/LocalizationData.json

@@ -0,0 +1,54 @@
+{
+  "$schema": "./LocalizationDataSchema.json",
+  "Languages": [
+    {
+      "name": "English",
+      "code": "en",
+      "localeFileName": "en.json",
+      "iconFileName": "en.png"
+    },
+    {
+      "name": "Polski",
+      "code": "pl",
+      "localeFileName": "pl.json",
+      "iconFileName": "pl.png"
+    },
+    {
+      "name": "Deutsch",
+      "code": "de",
+      "localeFileName": "de.json",
+      "iconFileName": "de.png"
+    },
+    {
+      "name": "Español",
+      "code": "es",
+      "localeFileName": "es.json",
+      "iconFileName": "es.png"
+    },
+    {
+      "name": "Русский",
+      "code": "ru",
+      "localeFileName": "ru.json",
+      "iconFileName": "ru.png"
+    },
+    {
+      "name": "Українська",
+      "code": "uk",
+      "localeFileName": "uk.json",
+      "iconFileName": "uk.png"
+    },
+    {
+      "name": "عربي",
+      "code": "ar",
+      "localeFileName": "ar.json",
+      "iconFileName": "ar.png",
+      "rightToLeft": true
+    },
+    {
+      "name": "Čeština",
+      "code": "cs",
+      "localeFileName": "cs.json",
+      "iconFileName": "cs.png"
+    }
+  ]
+}

+ 54 - 0
src/PixiEditor/Data/Localization/LocalizationDataSchema.json

@@ -0,0 +1,54 @@
+{
+  "type": "object",
+  "additionalProperties": false,
+  "properties": {
+    "Languages": {
+      "type": "array",
+      "items": {
+        "type": "object",
+        "properties": {
+          "name": {
+            "type": "string",
+            "description": "The localized name of the language"
+          },
+          "code": {
+            "type": "string",
+            "description": "The code associated with the language",
+            "minLength": 2
+          },
+          "localeFileName": {
+            "type": "string",
+            "description": "The name of the key-value json file found in Data/Localization/Languages",
+            "pattern": ".*\\.json",
+            "format": "uri",
+            "default": ".json"
+          },
+          "iconFileName": {
+            "type": "string",
+            "description": "The name of the png icon for the language found in Images/LanguageFlags",
+            "pattern": ".*\\.png",
+            "format": "uri",
+            "default": ".png"
+          },
+          "rightToLeft": {
+            "type": "boolean",
+            "description": "Does the language use RTL Layout",
+            "default": true
+          }
+        },
+        "required": [
+          "name",
+          "code",
+          "localeFileName",
+          "iconFileName"
+        ]
+      }
+    },
+    "$schema": {
+      "type": "string"
+    }
+  },
+  "required": [
+    "Languages"
+  ]
+}

+ 2 - 1
src/PixiEditor/Helpers/Converters/BlendModeToStringConverter.cs

@@ -1,6 +1,7 @@
 using System.Globalization;
 using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.Helpers.Extensions;
+using PixiEditor.Localization;
 
 namespace PixiEditor.Helpers.Converters;
 internal class BlendModeToStringConverter : SingleInstanceConverter<BlendModeToStringConverter>
@@ -9,7 +10,7 @@ internal class BlendModeToStringConverter : SingleInstanceConverter<BlendModeToS
     {
         if (value is not BlendMode mode)
             return "<null>";
-        return mode.EnglishName();
+        return new LocalizedString(mode.LocalizedKeys()).Value;
     }
 
     public override object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)

+ 14 - 3
src/PixiEditor/Helpers/Converters/BoolToValueConverter.cs

@@ -1,4 +1,5 @@
 using System.Globalization;
+using PixiEditor.Localization;
 
 namespace PixiEditor.Helpers.Converters;
 
@@ -10,12 +11,22 @@ internal class BoolToValueConverter : MarkupConverter
     
     public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
     {
-        if (value is bool boolean && boolean)
+        if (value is bool and true)
         {
-            return TrueValue;
+            return GetValue(TrueValue);
         }
 
-        return FalseValue;
+        return GetValue(FalseValue);
+    }
+
+    private object GetValue(object value)
+    {
+        if (value is string s && s.StartsWith("localized:"))
+        {
+            return new LocalizedString(s.Split("localized:")[1]);
+        }
+
+        return value;
     }
 
     public override object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)

+ 3 - 2
src/PixiEditor/Helpers/Converters/EnumToStringConverter.cs

@@ -1,5 +1,6 @@
 using PixiEditor.Models.Enums;
 using System;
+using PixiEditor.Localization;
 
 namespace PixiEditor.Helpers.Converters;
 
@@ -16,7 +17,7 @@ internal class EnumToStringConverter : SingleInstanceConverter<EnumToStringConve
                 if (valueCasted == SizeUnit.Percentage)
                     return "%";
 
-                return "px";
+                return "PIXEL_UNIT";
             }
             return Enum.GetName((value.GetType()), value);
         }
@@ -25,4 +26,4 @@ internal class EnumToStringConverter : SingleInstanceConverter<EnumToStringConve
             return string.Empty;
         }
     }
-}
+}

+ 13 - 14
src/PixiEditor/Helpers/Converters/KeyToStringConverter.cs

@@ -1,24 +1,23 @@
 using System.Globalization;
 using System.Windows.Input;
+using PixiEditor.Localization;
 
 namespace PixiEditor.Helpers.Converters;
 
 internal class KeyToStringConverter
     : SingleInstanceConverter<KeyToStringConverter>
 {
-    public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
-    {
-        if (value is Key key)
+    public override object Convert(object value, Type targetType, object parameter, CultureInfo culture) =>
+        value switch
         {
-            return InputKeyHelpers.GetKeyboardKey(key);
-        }
-        else if (value is ModifierKeys)
-        {
-            return value.ToString();
-        }
-        else
-        {
-            return string.Empty;
-        }
-    }
+            Key key => (object)InputKeyHelpers.GetKeyboardKey(key),
+            ModifierKeys modifier => modifier switch
+            {
+                ModifierKeys.Control => new LocalizedString("CTRL_KEY"),
+                ModifierKeys.Shift => new LocalizedString("SHIFT_KEY"),
+                ModifierKeys.Alt => new LocalizedString("ALT_KEY"),
+                _ => modifier.ToString()
+            },
+            _ => string.Empty
+        };
 }

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

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

+ 24 - 0
src/PixiEditor/Helpers/Converters/SubtractConverter.cs

@@ -0,0 +1,24 @@
+using System.Globalization;
+
+namespace PixiEditor.Helpers.Converters;
+
+internal class SubtractConverter : SingleInstanceConverter<SubtractConverter>
+{
+    public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+    {
+        object parsedValue = value is string stringValue ? double.Parse(stringValue) : value;
+        object parsedParameter = parameter is string parameterString ? double.Parse(parameterString) : parameter;
+        
+        if (parsedValue is not double doubleValue)
+        {
+            return value;
+        }
+
+        if (parsedParameter is not double doubleParameter)
+        {
+            return value;
+        }
+
+        return doubleValue - doubleParameter;
+    }
+}

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

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

+ 20 - 19
src/PixiEditor/Helpers/Extensions/BlendModeEx.cs

@@ -1,30 +1,31 @@
 using PixiEditor.ChangeableDocument.Enums;
+using PixiEditor.Localization;
 
 namespace PixiEditor.Helpers.Extensions;
 internal static class BlendModeEx
 {
-    public static string EnglishName(this BlendMode mode)
+    public static string LocalizedKeys(this BlendMode mode)
     {
         return mode switch
         {
-            BlendMode.Normal => "Normal",
-            BlendMode.Darken => "Darken",
-            BlendMode.Multiply => "Multiply",
-            BlendMode.ColorBurn => "Color Burn",
-            BlendMode.Lighten => "Lighten",
-            BlendMode.Screen => "Screen",
-            BlendMode.ColorDodge => "Color Dodge",
-            BlendMode.LinearDodge => "Linear Dodge (Add)",
-            BlendMode.Overlay => "Overlay",
-            BlendMode.SoftLight => "Soft Light",
-            BlendMode.HardLight => "Hard Light",
-            BlendMode.Difference => "Difference",
-            BlendMode.Exclusion => "Exclusion",
-            BlendMode.Hue => "Hue",
-            BlendMode.Saturation => "Saturation",
-            BlendMode.Luminosity => "Luminosity",
-            BlendMode.Color => "Color",
-            _ => "<no name>",
+            BlendMode.Normal => "NORMAL_BLEND_MODE",
+            BlendMode.Darken => "DARKEN_BLEND_MODE",
+            BlendMode.Multiply => "MULTIPLY_BLEND_MODE",
+            BlendMode.ColorBurn => "COLOR_BURN_BLEND_MODE",
+            BlendMode.Lighten => "LIGHTEN_BLEND_MODE",
+            BlendMode.Screen => "SCREEN_BLEND_MODE",
+            BlendMode.ColorDodge => "COLOR_DODGE_BLEND_MODE",
+            BlendMode.LinearDodge => "LINEAR_DODGE_BLEND_MODE",
+            BlendMode.Overlay => "OVERLAY_BLEND_MODE",
+            BlendMode.SoftLight => "SOFT_LIGHT_BLEND_MODE",
+            BlendMode.HardLight => "HARD_LIGHT_BLEND_MODE",
+            BlendMode.Difference => "DIFFERENCE_BLEND_MODE",
+            BlendMode.Exclusion => "EXCLUSION_BLEND_MODE",
+            BlendMode.Hue => "HUE_BLEND_MODE",
+            BlendMode.Saturation => "SATURATION_BLEND_MODE",
+            BlendMode.Luminosity => "LUMINOSITY_BLEND_MODE",
+            BlendMode.Color => "COLOR_BLEND_MODE",
+            _ => "NOT_SUPPORTED_BLEND_MODE"
         };
     }
 }

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

@@ -1,4 +1,5 @@
 using Microsoft.Extensions.DependencyInjection;
+using PixiEditor.Localization;
 using PixiEditor.Models.Commands;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.DataProviders;
@@ -17,11 +18,12 @@ namespace PixiEditor.Helpers.Extensions;
 internal static class ServiceCollectionHelpers
 {
     /// <summary>
-    /// Add's all the services required to fully run PixiEditor's MainWindow
+    /// Adds all the services required to fully run PixiEditor's MainWindow
     /// </summary>
     public static IServiceCollection AddPixiEditor(this IServiceCollection collection) => collection
         .AddSingleton<ViewModelMain>()
         .AddSingleton<IPreferences, PreferencesSettings>()
+        .AddSingleton<ILocalizationProvider, LocalizationProvider>()
         // View Models
         .AddSingleton<StylusViewModel>()
         .AddSingleton<WindowViewModel>()

+ 43 - 13
src/PixiEditor/Helpers/InputKeyHelpers.cs

@@ -7,35 +7,43 @@ namespace PixiEditor.Helpers;
 
 internal static class InputKeyHelpers
 {
+    const string Russian = "00000419";
+    const string Ukrainian = "00000422";
+    const string UkrainianEnhanced = "00020422";
+    const string Arabic1 = "00000401";
+    const string Arabic2 = "00010401";
+    const string Arabic3 = "00020401";
+    private const string InvariantLayoutCode = "00000409"; // Also known as the US Layout
+
+    private static nint? invariantLayout;
+    
     /// <summary>
     /// Returns the charcter of the <paramref name="key"/> mapped to the users keyboard layout
     /// </summary>
-    public static string GetKeyboardKey(Key key) => GetKeyboardKey(key, CultureInfo.CurrentCulture);
-
-    public static string GetKeyboardKey(Key key, CultureInfo culture) => key switch
+    public static string GetKeyboardKey(Key key, bool forceInvariant = false) => key switch
     {
-        >= Key.NumPad0 and <= Key.Divide => $"Num {GetMappedKey(key, culture)}",
+        >= Key.NumPad0 and <= Key.Divide => $"Num {GetMappedKey(key, forceInvariant)}",
         Key.Space => nameof(Key.Space),
         Key.Tab => nameof(Key.Tab),
         Key.Return => "Enter",
         Key.Back => "Backspace",
         Key.Escape => "Esc",
-        _ => GetMappedKey(key, culture),
+        _ => GetMappedKey(key, forceInvariant),
     };
 
-    private static string GetMappedKey(Key key, CultureInfo culture)
+    private static string GetMappedKey(Key key, bool forceInvariant)
     {
         int virtualKey = KeyInterop.VirtualKeyFromKey(key);
         byte[] keyboardState = new byte[256];
 
-        uint scanCode = Win32.MapVirtualKeyExW((uint)virtualKey, Win32.MapType.MAPVK_VK_TO_VSC, culture.KeyboardLayoutId);
-        StringBuilder stringBuilder = new(3);
-
-        int result = Win32.ToUnicode((uint)virtualKey, scanCode, keyboardState, stringBuilder, stringBuilder.Capacity, 0);
+        nint targetLayout = GetLayoutHkl(forceInvariant);
+        
+        uint scanCode = Win32.MapVirtualKeyExW((uint)virtualKey, Win32.MapType.MAPVK_VK_TO_VSC, targetLayout);
+        
+        StringBuilder stringBuilder = new(5);
+        int result = Win32.ToUnicodeEx((uint)virtualKey, scanCode, keyboardState, stringBuilder, stringBuilder.Capacity, 0, targetLayout);
 
-        string stringResult;
-
-        stringResult = result switch
+        string stringResult = result switch
         {
             0 => key.ToString(),
             -1 => stringBuilder.ToString().ToUpper(),
@@ -44,4 +52,26 @@ internal static class InputKeyHelpers
 
         return stringResult;
     }
+
+    private static nint GetLayoutHkl(bool forceInvariant = false)
+    {
+        if (forceInvariant)
+        {
+            invariantLayout ??= Win32.LoadKeyboardLayoutA(InvariantLayoutCode, 1);
+            return invariantLayout.Value;
+        }
+        
+        var builder = new StringBuilder(8);
+        bool success = Win32.GetKeyboardLayoutNameW(builder);
+
+        // Fallback to US layout for certain layouts. Do not prepend a 0x and make sure the string is 8 chars long
+        // Layouts can be found here https://learn.microsoft.com/en-us/windows-hardware/manufacture/desktop/windows-language-pack-default-values?view=windows-11
+        if (!success || builder.ToString() is not (Russian or Ukrainian or UkrainianEnhanced or Arabic1 or Arabic2 or Arabic3))
+        {
+            return Win32.GetKeyboardLayout(0);
+        }
+
+        invariantLayout ??= Win32.LoadKeyboardLayoutA(InvariantLayoutCode, 1);
+        return invariantLayout.Value;
+    }
 }

+ 45 - 0
src/PixiEditor/Helpers/LocalizationExtension.cs

@@ -0,0 +1,45 @@
+using System.Windows;
+using System.Windows.Data;
+using System.Windows.Markup;
+
+namespace PixiEditor.Helpers;
+
+public class LocalizationExtension : MarkupExtension
+{
+    private LocalizationExtensionToProvide toProvide;
+    private static Binding flowDirectionBinding;
+
+    public LocalizationExtension(LocalizationExtensionToProvide toProvide)
+    {
+        
+    }
+    
+    public override object ProvideValue(IServiceProvider serviceProvider)
+    {
+        switch (toProvide)
+        {
+            case LocalizationExtensionToProvide.FlowDirection:
+                return GetFlowDirectionBinding(serviceProvider);
+        }
+
+        throw new NotImplementedException();
+    }
+
+    private object GetFlowDirectionBinding(IServiceProvider serviceProvider)
+    {
+        flowDirectionBinding = new Binding("CurrentLanguage.FlowDirection");
+        flowDirectionBinding.Source = ViewModelMain.Current.LocalizationProvider;
+        flowDirectionBinding.Mode = BindingMode.OneWay;
+
+        var expression = (BindingExpression)flowDirectionBinding.ProvideValue(serviceProvider);
+
+        ViewModelMain.Current.LocalizationProvider.OnLanguageChanged += _ => expression.UpdateTarget();
+
+        return expression;
+    }
+}
+
+public enum LocalizationExtensionToProvide
+{
+    FlowDirection
+}

+ 39 - 0
src/PixiEditor/Helpers/RegistryHelpers.cs

@@ -0,0 +1,39 @@
+using System.Diagnostics;
+using System.Security.AccessControl;
+using System.Windows;
+using Microsoft.Win32;
+using PixiEditor.Localization;
+using PixiEditor.Models.Dialogs;
+
+namespace PixiEditor.Helpers;
+
+public static class RegistryHelpers
+{
+    public static bool IsKeyPresentInRoot(string keyName)
+    {
+        using var key = Registry.ClassesRoot.OpenSubKey(keyName, RegistryRights.ReadKey);
+        return key != null;
+    }
+
+    public static bool TryAssociate(Action associationMethod, LocalizedString errorMessage)
+    {
+        try
+        {
+            if (!ProcessHelper.IsRunningAsAdministrator())
+            {
+                ProcessHelper.RunAsAdmin(Process.GetCurrentProcess().MainModule?.FileName);
+                Application.Current.Shutdown();
+            }
+            else
+            {
+                associationMethod();
+            }
+        }
+        catch
+        {
+            NoticeDialog.Show(errorMessage, "ERROR");
+        }
+
+        return false;
+    }
+}

+ 24 - 1
src/PixiEditor/Helpers/Win32.cs

@@ -159,11 +159,34 @@ internal class Win32
         int cchBuff,
         uint wFlags);
 
+    [DllImport("user32.dll")]
+    public static extern nint GetKeyboardLayout(
+        uint idThread);
+
+    [DllImport("user32.dll")]
+    public static extern bool GetKeyboardLayoutNameW(
+        [Out, MarshalAs(UnmanagedType.LPWStr, SizeParamIndex = 4)]
+        StringBuilder klid);
+    
+    [DllImport("user32.dll")]
+    public static extern int ToUnicodeEx(
+        uint wVirtKey,
+        uint wScanCode,
+        byte[] lpKeyState,
+        [Out, MarshalAs(UnmanagedType.LPWStr, SizeParamIndex = 4)]
+        StringBuilder pwszBuff,
+        int cchBuff,
+        uint wFlags,
+        nint dwhkl);
+
     [DllImport("user32.dll")]
     public static extern bool GetKeyboardState(byte[] lpKeyState);
 
     [DllImport("user32.dll")]
-    public static extern uint MapVirtualKeyExW(uint uCode, MapType uMapType, int hkl);
+    public static extern uint MapVirtualKeyExW(uint uCode, MapType uMapType, nint hkl);
+    
+    [DllImport("user32.dll")]
+    public static extern IntPtr LoadKeyboardLayoutA(string pwszKLID, uint Flags);
 
     [DllImport(
         "user32.dll",

BIN
src/PixiEditor/Images/LanguageFlags/ar.png


BIN
src/PixiEditor/Images/LanguageFlags/cs.png


BIN
src/PixiEditor/Images/LanguageFlags/de.png


BIN
src/PixiEditor/Images/LanguageFlags/en.png


BIN
src/PixiEditor/Images/LanguageFlags/es.png


BIN
src/PixiEditor/Images/LanguageFlags/pl.png


BIN
src/PixiEditor/Images/LanguageFlags/ru.png


BIN
src/PixiEditor/Images/LanguageFlags/uk.png


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

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

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

@@ -0,0 +1,37 @@
+using System.Diagnostics;
+using System.Windows;
+
+namespace PixiEditor.Localization;
+
+[DebuggerDisplay("{LanguageData.Name}, strings: {Locale.Count}")]
+public class Language
+{
+    private FlowDirection flowDirection;
+    
+    public LanguageData LanguageData { get; }
+    public IReadOnlyDictionary<string, string> Locale { get; }
+
+    public FlowDirection FlowDirection
+    {
+        get
+        {
+            if (ViewModelMain.Current.DebugSubViewModel.ForceOtherFlowDirection)
+            {
+                return flowDirection switch
+                {
+                    FlowDirection.RightToLeft => FlowDirection.LeftToRight,
+                    FlowDirection.LeftToRight => FlowDirection.RightToLeft
+                };
+            }
+
+            return flowDirection;
+        }
+    }
+    
+    public Language(LanguageData languageData, Dictionary<string, string> locale, bool isRightToLeft)
+    {
+        LanguageData = languageData;
+        Locale = locale;
+        flowDirection = isRightToLeft ? FlowDirection.RightToLeft : FlowDirection.LeftToRight;
+    }
+}

+ 18 - 0
src/PixiEditor/Localization/LanguageData.cs

@@ -0,0 +1,18 @@
+namespace PixiEditor.Localization;
+
+public class LanguageData
+{
+    public string Name { get; set; }
+    public string Code { get; set; }
+    public string LocaleFileName { get; set; }
+    
+    // https://icons8.com/icon/set/flags/color
+    public string IconFileName { get; set; }
+    public string IconPath => $"pack://application:,,,/PixiEditor;component/Images/LanguageFlags/{IconFileName}";
+    public bool RightToLeft { get; set; }
+    
+    public override string ToString()
+    {
+        return Name;
+    }
+}

+ 9 - 0
src/PixiEditor/Localization/LocalizationData.cs

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

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

@@ -0,0 +1,114 @@
+using System.IO;
+using PixiEditor.Models.UserPreferences;
+
+namespace PixiEditor.Localization;
+
+internal class LocalizationProvider : ILocalizationProvider
+{
+    private Language debugLanguage;
+    public string LocalizationDataPath { get; } = Path.Combine("Data", "Localization", "LocalizationData.json");
+    public LocalizationData LocalizationData { get; private set; }
+    public Language CurrentLanguage { get; set; }
+    public event Action<Language> OnLanguageChanged;
+    public void ReloadLanguage() => OnLanguageChanged?.Invoke(CurrentLanguage);
+
+    public Language DefaultLanguage { get; private set; }
+
+    public void LoadData()
+    {
+        Newtonsoft.Json.JsonSerializer serializer = new();
+        
+        if (!File.Exists(LocalizationDataPath))
+        {
+            throw new FileNotFoundException("Localization data file not found.", LocalizationDataPath);
+        }
+        
+        using StreamReader reader = new(LocalizationDataPath);
+        LocalizationData = serializer.Deserialize<LocalizationData>(new Newtonsoft.Json.JsonTextReader(reader));
+            
+        if (LocalizationData is null)
+        {
+            throw new InvalidDataException("Localization data is null.");
+        }
+        
+        if (LocalizationData.Languages is null || LocalizationData.Languages.Length == 0)
+        {
+            throw new InvalidDataException("Localization data does not contain any languages.");
+        }
+
+        DefaultLanguage = LoadLanguageInternal(LocalizationData.Languages[0]);
+        
+        string currentLanguageCode = IPreferences.Current.GetPreference<string>("LanguageCode");
+
+        int languageIndex = 0;
+        
+        for (int i = 0; i < LocalizationData.Languages.Length; i++)
+        {
+            if (LocalizationData.Languages[i].Code == currentLanguageCode)
+            {
+                languageIndex = i;
+                break;
+            }
+        }
+        
+        LoadLanguage(LocalizationData.Languages[languageIndex]);
+    }
+
+    public void LoadLanguage(LanguageData languageData)
+    {
+        if (languageData is null)
+        {
+            throw new ArgumentNullException(nameof(languageData));
+        }
+        
+        if(languageData.Code == CurrentLanguage?.LanguageData.Code)
+        {
+            return;
+        }
+        
+        bool firstLoad = CurrentLanguage is null;
+
+        CurrentLanguage = LoadLanguageInternal(languageData);
+
+        if (!firstLoad)
+        {
+            OnLanguageChanged?.Invoke(CurrentLanguage);
+        }
+    }
+
+    public void LoadDebugKeys(Dictionary<string, string> languageKeys, bool rightToLeft)
+    {
+        debugLanguage = new Language(
+            new LanguageData
+        {
+            Code = "debug",
+            Name = "Debug"
+        }, languageKeys, rightToLeft);
+
+        CurrentLanguage = debugLanguage;
+        
+        OnLanguageChanged?.Invoke(debugLanguage);
+    }
+
+    private Language LoadLanguageInternal(LanguageData languageData)
+    {
+        string localePath = Path.Combine("Data", "Localization", "Languages", languageData.LocaleFileName);
+
+        if (!File.Exists(localePath))
+        {
+            throw new FileNotFoundException("Locale file not found.", localePath);
+        }
+
+        Newtonsoft.Json.JsonSerializer serializer = new();
+        using StreamReader reader = new(localePath);
+        Dictionary<string, string> locale =
+            serializer.Deserialize<Dictionary<string, string>>(new Newtonsoft.Json.JsonTextReader(reader));
+
+        if (locale is null)
+        {
+            throw new InvalidDataException("Locale is null.");
+        }
+
+        return new(languageData, locale, languageData.RightToLeft);
+    }
+}

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

@@ -0,0 +1,112 @@
+using System.Text;
+using PixiEditor.Models.Enums;
+
+namespace PixiEditor.Localization;
+
+public struct LocalizedString
+{
+    private string key;
+
+    public string Key
+    {
+        get => key;
+        set
+        {
+            key = value;
+            #if DEBUG_LOCALIZATION
+            Value = key;
+            #else
+            Value = ViewModelMain.Current.DebugSubViewModel?.LocalizationKeyShowMode switch
+            {
+                LocalizationKeyShowMode.Key => Key,
+                LocalizationKeyShowMode.ValueKey => $"{GetValue(value)} ({Key})",
+                LocalizationKeyShowMode.LALALA => $"#~{GetLongString(GetValue(value).Count(x => x == ' ') + 1)}{Math.Abs(Key.GetHashCode()).ToString()[..2]}~#",
+                _ => GetValue(value)
+            };
+            #endif
+        }
+    }
+    public string Value { get; private set; }
+
+    public object[] Parameters { get; set; }
+
+    public LocalizedString(string key)
+    {
+        Key = key;
+    }
+
+    public LocalizedString(string key, params object[] parameters)
+    {
+        Parameters = parameters;
+        Key = key;
+    }
+
+    public override string ToString()
+    {
+        return Value;
+    }
+
+    private string GetValue(string localizationKey)
+    {
+        if (string.IsNullOrEmpty(localizationKey))
+        {
+            return localizationKey;
+        }
+        
+        ILocalizationProvider localizationProvider = ILocalizationProvider.Current;
+        if (localizationProvider?.LocalizationData == null)
+        {
+            return localizationKey;
+        }
+
+        if (!localizationProvider.CurrentLanguage.Locale.ContainsKey(localizationKey))
+        {
+            Language defaultLanguage = localizationProvider.DefaultLanguage;
+
+            if (localizationProvider.CurrentLanguage == defaultLanguage || !defaultLanguage.Locale.ContainsKey(localizationKey))
+            {
+                return localizationKey;
+            }
+
+            return ApplyParameters(defaultLanguage.Locale[localizationKey]);
+        }
+
+
+        return ApplyParameters(ILocalizationProvider.Current.CurrentLanguage.Locale[localizationKey]);
+    }
+
+    private string GetLongString(int length) => string.Join(' ', Enumerable.Repeat("LaLaLaLaLa", length));
+
+    private string ApplyParameters(string value)
+    {
+        if (Parameters == null || Parameters.Length == 0)
+        {
+            return value;
+        }
+
+        try
+        {
+            var executedParameters = new object[Parameters.Length];
+            for (var i = 0; i < Parameters.Length; i++)
+            {
+                var parameter = Parameters[i];
+                object objToExecute = parameter;
+                if (parameter is LocalizedString str)
+                {
+                    objToExecute = new LocalizedString(str.Key, str.Parameters).Value;
+                }
+
+                executedParameters[i] = objToExecute;
+            }
+
+            return string.Format(value, executedParameters);
+        }
+        catch (FormatException)
+        {
+            return value;
+        }
+    }
+
+    public static implicit operator LocalizedString(string key) => new(key);
+    public static implicit operator string(LocalizedString localizedString) => localizedString.Value;
+}

+ 4 - 4
src/PixiEditor/Models/Commands/Attributes/Commands/BasicAttribute.cs

@@ -13,8 +13,8 @@ internal partial class Command
         /// Create's a basic command which uses null as a paramter
         /// </summary>
         /// <param name="internalName">The internal name of the command</param>
-        /// <param name="displayName">A short description which is displayed in the the top menu, e.g. "Save as..."</param>
-        /// <param name="descriptiveName">A description which is displayed in the search bar, e.g. "Save image as new". Leave empty to hide it from the search bar</param>
+        /// <param name="displayName">A short description which is displayed in the the top menu, e.g. "Save as...". Accepts localized key</param>
+        /// <param name="descriptiveName">A description which is displayed in the search bar, e.g. "Save image as new". Leave empty to hide it from the search bar. Accepts localized key</param>
         public BasicAttribute(string internalName, string displayName, string descriptiveName)
             : this(internalName, null, displayName, descriptiveName)
         {
@@ -25,8 +25,8 @@ internal partial class Command
         /// </summary>
         /// <param name="internalName">The internal name of the command</param>
         /// <param name="parameter">The parameter that will be passed to the first argument of the method</param>
-        /// <param name="displayName">A short description which is displayed in the the top menu, e.g. "Save as..."</param>
-        /// <param name="description">A description which is displayed in the search bar, e.g. "Save image as new". Leave empty to hide it from the search bar</param>
+        /// <param name="displayName">A short description which is displayed in the the top menu, e.g. "Save as...". Accepts localized key</param>
+        /// <param name="description">A description which is displayed in the search bar, e.g. "Save image as new". Leave empty to hide it from the search bar. Accepts localized key</param>
         public BasicAttribute(string internalName, object parameter, string displayName, string description)
             : base(internalName, displayName, description)
         {

+ 3 - 2
src/PixiEditor/Models/Commands/Attributes/Commands/CommandAttribute.cs

@@ -1,4 +1,5 @@
 using System.Windows.Input;
+using PixiEditor.Localization;
 using PixiEditor.Models.DataHolders;
 
 namespace PixiEditor.Models.Commands.Attributes.Commands;
@@ -10,9 +11,9 @@ internal partial class Command
     {
         public string InternalName { get; }
 
-        public string DisplayName { get; }
+        public LocalizedString DisplayName { get; }
 
-        public string Description { get; }
+        public LocalizedString Description { get; }
 
         public string CanExecute { get; set; }
 

+ 4 - 2
src/PixiEditor/Models/Commands/Attributes/Commands/FilterAttribute.cs

@@ -1,10 +1,12 @@
-namespace PixiEditor.Models.Commands.Attributes.Commands;
+using PixiEditor.Localization;
+
+namespace PixiEditor.Models.Commands.Attributes.Commands;
 
 internal partial class Command
 {
     public class FilterAttribute : CommandAttribute
     {
-        public string SearchTerm { get; }
+        public LocalizedString SearchTerm { get; }
         
         public FilterAttribute(string internalName, string displayName, string searchTerm) : base(internalName, displayName, string.Empty)
         {

+ 4 - 2
src/PixiEditor/Models/Commands/Attributes/Commands/GroupAttribute.cs

@@ -1,4 +1,6 @@
-namespace PixiEditor.Models.Commands.Attributes.Commands;
+using PixiEditor.Localization;
+
+namespace PixiEditor.Models.Commands.Attributes.Commands;
 
 internal partial class Command
 {
@@ -7,7 +9,7 @@ internal partial class Command
     {
         public string InternalName { get; }
 
-        public string DisplayName { get; }
+        public LocalizedString DisplayName { get; }
 
         /// <summary>
         /// Groups all commands that start with the name <paramref name="internalName"/>

+ 15 - 12
src/PixiEditor/Models/Commands/CommandController.cs

@@ -3,6 +3,7 @@ using System.Reflection;
 using System.Windows.Media;
 using Microsoft.Extensions.DependencyInjection;
 using Newtonsoft.Json;
+using PixiEditor.Localization;
 using PixiEditor.Models.Commands.Commands;
 using PixiEditor.Models.Commands.Evaluators;
 using PixiEditor.Models.DataHolders;
@@ -70,9 +71,9 @@ internal class CommandController
         }
     }
 
-    private static List<(string internalName, string displayName)> FindCommandGroups(IEnumerable<Type> typesToSearchForAttributes)
+    private static List<(string internalName, LocalizedString displayName)> FindCommandGroups(IEnumerable<Type> typesToSearchForAttributes)
     {
-        List<(string internalName, string displayName)> result = new();
+        List<(string internalName, LocalizedString displayName)> result = new();
 
         foreach (var type in typesToSearchForAttributes)
         {
@@ -111,10 +112,10 @@ internal class CommandController
             File.Move(shortcutFile.Path, $"{shortcutFile.Path}.corrupted", true);
             shortcutFile = new ShortcutFile(ShortcutsPath, this);
             template = shortcutFile.LoadTemplate();
-            NoticeDialog.Show("Shortcuts file was corrupted, resetting to default.", "Corrupted shortcuts file");
+            NoticeDialog.Show("SHORTCUTS_CORRUPTED", "SHORTCUTS_CORRUPTED_TITLE");
         }
         var compiledCommandList = new CommandNameList();
-        List<(string internalName, string displayName)> commandGroupsData = FindCommandGroups(compiledCommandList.Groups);
+        List<(string internalName, LocalizedString displayName)> commandGroupsData = FindCommandGroups(compiledCommandList.Groups);
         OneToManyDictionary<string, Command> commands = new(); // internal name of the corr. group -> command in that group
 
         LoadEvaluators(serviceProvider, compiledCommandList);
@@ -132,14 +133,14 @@ internal class CommandController
                 continue;
             }
 
-            string groupDisplayName = groupData.displayName;
+            LocalizedString groupDisplayName = groupData.displayName;
             CommandGroups.Add(new CommandGroup(groupDisplayName, storedCommands));
         }
         
-        CommandGroups.Add(new CommandGroup("Misc", miscList));
+        CommandGroups.Add(new CommandGroup("MISC", miscList));
     }
 
-    private void LoadTools(IServiceProvider serviceProvider, List<(string internalName, string displayName)> commandGroupsData, OneToManyDictionary<string, Command> commands,
+    private void LoadTools(IServiceProvider serviceProvider, List<(string internalName, LocalizedString displayName)> commandGroupsData, OneToManyDictionary<string, Command> commands,
         ShortcutsTemplate template)
     {
         foreach (var toolInstance in serviceProvider.GetServices<ToolViewModel>())
@@ -155,11 +156,13 @@ internal class CommandController
 
             string internalName = $"PixiEditor.Tools.Select.{type.Name}";
 
+            LocalizedString displayName = new("SELECT_TOOL", toolInstance.DisplayName);
+
             var command = new Command.ToolCommand()
             {
                 InternalName = internalName,
-                DisplayName = $"Select {toolInstance.DisplayName} Tool",
-                Description = $"Select {toolInstance.DisplayName} Tool",
+                DisplayName = displayName,
+                Description = displayName,
                 IconPath = $"@{toolInstance.ImagePath}",
                 IconEvaluator = IconEvaluator.Default,
                 TransientKey = toolAttr.Transient,
@@ -178,7 +181,7 @@ internal class CommandController
             .FirstOrDefault(x => x.Commands.Contains(internalName), new Shortcut(defaultShortcut, (List<string>)null))
             .KeyCombination;
 
-    private void AddCommandToCommandsCollection(Command command, List<(string internalName, string displayName)> commandGroupsData, OneToManyDictionary<string, Command> commands)
+    private void AddCommandToCommandsCollection(Command command, List<(string internalName, LocalizedString displayName)> commandGroupsData, OneToManyDictionary<string, Command> commands)
     {
         (string internalName, string displayName) group = commandGroupsData.FirstOrDefault(x => command.InternalName.StartsWith(x.internalName));
         if (group == default)
@@ -187,7 +190,7 @@ internal class CommandController
             commands.Add(group.internalName, command);
     }
 
-    private void LoadCommands(IServiceProvider serviceProvider, CommandNameList compiledCommandList, List<(string internalName, string displayName)> commandGroupsData, OneToManyDictionary<string, Command> commands, ShortcutsTemplate template)
+    private void LoadCommands(IServiceProvider serviceProvider, CommandNameList compiledCommandList, List<(string internalName, LocalizedString displayName)> commandGroupsData, OneToManyDictionary<string, Command> commands, ShortcutsTemplate template)
     {
         foreach (var type in compiledCommandList.Commands)
         {
@@ -247,7 +250,7 @@ internal class CommandController
                             {
                                 InternalName = menu.InternalName,
                                 DisplayName = menu.DisplayName,
-                                Description = string.Empty,
+                                Description = menu.DisplayName,
                                 IconEvaluator = IconEvaluator.Default,
                                 DefaultShortcut = menu.GetShortcut(),
                                 Shortcut = GetShortcut(name, attribute.GetShortcut(), template)

+ 19 - 6
src/PixiEditor/Models/Commands/CommandGroup.cs

@@ -1,16 +1,24 @@
 using System.Collections;
+using System.ComponentModel;
 using System.Windows.Input;
+using PixiEditor.Localization;
 using PixiEditor.Models.Commands.Commands;
 using PixiEditor.Models.DataHolders;
 
 namespace PixiEditor.Models.Commands;
 
-internal class CommandGroup : IEnumerable<Command>
+internal class CommandGroup : NotifyableObject
 {
     private readonly Command[] commands;
     private readonly Command[] visibleCommands;
 
-    public string DisplayName { get; set; }
+    private LocalizedString displayName;
+
+    public LocalizedString DisplayName
+    {
+        get => displayName;
+        set => SetProperty(ref displayName, value);
+    }
 
     public bool HasAssignedShortcuts { get; set; }
 
@@ -18,17 +26,24 @@ internal class CommandGroup : IEnumerable<Command>
 
     public IEnumerable<Command> VisibleCommands => visibleCommands;
 
-    public CommandGroup(string displayName, IEnumerable<Command> commands)
+    public CommandGroup(LocalizedString displayName, IEnumerable<Command> commands)
     {
         DisplayName = displayName;
         this.commands = commands.ToArray();
-        visibleCommands = commands.Where(x => !string.IsNullOrEmpty(x.DisplayName)).ToArray();
+        visibleCommands = commands.Where(x => !string.IsNullOrEmpty(x.DisplayName.Value)).ToArray();
 
         foreach (var command in commands)
         {
             HasAssignedShortcuts |= command.Shortcut.Key != Key.None;
             command.ShortcutChanged += Command_ShortcutChanged;
         }
+
+        ILocalizationProvider.Current.OnLanguageChanged += OnLanguageChanged;
+    }
+
+    private void OnLanguageChanged(Language obj)
+    {
+        DisplayName = new LocalizedString(DisplayName.Key);
     }
 
     private void Command_ShortcutChanged(Command cmd, ShortcutChangedEventArgs args)
@@ -50,6 +65,4 @@ internal class CommandGroup : IEnumerable<Command>
     }
 
     public IEnumerator<Command> GetEnumerator() => Commands.GetEnumerator();
-
-    IEnumerator IEnumerable.GetEnumerator() => Commands.GetEnumerator();
 }

+ 18 - 3
src/PixiEditor/Models/Commands/Commands/Command.cs

@@ -1,5 +1,7 @@
 using System.Diagnostics;
+using System.Windows.Input;
 using System.Windows.Media;
+using PixiEditor.Localization;
 using PixiEditor.Models.Commands.Evaluators;
 using PixiEditor.Models.DataHolders;
 
@@ -18,9 +20,9 @@ internal abstract partial class Command : NotifyableObject
 
     public IconEvaluator IconEvaluator { get; init; }
 
-    public string DisplayName { get; init; }
+    public LocalizedString DisplayName { get; set; }
 
-    public string Description { get; init; }
+    public LocalizedString Description { get; set; }
 
     public CommandMethods Methods { get; init; }
 
@@ -42,8 +44,21 @@ internal abstract partial class Command : NotifyableObject
 
     public abstract object GetParameter();
 
-    protected Command(Action<object> onExecute, CanExecuteEvaluator canExecute) =>
+    protected Command(Action<object> onExecute, CanExecuteEvaluator canExecute)
+    {
         Methods = new(this, onExecute, canExecute);
+        ILocalizationProvider.Current.OnLanguageChanged += OnLanguageChanged;
+        InputLanguageManager.Current.InputLanguageChanged += (_, _) => RaisePropertyChanged(nameof(Shortcut));
+    }
+
+    private void OnLanguageChanged(Language obj)
+    {
+        DisplayName = new LocalizedString(DisplayName.Key, DisplayName.Parameters);
+        Description = new LocalizedString(Description.Key, Description.Parameters);
+
+        RaisePropertyChanged(nameof(DisplayName));
+        RaisePropertyChanged(nameof(Description));
+    }
 
     public void Execute() => Methods.Execute(GetParameter());
 

+ 5 - 4
src/PixiEditor/Models/Commands/XAML/ShortcutBinding.cs

@@ -18,10 +18,11 @@ internal class ShortcutBinding : MarkupExtension
 
     public override object ProvideValue(IServiceProvider serviceProvider)
     {
-#if DEBUG
-        var attribute = DesignCommandHelpers.GetCommandAttribute(Name);
-        return new KeyCombination(attribute.Key, attribute.Modifiers).ToString();
-#endif
+        if (ViewModelMain.Current == null)
+        {
+            var attribute = DesignCommandHelpers.GetCommandAttribute(Name);
+            return new KeyCombination(attribute.Key, attribute.Modifiers).ToString();
+        }
 
         commandController ??= ViewModelMain.Current.CommandController;
         return GetBinding(commandController.Commands[Name]).ProvideValue(serviceProvider);

+ 1 - 1
src/PixiEditor/Models/Controllers/ClipboardController.cs

@@ -38,7 +38,7 @@ internal static class ClipboardController
             return;
         if (surface.IsT1)
         {
-            NoticeDialog.Show("Selected area is empty", "Nothing to copy");
+            NoticeDialog.Show("SELECTED_AREA_EMPTY", "NOTHING_TO_COPY");
             return;
         }
         var (actuallySurface, _) = surface.AsT2;

+ 11 - 7
src/PixiEditor/Models/DataHolders/KeyCombination.cs

@@ -4,6 +4,7 @@ using System.Diagnostics;
 using System.Globalization;
 using System.Text;
 using System.Windows.Input;
+using PixiEditor.Localization;
 
 namespace PixiEditor.Models.DataHolders;
 
@@ -12,19 +13,21 @@ public record struct KeyCombination(Key Key, ModifierKeys Modifiers)
 {
     public static KeyCombination None => new(Key.None, ModifierKeys.None);
 
-    public override string ToString() => ToString(CultureInfo.CurrentCulture);
+    public override string ToString() => ToString(false);
 
-    public string ToString(CultureInfo culture)
+    private string ToString(bool forceInvariant)
     {
         StringBuilder builder = new();
 
-        foreach (ModifierKeys modifier in Modifiers.GetFlags().OrderByDescending(x => x != ModifierKeys.Alt))
+        foreach (var modifier in Modifiers.GetFlags().OrderByDescending(x => x != ModifierKeys.Alt))
         {
             if (modifier == ModifierKeys.None) continue;
 
             string key = modifier switch
             {
-                ModifierKeys.Control => "Ctrl",
+                ModifierKeys.Control => new LocalizedString("CTRL_KEY"),
+                ModifierKeys.Shift => new LocalizedString("SHIFT_KEY"),
+                ModifierKeys.Alt => new LocalizedString("ALT_KEY"),
                 _ => modifier.ToString()
             };
 
@@ -33,11 +36,12 @@ public record struct KeyCombination(Key Key, ModifierKeys Modifiers)
 
         if (Key != Key.None)
         {
-            builder.Append(InputKeyHelpers.GetKeyboardKey(Key, culture));
+            builder.Append(InputKeyHelpers.GetKeyboardKey(Key, forceInvariant));
         }
 
+        builder.Append('‎'); // left-to-right marker ensures WPF does not reverse the string when using punctuations as key
         return builder.ToString();
     }
 
-    private string GetDebuggerDisplay() => ToString(CultureInfo.InvariantCulture);
-}
+    private string GetDebuggerDisplay() => ToString(true);
+}

+ 3 - 2
src/PixiEditor/Models/Dialogs/ConfirmationDialog.cs

@@ -1,4 +1,5 @@
-using PixiEditor.Models.Enums;
+using PixiEditor.Localization;
+using PixiEditor.Models.Enums;
 using PixiEditor.Views;
 using PixiEditor.Views.Dialogs;
 
@@ -6,7 +7,7 @@ namespace PixiEditor.Models.Dialogs;
 
 internal static class ConfirmationDialog
 {
-    public static ConfirmationType Show(string message, string title)
+    public static ConfirmationType Show(LocalizedString message, LocalizedString title)
     {
         ConfirmationPopup popup = new ConfirmationPopup
         {

+ 3 - 2
src/PixiEditor/Models/Dialogs/NoticeDialog.cs

@@ -1,10 +1,11 @@
-using PixiEditor.Views.Dialogs;
+using PixiEditor.Localization;
+using PixiEditor.Views.Dialogs;
 
 namespace PixiEditor.Models.Dialogs;
 
 internal static class NoticeDialog
 {
-    public static void Show(string message, string title)
+    public static void Show(LocalizedString message, LocalizedString title)
     {
         NoticePopup popup = new()
         {

+ 3 - 2
src/PixiEditor/Models/Dialogs/OptionDialog.cs

@@ -1,11 +1,12 @@
-using PixiEditor.Models.Enums;
+using PixiEditor.Localization;
+using PixiEditor.Models.Enums;
 using PixiEditor.Views.Dialogs;
 
 namespace PixiEditor.Models.Dialogs;
 
 internal static class OptionDialog
 {
-    public static OptionResult Show(string message, string title, string option1Text, string option2Text)
+    public static OptionResult Show(LocalizedString message, LocalizedString title, LocalizedString option1Text, LocalizedString option2Text)
     {
         ConfirmationPopup popup = new ConfirmationPopup
         {

+ 2 - 1
src/PixiEditor/Models/Dialogs/OptionsDialog.cs

@@ -1,6 +1,7 @@
 using System.Collections;
 using System.Windows.Controls;
 using System.Windows.Media;
+using PixiEditor.Localization;
 using PixiEditor.Views.Dialogs;
 
 namespace PixiEditor.Models.Dialogs;
@@ -15,7 +16,7 @@ internal class OptionsDialog<T> : CustomDialog, IEnumerable<T>
 
     public T Result { get; private set; }
 
-    public OptionsDialog(string title, object content)
+    public OptionsDialog(LocalizedString title, object content)
     {
         Title = title;
 

+ 4 - 3
src/PixiEditor/Models/DocumentModels/DocumentStructureHelper.cs

@@ -1,4 +1,5 @@
 using PixiEditor.ChangeableDocument.Enums;
+using PixiEditor.Localization;
 using PixiEditor.Models.Enums;
 using PixiEditor.ViewModels.SubViewModels.Document;
 
@@ -34,7 +35,7 @@ internal class DocumentStructureHelper
             Guid guid = Guid.NewGuid();
             //put member on top
             internals.ActionAccumulator.AddActions(new CreateStructureMember_Action(doc.StructureRoot.GuidValue, guid, doc.StructureRoot.Children.Count, type));
-            name ??= GetUniqueName(type == StructureMemberType.Layer ? "New Layer" : "New Folder", doc.StructureRoot);
+            name ??= GetUniqueName(type == StructureMemberType.Layer ? new LocalizedString("NEW_LAYER") : new LocalizedString("NEW_FOLDER"), doc.StructureRoot);
             internals.ActionAccumulator.AddActions(new StructureMemberName_Action(guid, name));
             if (finish)
                 internals.ActionAccumulator.AddFinishedActions();
@@ -45,7 +46,7 @@ internal class DocumentStructureHelper
             Guid guid = Guid.NewGuid();
             //put member inside folder on top
             internals.ActionAccumulator.AddActions(new CreateStructureMember_Action(folder.GuidValue, guid, folder.Children.Count, type));
-            name ??= GetUniqueName(type == StructureMemberType.Layer ? "New Layer" : "New Folder", folder);
+            name ??= GetUniqueName(type == StructureMemberType.Layer ? new LocalizedString("NEW_LAYER") : new LocalizedString("NEW_FOLDER"), folder);
             internals.ActionAccumulator.AddActions(new StructureMemberName_Action(guid, name));
             if (finish)
                 internals.ActionAccumulator.AddFinishedActions();
@@ -60,7 +61,7 @@ internal class DocumentStructureHelper
                 throw new InvalidOperationException("Couldn't find a path to the selected member");
             FolderViewModel parent = (FolderViewModel)path[1];
             internals.ActionAccumulator.AddActions(new CreateStructureMember_Action(parent.GuidValue, guid, parent.Children.IndexOf(layer) + 1, type));
-            name ??= GetUniqueName(type == StructureMemberType.Layer ? "New Layer" : "New Folder", parent);
+            name ??= GetUniqueName(type == StructureMemberType.Layer ? new LocalizedString("NEW_LAYER") : new LocalizedString("NEW_FOLDER"), parent);
             internals.ActionAccumulator.AddActions(new StructureMemberName_Action(guid, name));
             if (finish)
                 internals.ActionAccumulator.AddFinishedActions();

+ 3 - 3
src/PixiEditor/Models/Enums/DocumentScope.cs

@@ -4,8 +4,8 @@ namespace PixiEditor.Models.Enums;
 
 public enum DocumentScope
 {
-    [Description("Single Layer")]
+    [Description("SINGLE_LAYER")]
     SingleLayer,
-    [Description("All Layers")]
+    [Description("ALL_LAYERS")]
     AllLayers
-}
+}

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

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

+ 4 - 1
src/PixiEditor/Models/Enums/SelectionShape.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.ComponentModel;
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
@@ -8,6 +9,8 @@ namespace PixiEditor.Models.Enums;
 
 public enum SelectionShape
 {
+    [Description("RECTANGLE")]
     Rectangle,
+    [Description("CIRCLE")]
     Circle
-}
+}

+ 1 - 1
src/PixiEditor/Models/ExternalServices/LospecPaletteFetcher.cs

@@ -33,7 +33,7 @@ internal static class LospecPaletteFetcher
         }
         catch (HttpRequestException)
         {
-            NoticeDialog.Show("Failed to download palette.", "Error");
+            NoticeDialog.Show("FAILED_DOWNLOAD_PALETTE", "ERROR");
             return null;
         }
 

+ 22 - 0
src/PixiEditor/PixiEditor.csproj

@@ -272,6 +272,7 @@
 		<Resource Include="Images\Folder.png" />
 		<Resource Include="Images\Globe.png" />
 		<Resource Include="Images\hard-drive.png" />
+		<Resource Include="Images\LanguageFlags\uk.png" />
 		<Resource Include="Images\Layer-add.png" />
 		<Resource Include="Images\Lock-alpha.png" />
 		<Resource Include="Images\Merge-downwards.png" />
@@ -399,6 +400,21 @@
 		<Resource Include="Images\Commands\PixiEditor\Selection\AddToMask.png" />
 		<None Remove="Images\Commands\PixiEditor\Selection\NewToMask.png" />
 		<Resource Include="Images\Commands\PixiEditor\Selection\NewToMask.png" />
+		<None Remove="Images\LanguageFlags\en.png" />
+		<Resource Include="Images\LanguageFlags\en.png" />
+		<None Remove="Images\LanguageFlags\pl.png" />
+		<Resource Include="Images\LanguageFlags\pl.png" />
+		<None Remove="Images\LanguageFlags\ar.png" />
+		<Resource Include="Images\LanguageFlags\ar.png" />
+		<None Remove="Images\LanguageFlags\cz.png" />
+		<None Remove="Images\LanguageFlags\de.png" />
+		<Resource Include="Images\LanguageFlags\de.png" />
+		<None Remove="Images\LanguageFlags\es.png" />
+		<Resource Include="Images\LanguageFlags\es.png" />
+		<None Remove="Images\LanguageFlags\ru.png" />
+		<Resource Include="Images\LanguageFlags\ru.png" />
+		<None Remove="Images\LanguageFlags\cs.png" />
+		<Resource Include="Images\LanguageFlags\cs.png" />
 	</ItemGroup>
 	<ItemGroup>
 		<None Include="..\LICENSE">
@@ -446,6 +462,12 @@
     <Content Include="Data\ShortcutActionMaps\*">
       <CopyToOutputDirectory>Always</CopyToOutputDirectory>
     </Content>
+    <Content Include="Data\Localization\*">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </Content>
+    <Content Include="Data\Localization\Languages\*">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </Content>
   </ItemGroup>
   <ItemGroup>
     <Folder Include="Models\Colors" />

+ 1 - 1
src/PixiEditor/Styles/DarkCheckboxStyle.xaml

@@ -13,7 +13,7 @@
                         <BulletDecorator.Bullet>
                             <Border x:Name="Border" Width="20" Height="20" CornerRadius="2.5" Background="#FF1B1B1B"
                                     BorderThickness="1">
-                                <Path Width="9" Height="9" x:Name="CheckMark" SnapsToDevicePixels="False" Stroke="{StaticResource UIElementBlue}" StrokeThickness="1.5" Data="M 0 4 L 3 8 8 0" />
+                                <Path FlowDirection="LeftToRight" Width="9" Height="9" x:Name="CheckMark" SnapsToDevicePixels="False" Stroke="{StaticResource UIElementBlue}" StrokeThickness="1.5" Data="M 0 4 L 3 8 8 0" />
                             </Border>
                         </BulletDecorator.Bullet>
                         <ContentPresenter Margin="4,0,0,0" VerticalAlignment="Center" HorizontalAlignment="Left" RecognizesAccessKey="True"/>

+ 1 - 0
src/PixiEditor/Styles/LabelStyles.xaml

@@ -11,6 +11,7 @@
         <Setter Property="Padding" Value="0"/>
         <Setter Property="VerticalAlignment" Value="Center"/>
         <Setter Property="FontWeight" Value="DemiBold"/>
+        <Setter Property="Margin" Value="5 10"/>
     </Style>
 
     <Style x:Key="SettingsLink" TargetType="Hyperlink" BasedOn="{StaticResource {x:Type Hyperlink}}">

+ 3 - 2
src/PixiEditor/Styles/PixiListBoxItemStyle.xaml

@@ -1,5 +1,6 @@
 <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
-                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
+                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+                    xmlns:views="clr-namespace:PixiEditor.Views">
     <Style TargetType="ListBoxItem" x:Key="PixiListBoxItemStyle">
         <Setter Property="OverridesDefaultStyle" Value="True"/>
         <Setter Property="Foreground" Value="White"/>
@@ -9,7 +10,7 @@
             <Setter.Value>
                 <ControlTemplate TargetType="ListBoxItem">
                     <Border x:Name="Border" Padding="15,7" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}">
-                        <ContentPresenter Content="{TemplateBinding Content}"/>
+                        <ContentPresenter/>
                     </Border>
                     <ControlTemplate.Triggers>
                         <Trigger Property="IsMouseOver" Value="True">

+ 8 - 2
src/PixiEditor/Styles/ThemeStyle.xaml

@@ -22,6 +22,8 @@
         <Setter Property="Foreground" Value="White" />
         <Setter Property="Focusable" Value="False" />
         <Setter Property="FontSize" Value="15" />
+        <Setter Property="MinWidth" Value="70"/>
+        <Setter Property="Padding" Value="10,0"/>
         <Setter Property="SnapsToDevicePixels" Value="True" />
         <Setter Property="Template">
             <Setter.Value>
@@ -131,7 +133,8 @@
         <Setter Property="Background" Value="Transparent" />
         <Setter Property="Focusable" Value="False" />
         <Setter Property="Height" Value="28"/>
-        <Setter Property="Width" Value="70"/>
+        <Setter Property="MinWidth" Value="0"/>
+        <Setter Property="Padding" Value="0"/>
         <Setter Property="Template">
             <Setter.Value>
                 <ControlTemplate TargetType="Button">
@@ -165,7 +168,8 @@
         <Setter Property="Background" Value="Transparent" />
         <Setter Property="Focusable" Value="False" />
         <Setter Property="Height" Value="28"/>
-        <Setter Property="Width" Value="70"/>
+        <Setter Property="MinWidth" Value="0"/>
+        <Setter Property="Padding" Value="0"/>
         <Setter Property="Template">
             <Setter.Value>
                 <ControlTemplate TargetType="ToggleButton">
@@ -281,6 +285,7 @@
         <Setter Property="FontSize" Value="18"/>
         <Setter Property="Height" Value="50"/>
         <Setter Property="Cursor" Value="Hand"/>
+        <Setter Property="MinWidth" Value="0"/>
         <Setter Property="Template">
             <Setter.Value>
                 <ControlTemplate TargetType="Button">
@@ -311,6 +316,7 @@
         <Setter Property="TextBlock.FontFamily" Value="Segoe MDL2 Assets"/>
         <Setter Property="TextBlock.FontSize" Value="15"/>
         <Setter Property="Width" Value="30"/>
+        <Setter Property="MinWidth" Value="0"/>
 
         <Style.Triggers>
             <Trigger Property="IsEnabled" Value="True">

+ 58 - 13
src/PixiEditor/ViewModels/SettingsWindowViewModel.cs

@@ -1,12 +1,13 @@
-using System.IO;
+using System.Collections.ObjectModel;
+using System.IO;
 using System.Text;
-using PixiEditor.Helpers;
 using PixiEditor.Models.Commands;
 using PixiEditor.Models.Dialogs;
 using PixiEditor.ViewModels.SubViewModels.UserPreferences;
 using System.Windows;
 using System.Windows.Input;
 using Microsoft.Win32;
+using PixiEditor.Localization;
 using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.Models.Commands.Templates;
 using PixiEditor.Models.UserPreferences;
@@ -14,11 +15,31 @@ using PixiEditor.Views.Dialogs;
 
 namespace PixiEditor.ViewModels;
 
+internal class SettingsPage : NotifyableObject
+{
+    private LocalizedString name;
+
+    public LocalizedString Name
+    {
+        get => name;
+        set => SetProperty(ref name, value);
+    }
+
+    public SettingsPage(string nameKey)
+    {
+        Name = new LocalizedString(nameKey);
+    }
+
+    public void UpdateName()
+    {
+        Name = new LocalizedString(Name.Key);
+    }
+}
 internal class SettingsWindowViewModel : ViewModelBase
 {
     private string searchTerm;
     private int visibleGroups;
-    private string currentPage;
+    private int currentPage;
 
     public bool ShowUpdateTab
     {
@@ -46,7 +67,7 @@ internal class SettingsWindowViewModel : ViewModelBase
         }
     }
 
-    public string CurrentPage
+    public int CurrentPage
     {
         get => currentPage;
         set => SetProperty(ref currentPage, value);
@@ -61,16 +82,17 @@ internal class SettingsWindowViewModel : ViewModelBase
     public SettingsViewModel SettingsSubViewModel { get; set; }
 
     public List<GroupSearchResult> Commands { get; }
+    public ObservableCollection<SettingsPage> Pages { get; }
 
     private static List<ICustomShortcutFormat> _customShortcutFormats;
 
     [Command.Internal("PixiEditor.Shortcuts.Reset")]
     public static void ResetCommand()
     {
-        var dialog = new OptionsDialog<string>("Are you sure?", "Are you sure you want to reset all shortcuts to their default value?")
+        var dialog = new OptionsDialog<string>("ARE_YOU_SURE", new LocalizedString("WARNING_RESET_SHORTCUTS_DEFAULT"))
         {
-            { "Yes", x => CommandController.Current.ResetShortcuts() },
-            "Cancel"
+            { new LocalizedString("YES"), x => CommandController.Current.ResetShortcuts() },
+            new LocalizedString("CANCEL")
         }.ShowDialog();
     }
 
@@ -109,14 +131,14 @@ internal class SettingsWindowViewModel : ViewModelBase
             }
             catch (Exception e)
             {
-                NoticeDialog.Show("Shortcuts file was not in a valid format", "Invalid file");
+                NoticeDialog.Show("SHORTCUTS_FILE_INCORRECT_FORMAT", "INVALID_FILE");
                 return;
             }
             
             CommandController.Current.ResetShortcuts();
             CommandController.Current.Import(shortcuts, false);
             File.Copy(dialog.FileName, CommandController.ShortcutsPath, true);
-            NoticeDialog.Show("Shortcuts were imported successfully", "Success");
+            NoticeDialog.Show("SHORTCUTS_IMPORTED_SUCCESS", "SUCCESS");
         }
         // Sometimes, focus was brought back to the last edited shortcut
         Keyboard.ClearFocus();
@@ -129,7 +151,7 @@ internal class SettingsWindowViewModel : ViewModelBase
             shortcuts = ShortcutFile.LoadTemplate(dialog.FileName)?.Shortcuts.ToList();
             if (shortcuts is null)
             {
-                NoticeDialog.Show("Shortcuts file was not in a valid format", "Invalid file");
+                NoticeDialog.Show("SHORTCUTS_FILE_INCORRECT_FORMAT", "INVALID_FILE");
                 return false;
             }
         }
@@ -139,7 +161,7 @@ internal class SettingsWindowViewModel : ViewModelBase
                 x.CustomShortcutExtensions.Contains(Path.GetExtension(dialog.FileName), StringComparer.OrdinalIgnoreCase));
             if (provider is null)
             {
-                NoticeDialog.Show("This file format is unsupported.", "Invalid file");
+                NoticeDialog.Show("UNSUPPORTED_FILE_FORMAT", "INVALID_FILE");
                 return false;
             }
 
@@ -188,6 +210,14 @@ internal class SettingsWindowViewModel : ViewModelBase
 
     public SettingsWindowViewModel()
     {
+        Pages = new ObservableCollection<SettingsPage>
+        {
+            new SettingsPage("GENERAL"),
+            new SettingsPage("DISCORD"),
+            new SettingsPage("KEY_BINDINGS"),
+        };
+
+        ILocalizationProvider.Current.OnLanguageChanged += OnLanguageChanged;
         Commands = new(CommandController.Current.CommandGroups.Select(x => new GroupSearchResult(x)));
         UpdateSearchResults();
         SettingsSubViewModel = new SettingsViewModel(this);
@@ -195,6 +225,21 @@ internal class SettingsWindowViewModel : ViewModelBase
         VisibleGroups = Commands.Count(x => x.Visibility == Visibility.Visible);
     }
 
+    private void UpdatePages()
+    {
+        foreach (var page in Pages)
+        {
+            page.UpdateName();
+        }
+
+        RaisePropertyChanged(nameof(Pages));
+    }
+
+    private void OnLanguageChanged(Language obj)
+    {
+        UpdatePages();
+    }
+
     public void UpdateSearchResults()
     {
         if (string.IsNullOrWhiteSpace(searchTerm))
@@ -227,7 +272,7 @@ internal class SettingsWindowViewModel : ViewModelBase
             var visibleCommands = 0;
             foreach (var command in group.Commands)
             {
-                if (command.Command.DisplayName.Contains(SearchTerm, StringComparison.OrdinalIgnoreCase))
+                if (command.Command.DisplayName.Value.Contains(SearchTerm, StringComparison.OrdinalIgnoreCase))
                 {
                     visibleCommands++;
                     command.Visibility = Visibility.Visible;
@@ -246,7 +291,7 @@ internal class SettingsWindowViewModel : ViewModelBase
     {
         private Visibility visibility;
 
-        public string DisplayName { get; set; }
+        public LocalizedString DisplayName { get; set; }
 
         public List<CommandSearchResult> Commands { get; set; }
 

+ 24 - 24
src/PixiEditor/ViewModels/SubViewModels/Document/DocumentManagerViewModel.cs

@@ -11,7 +11,7 @@ using PixiEditor.Views.UserControls.SymmetryOverlay;
 
 namespace PixiEditor.ViewModels.SubViewModels.Document;
 #nullable enable
-[Command.Group("PixiEditor.Document", "Image")]
+[Command.Group("PixiEditor.Document", "IMAGE")]
 internal class DocumentManagerViewModel : SubViewModel<ViewModelMain>
 {
     public ObservableCollection<DocumentViewModel> Documents { get; } = new ObservableCollection<DocumentViewModel>();
@@ -49,15 +49,15 @@ internal class DocumentManagerViewModel : SubViewModel<ViewModelMain>
     [Evaluator.CanExecute("PixiEditor.HasDocument")]
     public bool DocumentNotNull() => ActiveDocument != null;
 
-    [Command.Basic("PixiEditor.Document.ClipCanvas", "Clip Canvas", "Clip Canvas", CanExecute = "PixiEditor.HasDocument", IconPath = "crop.png")]
+    [Command.Basic("PixiEditor.Document.ClipCanvas", "CLIP_CANVAS", "CLIP_CANVAS", CanExecute = "PixiEditor.HasDocument", IconPath = "crop.png")]
     public void ClipCanvas() => ActiveDocument?.Operations.ClipCanvas();
 
-    [Command.Basic("PixiEditor.Document.FlipImageHorizontal", FlipType.Horizontal, "Flip Image Horizontally", "Flip Image Horizontally", CanExecute = "PixiEditor.HasDocument")]
-    [Command.Basic("PixiEditor.Document.FlipImageVertical", FlipType.Vertical, "Flip Image Vertically", "Flip Image Vertically", CanExecute = "PixiEditor.HasDocument")]
+    [Command.Basic("PixiEditor.Document.FlipImageHorizontal", FlipType.Horizontal, "FLIP_IMG_HORIZONTALLY", "FLIP_IMG_HORIZONTALLY", CanExecute = "PixiEditor.HasDocument")]
+    [Command.Basic("PixiEditor.Document.FlipImageVertical", FlipType.Vertical, "FLIP_IMG_VERTICALLY", "FLIP_IMG_VERTICALLY", CanExecute = "PixiEditor.HasDocument")]
     public void FlipImage(FlipType type) => ActiveDocument?.Operations.FlipImage(type);
 
-    [Command.Basic("PixiEditor.Document.FlipLayersHorizontal", FlipType.Horizontal, "Flip Selected Layers Horizontally", "Flip Selected Layers Horizontally", CanExecute = "PixiEditor.HasDocument")]
-    [Command.Basic("PixiEditor.Document.FlipLayersVertical", FlipType.Vertical, "Flip Selected Layers Vertically", "Flip Selected Layers Vertically", CanExecute = "PixiEditor.HasDocument")]
+    [Command.Basic("PixiEditor.Document.FlipLayersHorizontal", FlipType.Horizontal, "FLIP_LAYERS_HORIZONTALLY", "FLIP_LAYERS_HORIZONTALLY", CanExecute = "PixiEditor.HasDocument")]
+    [Command.Basic("PixiEditor.Document.FlipLayersVertical", FlipType.Vertical, "FLIP_LAYERS_VERTICALLY", "FLIP_LAYERS_VERTICALLY", CanExecute = "PixiEditor.HasDocument")]
     public void FlipLayers(FlipType type)
     {
         if (ActiveDocument?.SelectedStructureMember == null)
@@ -66,20 +66,20 @@ internal class DocumentManagerViewModel : SubViewModel<ViewModelMain>
         ActiveDocument?.Operations.FlipImage(type, ActiveDocument.GetSelectedMembers());
     }
     
-    [Command.Basic("PixiEditor.Document.Rotate90Deg", "Rotate Image 90 degrees", 
-        "Rotate Image 90 degrees", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D90)]
-    [Command.Basic("PixiEditor.Document.Rotate180Deg", "Rotate Image 180 degrees", 
-        "Rotate Image 180 degrees", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D180)]
-    [Command.Basic("PixiEditor.Document.Rotate270Deg", "Rotate Image -90 degrees", 
-        "Rotate Image -90 degrees", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D270)]
+    [Command.Basic("PixiEditor.Document.Rotate90Deg", "ROT_IMG_90",
+        "ROT_IMG_90", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D90)]
+    [Command.Basic("PixiEditor.Document.Rotate180Deg", "ROT_IMG_180",
+        "ROT_IMG_180", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D180)]
+    [Command.Basic("PixiEditor.Document.Rotate270Deg", "ROT_IMG_-90",
+        "ROT_IMG_-90", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D270)]
     public void RotateImage(RotationAngle angle) => ActiveDocument?.Operations.RotateImage(angle);
 
-    [Command.Basic("PixiEditor.Document.Rotate90DegLayers", "Rotate Selected Layers 90 degrees", 
-        "Rotate Selected Layers 90 degrees", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D90)]
-    [Command.Basic("PixiEditor.Document.Rotate180DegLayers", "Rotate Selected Layers 180 degrees", 
-        "Rotate Selected Layers 180 degrees", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D180)]
-    [Command.Basic("PixiEditor.Document.Rotate270DegLayers", "Rotate Selected Layers -90 degrees", 
-        "Rotate Selected Layers -90 degrees", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D270)]
+    [Command.Basic("PixiEditor.Document.Rotate90DegLayers", "ROT_LAYERS_90",
+        "ROT_LAYERS_90", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D90)]
+    [Command.Basic("PixiEditor.Document.Rotate180DegLayers", "ROT_LAYERS_180",
+        "ROT_LAYERS_180", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D180)]
+    [Command.Basic("PixiEditor.Document.Rotate270DegLayers", "ROT_LAYERS_-90",
+        "ROT_LAYERS_-90", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D270)]
     public void RotateLayers(RotationAngle angle)
     {
         if (ActiveDocument?.SelectedStructureMember == null)
@@ -88,7 +88,7 @@ internal class DocumentManagerViewModel : SubViewModel<ViewModelMain>
         ActiveDocument?.Operations.RotateImage(angle, ActiveDocument.GetSelectedMembers());
     }
 
-    [Command.Basic("PixiEditor.Document.ToggleVerticalSymmetryAxis", "Toggle vertical symmetry axis", "Toggle vertical symmetry axis", CanExecute = "PixiEditor.HasDocument", IconPath = "SymmetryVertical.png")]
+    [Command.Basic("PixiEditor.Document.ToggleVerticalSymmetryAxis", "TOGGLE_VERT_SYMMETRY_AXIS", "TOGGLE_VERT_SYMMETRY_AXIS", CanExecute = "PixiEditor.HasDocument", IconPath = "SymmetryVertical.png")]
     public void ToggleVerticalSymmetryAxis()
     {
         if (ActiveDocument is null)
@@ -96,7 +96,7 @@ internal class DocumentManagerViewModel : SubViewModel<ViewModelMain>
         ActiveDocument.VerticalSymmetryAxisEnabledBindable ^= true;
     }
 
-    [Command.Basic("PixiEditor.Document.ToggleHorizontalSymmetryAxis", "Toggle horizontal symmetry axis", "Toggle horizontal symmetry axis", CanExecute = "PixiEditor.HasDocument", IconPath = "SymmetryHorizontal.png")]
+    [Command.Basic("PixiEditor.Document.ToggleHorizontalSymmetryAxis", "TOGGLE_HOR_SYMMETRY_AXIS", "TOGGLE_HOR_SYMMETRY_AXIS", CanExecute = "PixiEditor.HasDocument", IconPath = "SymmetryHorizontal.png")]
     public void ToggleHorizontalSymmetryAxis()
     {
         if (ActiveDocument is null)
@@ -129,15 +129,15 @@ internal class DocumentManagerViewModel : SubViewModel<ViewModelMain>
         ActiveDocument.EventInlet.OnSymmetryDragEnded(dir);
     }
 
-    [Command.Basic("PixiEditor.Document.DeletePixels", "Delete pixels", "Delete selected pixels", CanExecute = "PixiEditor.Selection.IsNotEmpty", Key = Key.Delete, IconPath = "Tools/EraserImage.png")]
+    [Command.Basic("PixiEditor.Document.DeletePixels", "DELETE_PIXELS", "DELETE_PIXELS_DESCRIPTIVE", CanExecute = "PixiEditor.Selection.IsNotEmpty", Key = Key.Delete, IconPath = "Tools/EraserImage.png")]
     public void DeletePixels()
     {
         Owner.DocumentManagerSubViewModel.ActiveDocument?.Operations.DeleteSelectedPixels();
     }
 
 
-    [Command.Basic("PixiEditor.Document.ResizeDocument", false, "Resize Document", "Resize Document", CanExecute = "PixiEditor.HasDocument", Key = Key.I, Modifiers = ModifierKeys.Control | ModifierKeys.Shift)]
-    [Command.Basic("PixiEditor.Document.ResizeCanvas", true, "Resize Canvas", "Resize Canvas", CanExecute = "PixiEditor.HasDocument", Key = Key.C, Modifiers = ModifierKeys.Control | ModifierKeys.Shift)]
+    [Command.Basic("PixiEditor.Document.ResizeDocument", false, "RESIZE_DOCUMENT", "RESIZE_DOCUMENT", CanExecute = "PixiEditor.HasDocument", Key = Key.I, Modifiers = ModifierKeys.Control | ModifierKeys.Shift)]
+    [Command.Basic("PixiEditor.Document.ResizeCanvas", true, "RESIZE_CANVAS", "RESIZE_CANVAS", CanExecute = "PixiEditor.HasDocument", Key = Key.C, Modifiers = ModifierKeys.Control | ModifierKeys.Shift)]
     public void OpenResizePopup(bool canvas)
     {
         DocumentViewModel? doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
@@ -161,7 +161,7 @@ internal class DocumentManagerViewModel : SubViewModel<ViewModelMain>
         }
     }
 
-    [Command.Basic("PixiEditor.Document.CenterContent", "Center Content", "Center Content", CanExecute = "PixiEditor.HasDocument")]
+    [Command.Basic("PixiEditor.Document.CenterContent", "CENTER_CONTENT", "CENTER_CONTENT", CanExecute = "PixiEditor.HasDocument")]
     public void CenterContent()
     {
         if(ActiveDocument?.SelectedStructureMember == null)

+ 2 - 1
src/PixiEditor/ViewModels/SubViewModels/Document/DocumentViewModel.cs

@@ -16,6 +16,7 @@ using PixiEditor.DrawingApi.Core.Surface;
 using PixiEditor.DrawingApi.Core.Surface.ImageData;
 using PixiEditor.DrawingApi.Core.Surface.Vector;
 using PixiEditor.Helpers;
+using PixiEditor.Localization;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.DocumentModels;
@@ -61,7 +62,7 @@ internal partial class DocumentViewModel : NotifyableObject
     
     public string FileName
     {
-        get => fullFilePath is null ? "Unnamed" : Path.GetFileName(fullFilePath);
+        get => fullFilePath is null ? new LocalizedString("UNNAMED") : Path.GetFileName(fullFilePath);
     }
 
     private Guid? lastChangeOnSave = null;

+ 12 - 12
src/PixiEditor/ViewModels/SubViewModels/Main/ClipboardViewModel.cs

@@ -10,7 +10,7 @@ using PixiEditor.Models.IO;
 
 namespace PixiEditor.ViewModels.SubViewModels.Main;
 #nullable enable
-[Command.Group("PixiEditor.Clipboard", "Clipboard")]
+[Command.Group("PixiEditor.Clipboard", "CLIPBOARD")]
 internal class ClipboardViewModel : SubViewModel<ViewModelMain>
 {
     public ClipboardViewModel(ViewModelMain owner)
@@ -18,7 +18,7 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
     {
     }
 
-    [Command.Basic("PixiEditor.Clipboard.Cut", "Cut", "Cut selected area/layer", CanExecute = "PixiEditor.Selection.IsNotEmpty", Key = Key.X, Modifiers = ModifierKeys.Control)]
+    [Command.Basic("PixiEditor.Clipboard.Cut", "CUT", "CUT_DESCRIPTIVE", CanExecute = "PixiEditor.Selection.IsNotEmpty", Key = Key.X, Modifiers = ModifierKeys.Control)]
     public void Cut()
     {
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
@@ -28,8 +28,8 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
         doc.Operations.DeleteSelectedPixels(true);
     }
 
-    [Command.Basic("PixiEditor.Clipboard.Paste", false, "Paste", "Paste from clipboard", CanExecute = "PixiEditor.Clipboard.CanPaste", Key = Key.V, Modifiers = ModifierKeys.Shift)]
-    [Command.Basic("PixiEditor.Clipboard.PasteAsNewLayer", true, "Paste as new layer", "Paste from clipboard as new layer", CanExecute = "PixiEditor.Clipboard.CanPaste", IconPath = "$PixiEditor.Clipboard.Paste", Key = Key.V, Modifiers = ModifierKeys.Control)]
+    [Command.Basic("PixiEditor.Clipboard.Paste", false, "PASTE", "PASTE_DESCRIPTIVE", CanExecute = "PixiEditor.Clipboard.CanPaste", Key = Key.V, Modifiers = ModifierKeys.Shift)]
+    [Command.Basic("PixiEditor.Clipboard.PasteAsNewLayer", true, "PASTE_AS_NEW_LAYER", "PASTE_AS_NEW_LAYER_DESCRIPTIVE", CanExecute = "PixiEditor.Clipboard.CanPaste", IconPath = "$PixiEditor.Clipboard.Paste", Key = Key.V, Modifiers = ModifierKeys.Control)]
     public void Paste(bool pasteAsNewLayer)
     {
         if (Owner.DocumentManagerSubViewModel.ActiveDocument is null) 
@@ -37,7 +37,7 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
         ClipboardController.TryPasteFromClipboard(Owner.DocumentManagerSubViewModel.ActiveDocument, pasteAsNewLayer);
     }
     
-    [Command.Basic("PixiEditor.Clipboard.PasteReferenceLayer", "Paste reference layer", "Paste reference layer from clipboard", CanExecute = "PixiEditor.Clipboard.CanPaste", IconPath = "Commands/PixiEditor/Clipboard/Paste.png")]
+    [Command.Basic("PixiEditor.Clipboard.PasteReferenceLayer", "PASTE_REFERENCE_LAYER", "PASTE_REFERENCE_LAYER_DESCRIPTIVE", CanExecute = "PixiEditor.Clipboard.CanPaste", IconPath = "Commands/PixiEditor/Clipboard/Paste.png")]
     public void PasteReferenceLayer(DataObject data)
     {
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
@@ -69,8 +69,8 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
             new VecI(bitmap.PixelWidth, bitmap.PixelHeight));
     }
 
-    [Command.Basic("PixiEditor.Clipboard.PasteColor", false, "Paste color", "Paste color from clipboard", CanExecute = "PixiEditor.Clipboard.CanPasteColor", IconEvaluator = "PixiEditor.Clipboard.PasteColorIcon")]
-    [Command.Basic("PixiEditor.Clipboard.PasteColorAsSecondary", true, "Paste color as secondary", "Paste color as secondary from clipboard", CanExecute = "PixiEditor.Clipboard.CanPasteColor", IconEvaluator = "PixiEditor.Clipboard.PasteColorIcon")]
+    [Command.Basic("PixiEditor.Clipboard.PasteColor", false, "PASTE_COLOR", "PASTE_COLOR_DESCRIPTIVE", CanExecute = "PixiEditor.Clipboard.CanPasteColor", IconEvaluator = "PixiEditor.Clipboard.PasteColorIcon")]
+    [Command.Basic("PixiEditor.Clipboard.PasteColorAsSecondary", true, "PASTE_COLOR_SECONDARY", "PASTE_COLOR_SECONDARY_DESCRIPTIVE", CanExecute = "PixiEditor.Clipboard.CanPasteColor", IconEvaluator = "PixiEditor.Clipboard.PasteColorIcon")]
     public void PasteColor(bool secondary)
     {
         if (!ColorHelper.ParseAnyFormat(Clipboard.GetText().Trim(), out var result))
@@ -88,7 +88,7 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
         }
     }
 
-    [Command.Basic("PixiEditor.Clipboard.Copy", "Copy", "Copy to clipboard", CanExecute = "PixiEditor.Selection.IsNotEmpty", Key = Key.C, Modifiers = ModifierKeys.Control)]
+    [Command.Basic("PixiEditor.Clipboard.Copy", "COPY", "COPY_DESCRIPTIVE", CanExecute = "PixiEditor.Selection.IsNotEmpty", Key = Key.C, Modifiers = ModifierKeys.Control)]
     public void Copy()
     {
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
@@ -97,10 +97,10 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
         ClipboardController.CopyToClipboard(doc);
     }
 
-    [Command.Basic("PixiEditor.Clipboard.CopyPrimaryColorAsHex", CopyColor.PrimaryHEX, "Copy primary color (HEX)", "Copy primary color as hex code", IconEvaluator = "PixiEditor.Clipboard.CopyColorIcon")]
-    [Command.Basic("PixiEditor.Clipboard.CopyPrimaryColorAsRgb", CopyColor.PrimaryRGB, "Copy primary color (RGB)", "Copy primary color as RGB code", IconEvaluator = "PixiEditor.Clipboard.CopyColorIcon")]
-    [Command.Basic("PixiEditor.Clipboard.CopySecondaryColorAsHex", CopyColor.SecondaryHEX, "Copy secondary color (HEX)", "Copy secondary color as hex code", IconEvaluator = "PixiEditor.Clipboard.CopyColorIcon")]
-    [Command.Basic("PixiEditor.Clipboard.CopySecondaryColorAsRgb", CopyColor.SecondardRGB, "Copy secondary color (RGB)", "Copy secondary color as RGB code", IconEvaluator = "PixiEditor.Clipboard.CopyColorIcon")]
+    [Command.Basic("PixiEditor.Clipboard.CopyPrimaryColorAsHex", CopyColor.PrimaryHEX, "COPY_COLOR_HEX", "COPY_COLOR_HEX_DESCRIPTIVE", IconEvaluator = "PixiEditor.Clipboard.CopyColorIcon")]
+    [Command.Basic("PixiEditor.Clipboard.CopyPrimaryColorAsRgb", CopyColor.PrimaryRGB, "COPY_COLOR_RGB", "COPY_COLOR_RGB_DESCRIPTIVE", IconEvaluator = "PixiEditor.Clipboard.CopyColorIcon")]
+    [Command.Basic("PixiEditor.Clipboard.CopySecondaryColorAsHex", CopyColor.SecondaryHEX, "COPY_COLOR_SECONDARY_HEX", "COPY_COLOR_SECONDARY_HEX_DESCRIPTIVE", IconEvaluator = "PixiEditor.Clipboard.CopyColorIcon")]
+    [Command.Basic("PixiEditor.Clipboard.CopySecondaryColorAsRgb", CopyColor.SecondardRGB, "COPY_COLOR_SECONDARY_RGB", "COPY_COLOR_SECONDARY_RGB_DESCRIPTIVE", IconEvaluator = "PixiEditor.Clipboard.CopyColorIcon")]
     public void CopyColorAsHex(CopyColor color)
     {
         var targetColor = color switch

+ 20 - 18
src/PixiEditor/ViewModels/SubViewModels/Main/ColorsViewModel.cs

@@ -3,6 +3,7 @@ using System.Windows.Input;
 using System.Windows.Media;
 using Microsoft.Extensions.DependencyInjection;
 using PixiEditor.Helpers;
+using PixiEditor.Localization;
 using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.DataHolders;
@@ -18,7 +19,7 @@ using Colors = PixiEditor.DrawingApi.Core.ColorsImpl.Colors;
 
 namespace PixiEditor.ViewModels.SubViewModels.Main;
 
-[Command.Group("PixiEditor.Colors", "Palette Colors")]
+[Command.Group("PixiEditor.Colors", "PALETTE_COLORS")]
 internal class ColorsViewModel : SubViewModel<ViewModelMain>
 {
     public RelayCommand<List<string>> ImportPaletteCommand { get; set; }
@@ -82,8 +83,8 @@ internal class ColorsViewModel : SubViewModel<ViewModelMain>
         doc.Operations.ReplaceColor(colors.oldColor, colors.newColor);
     }
 
-    [Command.Basic("PixiEditor.Colors.ReplaceSecondaryByPrimaryColor", false, "Replace secondary color by primary", "Replace the secondary color by the primary color", IconEvaluator = "PixiEditor.Colors.ReplaceColorIcon")]
-    [Command.Basic("PixiEditor.Colors.ReplacePrimaryBySecondaryColor", true, "Replace primary color by secondary", "Replace the primary color by the secondary color", IconEvaluator = "PixiEditor.Colors.ReplaceColorIcon")]
+    [Command.Basic("PixiEditor.Colors.ReplaceSecondaryByPrimaryColor", false, "REPLACE_SECONDARY_BY_PRIMARY", "REPLACE_SECONDARY_BY_PRIMARY", IconEvaluator = "PixiEditor.Colors.ReplaceColorIcon")]
+    [Command.Basic("PixiEditor.Colors.ReplacePrimaryBySecondaryColor", true, "REPLACE_PRIMARY_BY_SECONDARY", "REPLACE_PRIMARY_BY_SECONDARY_DESCRIPTIVE", IconEvaluator = "PixiEditor.Colors.ReplaceColorIcon")]
     public void ReplaceColors(bool replacePrimary)
     {
         var oldColor = replacePrimary ? PrimaryColor : SecondaryColor;
@@ -130,7 +131,7 @@ internal class ColorsViewModel : SubViewModel<ViewModelMain>
         await ImportLospecPalette();
     }
 
-    [Command.Basic("PixiEditor.Colors.OpenPaletteBrowser", "Open Palette Browser", "Open Palette Browser", CanExecute = "PixiEditor.HasDocument", IconPath = "Globe.png")]
+    [Command.Basic("PixiEditor.Colors.OpenPaletteBrowser", "OPEN_PALETTE_BROWSER", "OPEN_PALETTE_BROWSER", CanExecute = "PixiEditor.HasDocument", IconPath = "Globe.png")]
     public void OpenPalettesBrowser() 
     {
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
@@ -155,7 +156,8 @@ internal class ColorsViewModel : SubViewModel<ViewModelMain>
                 if (LocalPalettesFetcher.PaletteExists(palette.Name))
                 {
                     var consent = ConfirmationDialog.Show(
-                        $"Palette '{palette.Name}' already exists, do you want to overwrite it?", "Palette exists");
+                        new LocalizedString("OVERWRITE_PALETTE_CONSENT", palette.Name),
+                        new LocalizedString("PALETTE_EXISTS"));
                     if (consent == ConfirmationType.No)
                     {
                         palette.Name = LocalPalettesFetcher.GetNonExistingName(palette.Name);
@@ -210,14 +212,14 @@ internal class ColorsViewModel : SubViewModel<ViewModelMain>
         if (doc is null)
             return;
 
-        if (ConfirmationDialog.Show("Replace current palette with selected one?", "Replace current palette") == ConfirmationType.Yes)
+        if (ConfirmationDialog.Show(new LocalizedString("REPLACE_PALETTE_CONSENT"), new LocalizedString("REPLACE_PALETTE")) == ConfirmationType.Yes)
         {
             if (doc.Palette is null)
             {
                 doc.Palette = new WpfObservableRangeCollection<DrawingApi.Core.ColorsImpl.Color>();
             }
 
-            doc.Palette.ReplaceRange(palette.Select(x => Color.Parse(x)));
+            doc.Palette.ReplaceRange(palette.Select(Color.Parse));
         }
     }
 
@@ -263,16 +265,16 @@ internal class ColorsViewModel : SubViewModel<ViewModelMain>
         return ColorSearchResult.GetIcon(color);
     }
 
-    [Command.Basic("PixiEditor.Colors.SelectFirstPaletteColor", "Select color 1", "Select the first color in the palette", Key = Key.D1, Parameter = 0, CanExecute = "PixiEditor.Colors.CanSelectPaletteColor", IconEvaluator = "PixiEditor.Colors.FirstPaletteColorIcon")]
-    [Command.Basic("PixiEditor.Colors.SelectSecondPaletteColor", "Select color 2", "Select the second color in the palette", Key = Key.D2, Parameter = 1, CanExecute = "PixiEditor.Colors.CanSelectPaletteColor", IconEvaluator = "PixiEditor.Colors.SecondPaletteColorIcon")]
-    [Command.Basic("PixiEditor.Colors.SelectThirdPaletteColor", "Select color 3", "Select the third color in the palette", Key = Key.D3, Parameter = 2, CanExecute = "PixiEditor.Colors.CanSelectPaletteColor", IconEvaluator = "PixiEditor.Colors.ThirdPaletteColorIcon")]
-    [Command.Basic("PixiEditor.Colors.SelectFourthPaletteColor", "Select color 4", "Select the fourth color in the palette", Key = Key.D4, Parameter = 3, CanExecute = "PixiEditor.Colors.CanSelectPaletteColor", IconEvaluator = "PixiEditor.Colors.FourthPaletteColorIcon")]
-    [Command.Basic("PixiEditor.Colors.SelectFifthPaletteColor", "Select color 5", "Select the fifth color in the palette", Key = Key.D5, Parameter = 4, CanExecute = "PixiEditor.Colors.CanSelectPaletteColor", IconEvaluator = "PixiEditor.Colors.FifthPaletteColorIcon")]
-    [Command.Basic("PixiEditor.Colors.SelectSixthPaletteColor", "Select color 6", "Select the sixth color in the palette", Key = Key.D6, Parameter = 5, CanExecute = "PixiEditor.Colors.CanSelectPaletteColor", IconEvaluator = "PixiEditor.Colors.SixthPaletteColorIcon")]
-    [Command.Basic("PixiEditor.Colors.SelectSeventhPaletteColor", "Select color 7", "Select the seventh color in the palette", Key = Key.D7, Parameter = 6, CanExecute = "PixiEditor.Colors.CanSelectPaletteColor", IconEvaluator = "PixiEditor.Colors.SeventhPaletteColorIcon")]
-    [Command.Basic("PixiEditor.Colors.SelectEighthPaletteColor", "Select color 8", "Select the eighth color in the palette", Key = Key.D8, Parameter = 7, CanExecute = "PixiEditor.Colors.CanSelectPaletteColor", IconEvaluator = "PixiEditor.Colors.EighthPaletteColorIcon")]
-    [Command.Basic("PixiEditor.Colors.SelectNinthPaletteColor", "Select color 9", "Select the ninth color in the palette", Key = Key.D9, Parameter = 8, CanExecute = "PixiEditor.Colors.CanSelectPaletteColor", IconEvaluator = "PixiEditor.Colors.NinthPaletteColorIcon")]
-    [Command.Basic("PixiEditor.Colors.SelectTenthPaletteColor", "Select color 10", "Select the tenth color in the palette", Key = Key.D0, Parameter = 9, CanExecute = "PixiEditor.Colors.CanSelectPaletteColor", IconEvaluator = "PixiEditor.Colors.TenthPaletteColorIcon")]
+    [Command.Basic("PixiEditor.Colors.SelectFirstPaletteColor", "SELECT_COLOR_1", "SELECT_COLOR_1_DESCRIPTIVE", Key = Key.D1, Parameter = 0, CanExecute = "PixiEditor.Colors.CanSelectPaletteColor", IconEvaluator = "PixiEditor.Colors.FirstPaletteColorIcon")]
+    [Command.Basic("PixiEditor.Colors.SelectSecondPaletteColor", "SELECT_COLOR_2", "SELECT_COLOR_2_DESCRIPTIVE", Key = Key.D2, Parameter = 1, CanExecute = "PixiEditor.Colors.CanSelectPaletteColor", IconEvaluator = "PixiEditor.Colors.SecondPaletteColorIcon")]
+    [Command.Basic("PixiEditor.Colors.SelectThirdPaletteColor", "SELECT_COLOR_3", "SELECT_COLOR_3_DESCRIPTIVE", Key = Key.D3, Parameter = 2, CanExecute = "PixiEditor.Colors.CanSelectPaletteColor", IconEvaluator = "PixiEditor.Colors.ThirdPaletteColorIcon")]
+    [Command.Basic("PixiEditor.Colors.SelectFourthPaletteColor", "SELECT_COLOR_4", "SELECT_COLOR_4_DESCRIPTIVE", Key = Key.D4, Parameter = 3, CanExecute = "PixiEditor.Colors.CanSelectPaletteColor", IconEvaluator = "PixiEditor.Colors.FourthPaletteColorIcon")]
+    [Command.Basic("PixiEditor.Colors.SelectFifthPaletteColor", "SELECT_COLOR_5", "SELECT_COLOR_5_DESCRIPTIVE", Key = Key.D5, Parameter = 4, CanExecute = "PixiEditor.Colors.CanSelectPaletteColor", IconEvaluator = "PixiEditor.Colors.FifthPaletteColorIcon")]
+    [Command.Basic("PixiEditor.Colors.SelectSixthPaletteColor", "SELECT_COLOR_6", "SELECT_COLOR_6_DESCRIPTIVE", Key = Key.D6, Parameter = 5, CanExecute = "PixiEditor.Colors.CanSelectPaletteColor", IconEvaluator = "PixiEditor.Colors.SixthPaletteColorIcon")]
+    [Command.Basic("PixiEditor.Colors.SelectSeventhPaletteColor", "SELECT_COLOR_7", "SELECT_COLOR_7_DESCRIPTIVE", Key = Key.D7, Parameter = 6, CanExecute = "PixiEditor.Colors.CanSelectPaletteColor", IconEvaluator = "PixiEditor.Colors.SeventhPaletteColorIcon")]
+    [Command.Basic("PixiEditor.Colors.SelectEighthPaletteColor", "SELECT_COLOR_8", "SELECT_COLOR_8_DESCRIPTIVE", Key = Key.D8, Parameter = 7, CanExecute = "PixiEditor.Colors.CanSelectPaletteColor", IconEvaluator = "PixiEditor.Colors.EighthPaletteColorIcon")]
+    [Command.Basic("PixiEditor.Colors.SelectNinthPaletteColor", "SELECT_COLOR_9", "SELECT_COLOR_9_DESCRIPTIVE", Key = Key.D9, Parameter = 8, CanExecute = "PixiEditor.Colors.CanSelectPaletteColor", IconEvaluator = "PixiEditor.Colors.NinthPaletteColorIcon")]
+    [Command.Basic("PixiEditor.Colors.SelectTenthPaletteColor", "SELECT_COLOR_10", "SELECT_COLOR_10_DESCRIPTIVE", Key = Key.D0, Parameter = 9, CanExecute = "PixiEditor.Colors.CanSelectPaletteColor", IconEvaluator = "PixiEditor.Colors.TenthPaletteColorIcon")]
     public void SelectPaletteColor(int index)
     {
         var document = Owner.DocumentManagerSubViewModel.ActiveDocument;
@@ -282,7 +284,7 @@ internal class ColorsViewModel : SubViewModel<ViewModelMain>
         }
     }
 
-    [Command.Basic("PixiEditor.Colors.Swap", "Swap colors", "Swap primary and secondary colors", Key = Key.X)]
+    [Command.Basic("PixiEditor.Colors.Swap", "SWAP_COLORS", "SWAP_COLORS_DESCRIPTIVE", Key = Key.X)]
     public void SwapColors(object parameter)
     {
         (PrimaryColor, SecondaryColor) = (SecondaryColor, PrimaryColor);

+ 68 - 19
src/PixiEditor/ViewModels/SubViewModels/Main/DebugViewModel.cs

@@ -5,16 +5,18 @@ using System.Windows.Input;
 using Microsoft.Win32;
 using Newtonsoft.Json;
 using PixiEditor.Helpers;
+using PixiEditor.Localization;
 using PixiEditor.Models.Commands.Attributes;
 using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.Models.Commands.Templates.Parsers;
 using PixiEditor.Models.Dialogs;
+using PixiEditor.Models.Enums;
 using PixiEditor.Models.UserPreferences;
 using PixiEditor.Views.Dialogs;
 
 namespace PixiEditor.ViewModels.SubViewModels.Main;
 
-[Command.Group("PixiEditor.Debug", "Debug")]
+[Command.Group("PixiEditor.Debug", "DEBUG")]
 internal class DebugViewModel : SubViewModel<ViewModelMain>
 {
     public bool IsDebugBuild { get; set; }
@@ -28,6 +30,34 @@ internal class DebugViewModel : SubViewModel<ViewModelMain>
         set => SetProperty(ref useDebug, value);
     }
 
+    private LocalizationKeyShowMode localizationKeyShowMode;
+
+    public LocalizationKeyShowMode LocalizationKeyShowMode
+    {
+        get => localizationKeyShowMode;
+        set
+        {
+            if (SetProperty(ref localizationKeyShowMode, value))
+            {
+                Owner.LocalizationProvider.ReloadLanguage();
+            }
+        }
+    }
+
+    private bool forceOtherFlowDirection;
+    
+    public bool ForceOtherFlowDirection
+    {
+        get => forceOtherFlowDirection;
+        set
+        {
+            if (SetProperty(ref forceOtherFlowDirection, value))
+            {
+                Owner.LocalizationProvider.ReloadLanguage();
+            }
+        }
+    }
+
     public DebugViewModel(ViewModelMain owner, IPreferences preferences)
         : base(owner)
     {
@@ -40,36 +70,37 @@ internal class DebugViewModel : SubViewModel<ViewModelMain>
     {
         if (!Directory.Exists(path))
         {
-            NoticeDialog.Show($"{path} does not exist.", "Location does not exist");
+            NoticeDialog.Show(new LocalizedString("PATH_DOES_NOT_EXIST", path), "LOCATION_DOES_NOT_EXIST");
             return;
         }
 
         ProcessHelpers.ShellExecuteEV(path);
     }
+    
 
-    [Command.Debug("PixiEditor.Debug.OpenLocalAppDataDirectory", @"PixiEditor", "Open Local AppData Directory", "Open Local AppData Directory", IconPath = "Folder.png")]
-    [Command.Debug("PixiEditor.Debug.OpenCrashReportsDirectory", @"PixiEditor\crash_logs", "Open Crash Reports Directory", "Open Crash Reports Directory", IconPath = "Folder.png")]
+    [Command.Debug("PixiEditor.Debug.OpenLocalAppDataDirectory", @"%LocalAppData%\PixiEditor", "OPEN_LOCAL_APPDATA_DIR", "OPEN_LOCAL_APPDATA_DIR", IconPath = "Folder.png")]
+    [Command.Debug("PixiEditor.Debug.OpenCrashReportsDirectory", @"%LocalAppData%\PixiEditor\crash_logs", "OPEN_CRASH_REPORTS_DIR", "OPEN_CRASH_REPORTS_DIR", IconPath = "Folder.png")]
     public static void OpenLocalAppDataFolder(string subDirectory)
     {
         var path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), subDirectory);
         OpenFolder(path);
     }
 
-    [Command.Debug("PixiEditor.Debug.OpenRoamingAppDataDirectory", @"PixiEditor", "Open Roaming AppData Directory", "Open Roaming AppData Directory", IconPath = "Folder.png")]
+    [Command.Debug("PixiEditor.Debug.OpenRoamingAppDataDirectory", @"%AppData%\PixiEditor", "OPEN_ROAMING_APPDATA_DIR", "OPEN_ROAMING_APPDATA_DIR", IconPath = "Folder.png")]
     public static void OpenAppDataFolder(string subDirectory)
     {
         var path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), subDirectory);
         OpenFolder(path);
     }
 
-    [Command.Debug("PixiEditor.Debug.OpenTempDirectory", @"PixiEditor", "Open Temp Directory", "Open Temp Directory", IconPath = "Folder.png")]
+    [Command.Debug("PixiEditor.Debug.OpenTempDirectory", @"%Temp%\PixiEditor", "OPEN_TEMP_DIR", "OPEN_TEMP_DIR", IconPath = "Folder.png")]
     public static void OpenTempFolder(string subDirectory)
     {
         var path = Path.Combine(Path.GetTempPath(), subDirectory);
         OpenFolder(path);
     }
 
-    [Command.Debug("PixiEditor.Debug.DumpAllCommands", "Dump All Commands", "Dump All Commands to a text file")]
+    [Command.Debug("PixiEditor.Debug.DumpAllCommands", "DUMP_ALL_COMMANDS", "DUMP_ALL_COMMANDS_DESCRIPTIVE")]
     public void DumpAllCommands()
     {
         SaveFileDialog dialog = new SaveFileDialog();
@@ -89,7 +120,7 @@ internal class DebugViewModel : SubViewModel<ViewModelMain>
         }
     }
     
-    [Command.Debug("PixiEditor.Debug.GenerateKeysTemplate", "Generate key bindings template", "Generates key bindings json template")]
+    [Command.Debug("PixiEditor.Debug.GenerateKeysTemplate", "GENERATE_KEY_BINDINGS_TEMPLATE", "GENERATE_KEY_BINDINGS_TEMPLATE_DESCRIPTIVE")]
     public void GenerateKeysTemplate()
     {
         SaveFileDialog dialog = new SaveFileDialog();
@@ -122,7 +153,7 @@ internal class DebugViewModel : SubViewModel<ViewModelMain>
         }
     }
 
-    [Command.Debug("PixiEditor.Debug.ValidateShortcutMap", "Validate Shortcut Map", "Validates shortcut map")]
+    [Command.Debug("PixiEditor.Debug.ValidateShortcutMap", "VALIDATE_SHORTCUT_MAP", "VALIDATE_SHORTCUT_MAP_DESCRIPTIVE")]
     public void ValidateShortcutMap()
     {
         OpenFileDialog dialog = new OpenFileDialog();
@@ -145,18 +176,18 @@ internal class DebugViewModel : SubViewModel<ViewModelMain>
                 }
             }
 
-            NoticeDialog.Show($"Empty keys: {emptyKeys}\nUnknown Commands: {unknownCommands}", "Result");
+            NoticeDialog.Show(new LocalizedString("VALIDATION_KEYS_NOTICE_DIALOG", emptyKeys, unknownCommands), "RESULT");
         }
     }
 
-    [Command.Debug("PixiEditor.Debug.ClearRecentDocument", "Clear recent documents", "Clear recently opened documents")]
+    [Command.Debug("PixiEditor.Debug.ClearRecentDocument", "CLEAR_RECENT_DOCUMENTS", "CLEAR_RECENTLY_OPENED_DOCUMENTS")]
     public void ClearRecentDocuments()
     {
         Owner.FileSubViewModel.RecentlyOpened.Clear();
         IPreferences.Current.UpdateLocalPreference(PreferencesConstants.RecentlyOpened, Array.Empty<object>());
     }
 
-    [Command.Debug("PixiEditor.Debug.OpenCommandDebugWindow", "Open command debug window", "Open command debug window")]
+    [Command.Debug("PixiEditor.Debug.OpenCommandDebugWindow", "OPEN_CMD_DEBUG_WINDOW", "OPEN_CMD_DEBUG_WINDOW")]
     public void OpenCommandDebugWindow()
     {
         Mouse.OverrideCursor = Cursors.Wait;
@@ -164,28 +195,46 @@ internal class DebugViewModel : SubViewModel<ViewModelMain>
         Mouse.OverrideCursor = null;
     }
 
-    [Command.Debug("PixiEditor.Debug.OpenInstallDirectory", "Open Installation Directory", "Open Installation Directory", IconPath = "Folder.png")]
+    [Command.Debug("PixiEditor.Debug.OpenLocalizationDebugWindow", "OPEN_LOCALIZATION_DEBUG_WINDOW", "OPEN_LOCALIZATION_DEBUG_WINDOW")]
+    public void OpenLocalizationDebugWindow()
+    {
+        new LocalizationDebugWindow().Show();
+    }
+
+    [Command.Internal("PixiEditor.Debug.SetLanguageFromFilePicker")]
+    public void SetLanguageFromFilePicker()
+    {
+        var file = new OpenFileDialog { Filter = "key-value json (*.json)|*.json" };
+
+        if (file.ShowDialog().GetValueOrDefault())
+        {
+            Owner.LocalizationProvider.LoadDebugKeys(
+                JsonConvert.DeserializeObject<Dictionary<string, string>>(File.ReadAllText(file.FileName)), false);
+        }
+    }
+
+    [Command.Debug("PixiEditor.Debug.OpenInstallDirectory", "OPEN_INSTALLATION_DIR", "OPEN_INSTALLATION_DIR", IconPath = "Folder.png")]
     public static void OpenInstallLocation()
     {
         ProcessHelpers.ShellExecuteEV(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location));
     }
 
-    [Command.Debug("PixiEditor.Debug.Crash", "Crash", "Crash Application")]
+    [Command.Debug("PixiEditor.Debug.Crash", "CRASH", "CRASH_APP")]
     public static void Crash() => throw new InvalidOperationException("User requested to crash :c");
 
-    [Command.Debug("PixiEditor.Debug.DeleteUserPreferences", @"%appdata%\PixiEditor\user_preferences.json", "Delete User Preferences (Roaming)", "Delete User Preferences (Roaming AppData)")]
-    [Command.Debug("PixiEditor.Debug.DeleteShortcutFile", @"%appdata%\PixiEditor\shortcuts.json", "Delete Shortcut File (Roaming)", "Delete Shortcut File (Roaming AppData)")]
-    [Command.Debug("PixiEditor.Debug.DeleteEditorData", @"%localappdata%\PixiEditor\editor_data.json", "Delete Editor Data (Local)", "Delete Editor Data (Local AppData)")]
+    [Command.Debug("PixiEditor.Debug.DeleteUserPreferences", @"%appdata%\PixiEditor\user_preferences.json", "DELETE_USR_PREFS", "DELETE_USR_PREFS")]
+    [Command.Debug("PixiEditor.Debug.DeleteShortcutFile", @"%appdata%\PixiEditor\shortcuts.json", "DELETE_SHORTCUT_FILE", "DELETE_SHORTCUT_FILE")]
+    [Command.Debug("PixiEditor.Debug.DeleteEditorData", @"%localappdata%\PixiEditor\editor_data.json", "DELETE_EDITOR_DATA", "DELETE_EDITOR_DATA")]
     public static void DeleteFile(string path)
     {
         string file = Environment.ExpandEnvironmentVariables(path);
         if (!File.Exists(file))
         {
-            NoticeDialog.Show($"File {path} does not exist\n(Full Path: {file})", "File not found");
+            NoticeDialog.Show(new LocalizedString("File {0} does not exist\n(Full Path: {1})", path, file), "FILE_NOT_FOUND");
             return;
         }
 
-        OptionsDialog<string> dialog = new("Are you sure?", $"Are you sure you want to delete {path}?\nThis data will be lost for all installations.\n(Full Path: {file})")
+        OptionsDialog<string> dialog = new("ARE_YOU_SURE", new LocalizedString("ARE_YOU_SURE_PATH_FULL_PATH", path, file))
         {
             { "Yes", x => File.Delete(file) },
             "Cancel"

+ 5 - 3
src/PixiEditor/ViewModels/SubViewModels/Main/DiscordViewModel.cs

@@ -1,5 +1,6 @@
 using System.ComponentModel;
 using DiscordRPC;
+using PixiEditor.Localization;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.Events;
 using PixiEditor.Models.UserPreferences;
@@ -118,7 +119,8 @@ internal class DiscordViewModel : SubViewModel<ViewModelMain>, IDisposable
         {
             richPresence.WithTimestamps(new Timestamps(document.OpenedUTC));
 
-            richPresence.Details = ShowDocumentName ? $"Editing {document.FileName}".Limit(128) : "Editing an image";
+            richPresence.Details = ShowDocumentName
+                ? $"Editing {document.FileName.Limit(128)}" : "Editing an image";
 
             string state = string.Empty;
 
@@ -135,7 +137,7 @@ internal class DiscordViewModel : SubViewModel<ViewModelMain>, IDisposable
             if (ShowLayerCount)
             {
                 int count = CountLayers(document.StructureRoot);
-                state += count == 1 ? "1 Layer" : $"{count} Layers";
+                state += count == 1 ? "1 layer" : $"{count} layers";
             }
 
             richPresence.State = state;
@@ -175,7 +177,7 @@ internal class DiscordViewModel : SubViewModel<ViewModelMain>, IDisposable
                 LargeImageKey = "editorlogo",
                 LargeImageText = "You've discovered PixiEditor's logo",
                 SmallImageKey = "github",
-                SmallImageText = "Download PixiEditor on GitHub (github.com/PixiEditor/PixiEditor)!"
+                SmallImageText = "Download PixiEditor (pixieditor.net/download)!"
             },
             Timestamps = new Timestamps()
             {

+ 16 - 15
src/PixiEditor/ViewModels/SubViewModels/Main/FileViewModel.cs

@@ -11,6 +11,7 @@ using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.Exceptions;
 using PixiEditor.Helpers;
+using PixiEditor.Localization;
 using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.DataHolders;
@@ -23,7 +24,7 @@ using PixiEditor.Views.Dialogs;
 
 namespace PixiEditor.ViewModels.SubViewModels.Main;
 
-[Command.Group("PixiEditor.File", "File")]
+[Command.Group("PixiEditor.File", "FILE")]
 internal class FileViewModel : SubViewModel<ViewModelMain>
 {
     private bool hasRecent;
@@ -112,7 +113,7 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
         string path = (string)parameter;
         if (!File.Exists(path))
         {
-            NoticeDialog.Show("The file does not exist", "Failed to open the file");
+            NoticeDialog.Show("FILE_NOT_FOUND", "FAILED_TO_OPEN_FILE");
             RecentlyOpened.Remove(path);
             IPreferences.Current.UpdateLocalPreference(PreferencesConstants.RecentlyOpened, RecentlyOpened.Select(x => x.FilePath));
             return;
@@ -121,7 +122,7 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
         OpenFromPath(path);
     }
 
-    [Command.Basic("PixiEditor.File.Open", "Open", "Open file", Key = Key.O, Modifiers = ModifierKeys.Control)]
+    [Command.Basic("PixiEditor.File.Open", "OPEN", "OPEN_FILE", Key = Key.O, Modifiers = ModifierKeys.Control)]
     public void OpenFromOpenFileDialog()
     {
         string filter = SupportedFilesHelper.BuildOpenFilter();
@@ -173,11 +174,11 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
         }
         catch (CorruptedFileException ex)
         {
-            NoticeDialog.Show(ex.Message, "Failed to open the file");
+            NoticeDialog.Show(ex.Message, "FAILED_TO_OPEN_FILE");
         }
         catch (OldFileFormatException)
         {
-            NoticeDialog.Show("This .pixi file uses the old format,\n which is no longer supported and can't be opened.", "Old file format");
+            NoticeDialog.Show("OLD_FILE_FORMAT_DESCRIPTION", "OLD_FILE_FORMAT");
         }
     }
 
@@ -226,7 +227,7 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
         }
     }
 
-    [Command.Basic("PixiEditor.File.New", "New image", "Create new image", Key = Key.N, Modifiers = ModifierKeys.Control)]
+    [Command.Basic("PixiEditor.File.New", "NEW_IMAGE", "CREATE_NEW_IMAGE", Key = Key.N, Modifiers = ModifierKeys.Control)]
     public void CreateFromNewFileDialog()
     {
         NewFileDialog newFile = new NewFileDialog();
@@ -235,7 +236,7 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
             NewDocument(b => b
                 .WithSize(newFile.Width, newFile.Height)
                 .WithLayer(l => l
-                    .WithName("Base Layer")
+                    .WithName(new LocalizedString("BASE_LAYER_NAME"))
                     .WithSurface(new Surface(new VecI(newFile.Width, newFile.Height)))));
         }
     }
@@ -254,8 +255,8 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
         Owner.WindowSubViewModel.MakeDocumentViewportActive(doc);
     }
 
-    [Command.Basic("PixiEditor.File.Save", false, "Save", "Save image", CanExecute = "PixiEditor.HasDocument", Key = Key.S, Modifiers = ModifierKeys.Control, IconPath = "Save.png")]
-    [Command.Basic("PixiEditor.File.SaveAsNew", true, "Save as...", "Save image as new", CanExecute = "PixiEditor.HasDocument", Key = Key.S, Modifiers = ModifierKeys.Control | ModifierKeys.Shift, IconPath = "Save.png")]
+    [Command.Basic("PixiEditor.File.Save", false, "SAVE", "SAVE_IMAGE", CanExecute = "PixiEditor.HasDocument", Key = Key.S, Modifiers = ModifierKeys.Control, IconPath = "Save.png")]
+    [Command.Basic("PixiEditor.File.SaveAsNew", true, "SAVE_AS", "SAVE_IMAGE_AS", CanExecute = "PixiEditor.HasDocument", Key = Key.S, Modifiers = ModifierKeys.Control | ModifierKeys.Shift, IconPath = "Save.png")]
     public bool SaveActiveDocument(bool asNew)
     {
         DocumentViewModel doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
@@ -300,7 +301,7 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
     ///     Generates export dialog or saves directly if save data is known.
     /// </summary>
     /// <param name="parameter">CommandProperty.</param>
-    [Command.Basic("PixiEditor.File.Export", "Export", "Export image", CanExecute = "PixiEditor.HasDocument", Key = Key.S, Modifiers = ModifierKeys.Control | ModifierKeys.Alt | ModifierKeys.Shift)]
+    [Command.Basic("PixiEditor.File.Export", "EXPORT", "EXPORT_IMAGE", CanExecute = "PixiEditor.HasDocument", Key = Key.S, Modifiers = ModifierKeys.Control | ModifierKeys.Alt | ModifierKeys.Shift)]
     public void ExportFile()
     {
         DocumentViewModel doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
@@ -323,19 +324,19 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
         switch (result)
         {
             case DialogSaveResult.InvalidPath:
-                NoticeDialog.Show(title: "Error", message: "Couldn't save the file to the specified location");
+                NoticeDialog.Show("ERROR", "ERROR_SAVE_LOCATION");
                 break;
             case DialogSaveResult.ConcurrencyError:
-                NoticeDialog.Show(title: "Internal error", message: "An internal error occured while saving. Please try again.");
+                NoticeDialog.Show("INTERNAL_ERROR", "ERROR_WHILE_SAVING");
                 break;
             case DialogSaveResult.SecurityError:
-                NoticeDialog.Show(title: "Security error", message: "No rights to write to the specified location.");
+                NoticeDialog.Show(title: "SECURITY_ERROR", message: "SECURITY_ERROR_MSG");
                 break;
             case DialogSaveResult.IoError:
-                NoticeDialog.Show(title: "IO error", message: "Error while writing to disk.");
+                NoticeDialog.Show(title: "IO_ERROR", message: "IO_ERROR_MSG");
                 break;
             case DialogSaveResult.UnknownError:
-                NoticeDialog.Show(title: "Error", message: "An error occured while saving.");
+                NoticeDialog.Show("ERROR", "UNKNOWN_ERROR_SAVING");
                 break;
         }
     }

+ 24 - 23
src/PixiEditor/ViewModels/SubViewModels/Main/LayersViewModel.cs

@@ -6,6 +6,7 @@ using System.Windows.Media.Imaging;
 using Microsoft.Win32;
 using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.Localization;
 using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.Dialogs;
@@ -15,7 +16,7 @@ using PixiEditor.ViewModels.SubViewModels.Document;
 
 namespace PixiEditor.ViewModels.SubViewModels.Main;
 #nullable enable
-[Command.Group("PixiEditor.Layer", "Layer")]
+[Command.Group("PixiEditor.Layer", "LAYER")]
 internal class LayersViewModel : SubViewModel<ViewModelMain>
 {
     public LayersViewModel(ViewModelMain owner)
@@ -42,7 +43,7 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
         return true;
     }
 
-    [Command.Basic("PixiEditor.Layer.DeleteSelected", "Delete active layer/folder", "Delete active layer or folder", CanExecute = "PixiEditor.Layer.CanDeleteSelected", IconPath = "Trash.png")]
+    [Command.Basic("PixiEditor.Layer.DeleteSelected", "LAYER_DELETE_SELECTED", "LAYER_DELETE_SELECTED_DESCRIPTIVE", CanExecute = "PixiEditor.Layer.CanDeleteSelected", IconPath = "Trash.png")]
     public void DeleteSelected()
     {
         var member = Owner.DocumentManagerSubViewModel.ActiveDocument?.SelectedStructureMember;
@@ -84,7 +85,7 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
         return members;
     }
 
-    [Command.Basic("PixiEditor.Layer.DeleteAllSelected", "Delete all selected layers/folders", "Delete all selected layers and/or folders", CanExecute = "PixiEditor.Layer.HasSelectedMembers", IconPath = "Trash.png")]
+    [Command.Basic("PixiEditor.Layer.DeleteAllSelected", "LAYER_DELETE_ALL_SELECTED", "LAYER_DELETE_ALL_SELECTED_DESCRIPTIVE", CanExecute = "PixiEditor.Layer.HasSelectedMembers", IconPath = "Trash.png")]
     public void DeleteAllSelected()
     {
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
@@ -95,7 +96,7 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
             doc.Operations.DeleteStructureMembers(selected);
     }
 
-    [Command.Basic("PixiEditor.Layer.NewFolder", "New Folder", "Create new folder", CanExecute = "PixiEditor.Layer.CanCreateNewMember", IconPath = "Folder-add.png")]
+    [Command.Basic("PixiEditor.Layer.NewFolder", "NEW_FOLDER", "CREATE_NEW_FOLDER", CanExecute = "PixiEditor.Layer.CanCreateNewMember", IconPath = "Folder-add.png")]
     public void NewFolder()
     {
         if (Owner.DocumentManagerSubViewModel.ActiveDocument is not { } doc)
@@ -103,7 +104,7 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
         doc.Operations.CreateStructureMember(StructureMemberType.Folder);
     }
 
-    [Command.Basic("PixiEditor.Layer.NewLayer", "New Layer", "Create new layer", CanExecute = "PixiEditor.Layer.CanCreateNewMember", Key = Key.N, Modifiers = ModifierKeys.Control | ModifierKeys.Shift, IconPath = "Layer-add.png")]
+    [Command.Basic("PixiEditor.Layer.NewLayer", "NEW_LAYER", "CREATE_NEW_LAYER", CanExecute = "PixiEditor.Layer.CanCreateNewMember", Key = Key.N, Modifiers = ModifierKeys.Control | ModifierKeys.Shift, IconPath = "Layer-add.png")]
     public void NewLayer()
     {
         if (Owner.DocumentManagerSubViewModel.ActiveDocument is not { } doc)
@@ -156,7 +157,7 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
         }
     }
 
-    [Command.Basic("PixiEditor.Layer.DuplicateSelectedLayer", "Duplicate selected layer", "Duplicate selected layer", CanExecute = "PixiEditor.Layer.SelectedMemberIsLayer")]
+    [Command.Basic("PixiEditor.Layer.DuplicateSelectedLayer", "DUPLICATE_SELECTED_LAYER", "DUPLICATE_SELECTED_LAYER", CanExecute = "PixiEditor.Layer.SelectedMemberIsLayer")]
     public void DuplicateLayer()
     {
         var member = Owner.DocumentManagerSubViewModel.ActiveDocument?.SelectedStructureMember;
@@ -221,7 +222,7 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
     [Evaluator.CanExecute("PixiEditor.Layer.ActiveLayerHasNoMask")]
     public bool ActiveLayerHasNoMask() => !Owner.DocumentManagerSubViewModel.ActiveDocument?.SelectedStructureMember?.HasMaskBindable ?? false;
 
-    [Command.Basic("PixiEditor.Layer.CreateMask", "Create mask", "Create mask", CanExecute = "PixiEditor.Layer.ActiveLayerHasNoMask", IconPath = "Create-mask.png")]
+    [Command.Basic("PixiEditor.Layer.CreateMask", "CREATE_MASK", "CREATE_MASK", CanExecute = "PixiEditor.Layer.ActiveLayerHasNoMask", IconPath = "Create-mask.png")]
     public void CreateMask()
     {
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
@@ -231,7 +232,7 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
         doc!.Operations.CreateMask(member);
     }
 
-    [Command.Basic("PixiEditor.Layer.DeleteMask", "Delete mask", "Delete mask", CanExecute = "PixiEditor.Layer.ActiveLayerHasMask", IconPath = "Trash.png")]
+    [Command.Basic("PixiEditor.Layer.DeleteMask", "DELETE_MASK", "DELETE_MASK", CanExecute = "PixiEditor.Layer.ActiveLayerHasMask", IconPath = "Trash.png")]
     public void DeleteMask()
     {
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
@@ -241,7 +242,7 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
         doc!.Operations.DeleteMask(member);
     }
 
-    [Command.Basic("PixiEditor.Layer.ToggleMask", "Toggle mask", "Toggle mask", CanExecute = "PixiEditor.Layer.ActiveLayerHasMask")]
+    [Command.Basic("PixiEditor.Layer.ToggleMask", "TOGGLE_MASK", "TOGGLE_MASK", CanExecute = "PixiEditor.Layer.ActiveLayerHasMask")]
     public void ToggleMask()
     {
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
@@ -252,7 +253,7 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
         member.MaskIsVisibleBindable = !member.MaskIsVisibleBindable;
     }
     
-    [Command.Basic("PixiEditor.Layer.ApplyMask", "Apply mask", "Apply mask", CanExecute = "PixiEditor.Layer.ActiveLayerHasMask")]
+    [Command.Basic("PixiEditor.Layer.ApplyMask", "APPLY_MASK", "APPLY_MASK", CanExecute = "PixiEditor.Layer.ActiveLayerHasMask")]
     public void ApplyMask()
     {
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
@@ -263,7 +264,7 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
         doc!.Operations.ApplyMask(member);
     }
 
-    [Command.Basic("PixiEditor.Layer.ToggleVisible", "Toggle visibility", "Toggle visibility", CanExecute = "PixiEditor.HasDocument")]
+    [Command.Basic("PixiEditor.Layer.ToggleVisible", "TOGGLE_VISIBILITY", "TOGGLE_VISIBILITY", CanExecute = "PixiEditor.HasDocument")]
     public void ToggleVisible()
     {
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
@@ -279,12 +280,12 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
     [Evaluator.CanExecute("PixiEditor.Layer.HasMemberBelow")]
     public bool HasMemberBelow(object property) => HasSelectedMember(false);
 
-    [Command.Basic("PixiEditor.Layer.MoveSelectedMemberUpwards", "Move selected layer upwards", "Move selected layer or folder upwards", CanExecute = "PixiEditor.Layer.HasMemberAbove")]
+    [Command.Basic("PixiEditor.Layer.MoveSelectedMemberUpwards", "MOVE_MEMBER_UP", "MOVE_MEMBER_UP_DESCRIPTIVE", CanExecute = "PixiEditor.Layer.HasMemberAbove")]
     public void MoveSelectedMemberUpwards() => MoveSelectedMember(true);
-    [Command.Basic("PixiEditor.Layer.MoveSelectedMemberDownwards", "Move selected layer downwards", "Move selected layer or folder downwards", CanExecute = "PixiEditor.Layer.HasMemberBelow")]
+    [Command.Basic("PixiEditor.Layer.MoveSelectedMemberDownwards", "MOVE_MEMBER_DOWN", "MOVE_MEMBER_DOWN_DESCRIPTIVE", CanExecute = "PixiEditor.Layer.HasMemberBelow")]
     public void MoveSelectedMemberDownwards() => MoveSelectedMember(false);
 
-    [Command.Basic("PixiEditor.Layer.MergeSelected", "Merge all selected layers", "Merge all selected layers", CanExecute = "PixiEditor.Layer.HasMultipleSelectedMembers")]
+    [Command.Basic("PixiEditor.Layer.MergeSelected", "MERGE_ALL_SELECTED_LAYERS", "MERGE_ALL_SELECTED_LAYERS", CanExecute = "PixiEditor.Layer.HasMultipleSelectedMembers")]
     public void MergeSelected()
     {
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
@@ -313,10 +314,10 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
         doc.Operations.MergeStructureMembers(new List<Guid> { member.GuidValue, above ? parent.Children[index + 1].GuidValue : parent.Children[index - 1].GuidValue });
     }
 
-    [Command.Basic("PixiEditor.Layer.MergeWithAbove", "Merge selected layer with the one above it", "Merge selected layer with the one above it", CanExecute = "PixiEditor.Layer.HasMemberAbove")]
+    [Command.Basic("PixiEditor.Layer.MergeWithAbove", "MERGE_WITH_ABOVE", "MERGE_WITH_ABOVE_DESCRIPTIVE", CanExecute = "PixiEditor.Layer.HasMemberAbove")]
     public void MergeWithAbove() => MergeSelectedWith(true);
 
-    [Command.Basic("PixiEditor.Layer.MergeWithBelow", "Merge selected layer with the one below it", "Merge selected layer with the one below it", CanExecute = "PixiEditor.Layer.HasMemberBelow", IconPath = "Merge-downwards.png")]
+    [Command.Basic("PixiEditor.Layer.MergeWithBelow", "MERGE_WITH_BELOW", "MERGE_WITH_BELOW_DESCRIPTIVE", CanExecute = "PixiEditor.Layer.HasMemberBelow", IconPath = "Merge-downwards.png")]
     public void MergeWithBelow() => MergeSelectedWith(false);
 
     [Evaluator.CanExecute("PixiEditor.Layer.ReferenceLayerExists")]
@@ -341,7 +342,7 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
         return Owner.ClipboardSubViewModel.CanPaste();
     }
 
-    [Command.Basic("PixiEditor.Layer.ImportReferenceLayer", "Add reference layer", "Add reference layer", CanExecute = "PixiEditor.Layer.ReferenceLayerDoesntExist")]
+    [Command.Basic("PixiEditor.Layer.ImportReferenceLayer", "ADD_REFERENCE_LAYER", "ADD_REFERENCE_LAYER", CanExecute = "PixiEditor.Layer.ReferenceLayerDoesntExist")]
     public void ImportReferenceLayer()
     {
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
@@ -359,7 +360,7 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
         }
         catch (Exception e)
         {
-            NoticeDialog.Show("Error while importing the image", "Error");
+            NoticeDialog.Show("ERROR_IMPORTING_IMAGE", "ERROR");
             return;
         }
 
@@ -378,7 +379,7 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
         var imagesFilter = new FileTypeDialogDataSet(FileTypeDialogDataSet.SetKind.Images).GetFormattedTypes();
         OpenFileDialog dialog = new OpenFileDialog
         {
-            Title = "Reference layer path",
+            Title = new LocalizedString("REFERENCE_LAYER_PATH"),
             CheckPathExists = true,
             Filter = imagesFilter
         };
@@ -386,7 +387,7 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
         return (bool)dialog.ShowDialog() ? dialog.FileName : null;
     }
 
-    [Command.Basic("PixiEditor.Layer.DeleteReferenceLayer", "Delete reference layer", "Delete reference layer", CanExecute = "PixiEditor.Layer.ReferenceLayerExists", IconPath = "Trash.png")]
+    [Command.Basic("PixiEditor.Layer.DeleteReferenceLayer", "DELETE_REFERENCE_LAYER", "DELETE_REFERENCE_LAYER", CanExecute = "PixiEditor.Layer.ReferenceLayerExists", IconPath = "Trash.png")]
     public void DeleteReferenceLayer()
     {
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
@@ -396,7 +397,7 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
         doc.Operations.DeleteReferenceLayer();
     }
 
-    [Command.Basic("PixiEditor.Layer.TransformReferenceLayer", "Transform reference layer", "Transform reference layer", CanExecute = "PixiEditor.Layer.ReferenceLayerExists", IconPath = "crop.png")]
+    [Command.Basic("PixiEditor.Layer.TransformReferenceLayer", "TRANSFORM_REFERENCE_LAYER", "TRANSFORM_REFERENCE_LAYER", CanExecute = "PixiEditor.Layer.ReferenceLayerExists", IconPath = "crop.png")]
     public void TransformReferenceLayer()
     {
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
@@ -406,7 +407,7 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
         doc.Operations.TransformReferenceLayer();
     }
 
-    [Command.Basic("PixiEditor.Layer.ToggleReferenceLayerTopMost", "Toggle reference layer position", "Toggle reference layer between highest/lowest", CanExecute = "PixiEditor.Layer.ReferenceLayerExists", IconEvaluator = "PixiEditor.Layer.ToggleReferenceLayerTopMostIcon")]
+    [Command.Basic("PixiEditor.Layer.ToggleReferenceLayerTopMost", "TOGGLE_REFERENCE_LAYER_POS", "TOGGLE_REFERENCE_LAYER_POS_DESCRIPTIVE", CanExecute = "PixiEditor.Layer.ReferenceLayerExists", IconEvaluator = "PixiEditor.Layer.ToggleReferenceLayerTopMostIcon")]
     public void ToggleReferenceLayerTopMost()
     {
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
@@ -416,7 +417,7 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
         doc.ReferenceLayerViewModel.IsTopMost = !doc.ReferenceLayerViewModel.IsTopMost;
     }
 
-    [Command.Basic("PixiEditor.Layer.ResetReferenceLayerPosition", "Reset reference layer position", "Reset reference layer position", CanExecute = "PixiEditor.Layer.ReferenceLayerExists", IconPath = "Layout.png")]
+    [Command.Basic("PixiEditor.Layer.ResetReferenceLayerPosition", "RESET_REFERENCE_LAYER_POS", "RESET_REFERENCE_LAYER_POS", CanExecute = "PixiEditor.Layer.ReferenceLayerExists", IconPath = "Layout.png")]
     public void ResetReferenceLayerPosition()
     {
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;

+ 6 - 6
src/PixiEditor/ViewModels/SubViewModels/Main/MiscViewModel.cs

@@ -6,7 +6,7 @@ using PixiEditor.Views.Dialogs;
 
 namespace PixiEditor.ViewModels.SubViewModels.Main;
 
-[Command.Group("PixiEditor.Links", "Misc")]
+[Command.Group("PixiEditor.Links", "MISC")]
 internal class MiscViewModel : SubViewModel<ViewModelMain>
 {
     public MiscViewModel(ViewModelMain owner)
@@ -15,11 +15,11 @@ internal class MiscViewModel : SubViewModel<ViewModelMain>
     }
 
     [Command.Internal("PixiEditor.Links.OpenHyperlink")]
-    [Command.Basic("PixiEditor.Links.OpenDocumentation", "https://pixieditor.net/docs/introduction", "Documentation", "Open Documentation", IconPath = "Globe.png")]
-    [Command.Basic("PixiEditor.Links.OpenWebsite", "https://pixieditor.net", "Website", "Open Website", IconPath = "Globe.png")]
-    [Command.Basic("PixiEditor.Links.OpenRepository", "https://github.com/PixiEditor/PixiEditor", "Repository", "Open Repository", IconPath = "Globe.png")]
-    [Command.Basic("PixiEditor.Links.OpenLicense", "https://github.com/PixiEditor/PixiEditor/blob/master/LICENSE", "License", "Open License", IconPath = "Globe.png")]
-    [Command.Basic("PixiEditor.Links.OpenOtherLicenses", "https://pixieditor.net/docs/Third-party-licenses", "Third Party Licenses", "Open Third Party Licenses", IconPath = "Globe.png")]
+    [Command.Basic("PixiEditor.Links.OpenDocumentation", "https://pixieditor.net/docs/introduction", "DOCUMENTATION", "Open Documentation", IconPath = "Globe.png")]
+    [Command.Basic("PixiEditor.Links.OpenWebsite", "https://pixieditor.net", "WEBSITE", "OPEN_WEBSITE", IconPath = "Globe.png")]
+    [Command.Basic("PixiEditor.Links.OpenRepository", "https://github.com/PixiEditor/PixiEditor", "REPOSITORY", "OPEN_REPOSITORY", IconPath = "Globe.png")]
+    [Command.Basic("PixiEditor.Links.OpenLicense", "https://github.com/PixiEditor/PixiEditor/blob/master/LICENSE", "LICENSE", "OPEN_LICENSE", IconPath = "Globe.png")]
+    [Command.Basic("PixiEditor.Links.OpenOtherLicenses", "https://pixieditor.net/docs/Third-party-licenses", "THIRD_PARTY_LICENSES", "OPEN_THIRD_PARTY_LICENSES", IconPath = "Globe.png")]
     public static async Task OpenHyperlink(string url)
     {
         try

+ 21 - 15
src/PixiEditor/ViewModels/SubViewModels/Main/RegistryViewModel.cs

@@ -21,27 +21,35 @@ internal class RegistryViewModel : SubViewModel<ViewModelMain>
         if (!LospecPaletteIsAssociated())
         {
             // Associate lospec-palette URL protocol
-            AssociateLospecPalette();
+            RegistryHelpers.TryAssociate(AssociateLospecPaletteInRegistry, "FAILED_ASSOCIATE_LOSPEC");
         }
+
+#if STEAM // Only associate .pixi file if it's a steam version, other versions handle it during installation
+        if(!PixiFileIsAssociated())
+        {
+            RegistryHelpers.TryAssociate(AssociatePixiFileInRegistry, "FAILED_ASSOCIATE_PIXI");
+        }
+#endif
     }
 
-    private void AssociateLospecPalette()
+    private bool PixiFileIsAssociated()
+    {
+        // Check if HKEY_CLASSES_ROOT\.pixi is present
+        return RegistryHelpers.IsKeyPresentInRoot(".pixi");
+    }
+
+    private void AssociatePixiFileInRegistry()
     {
         try
         {
-            if (!ProcessHelper.IsRunningAsAdministrator())
-            {
-                ProcessHelper.RunAsAdmin(Process.GetCurrentProcess().MainModule?.FileName);
-                Application.Current.Shutdown();
-            }
-            else
-            {
-                AssociateLospecPaletteInRegistry();
-            }
+            using RegistryKey key = Registry.ClassesRoot.CreateSubKey(".pixi");
+            key.SetValue("", "PixiEditor");
+
+            using RegistryKey shellKey = key.CreateSubKey("OpenWithProgids");
         }
         catch
         {
-            NoticeDialog.Show("Failed to associate Lospec Palette protocol", "Error");
+            NoticeDialog.Show("FAILED_ASSOCIATE_PIXI", "ERROR");
         }
     }
 
@@ -72,8 +80,6 @@ internal class RegistryViewModel : SubViewModel<ViewModelMain>
     private bool LospecPaletteIsAssociated()
     {
         // Check if HKEY_CLASSES_ROOT\lospec-palette is present
-
-        RegistryKey lospecPaletteKey = Registry.ClassesRoot.OpenSubKey("lospec-palette", RegistryRights.ReadKey);
-        return lospecPaletteKey != null;
+        return RegistryHelpers.IsKeyPresentInRoot("lospec-palette");
     }
 }

+ 2 - 2
src/PixiEditor/ViewModels/SubViewModels/Main/SearchViewModel.cs

@@ -3,7 +3,7 @@ using PixiEditor.Models.Commands.Attributes.Commands;
 
 namespace PixiEditor.ViewModels.SubViewModels.Main;
 
-[Command.Group("PixiEditor.Search", "Search")]
+[Command.Group("PixiEditor.Search", "SEARCH")]
 internal class SearchViewModel : SubViewModel<ViewModelMain>
 {
     private bool searchWindowOpen;
@@ -34,7 +34,7 @@ internal class SearchViewModel : SubViewModel<ViewModelMain>
     [Evaluator.CanExecute("PixiEditor.Search.CanOpenSearchWindow")]
     public bool CanToggleSeachWindow() => !ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument?.Busy ?? true;
 
-    [Command.Basic("PixiEditor.Search.Toggle", "", "Command Search", "Open the command search window", Key = Key.K, Modifiers = ModifierKeys.Control, CanExecute = "PixiEditor.Search.CanOpenSearchWindow")]
+    [Command.Basic("PixiEditor.Search.Toggle", "", "COMMAND_SEARCH", "OPEN_COMMAND_SEARCH", Key = Key.K, Modifiers = ModifierKeys.Control, CanExecute = "PixiEditor.Search.CanOpenSearchWindow")]
     public void ToggleSearchWindow(string searchTerm)
     {
         SelectAll = true;

+ 14 - 14
src/PixiEditor/ViewModels/SubViewModels/Main/SelectionViewModel.cs

@@ -5,7 +5,7 @@ using PixiEditor.Models.Commands.Attributes.Commands;
 
 namespace PixiEditor.ViewModels.SubViewModels.Main;
 
-[Command.Group("PixiEditor.Selection", "Selection")]
+[Command.Group("PixiEditor.Selection", "SELECTION")]
 internal class SelectionViewModel : SubViewModel<ViewModelMain>
 {
     public SelectionViewModel(ViewModelMain owner)
@@ -13,7 +13,7 @@ internal class SelectionViewModel : SubViewModel<ViewModelMain>
     {
     }
 
-    [Command.Basic("PixiEditor.Selection.SelectAll", "Select all", "Select everything", CanExecute = "PixiEditor.HasDocument", Key = Key.A, Modifiers = ModifierKeys.Control)]
+    [Command.Basic("PixiEditor.Selection.SelectAll", "SELECT_ALL", "SELECT_ALL_DESCRIPTIVE", CanExecute = "PixiEditor.HasDocument", Key = Key.A, Modifiers = ModifierKeys.Control)]
     public void SelectAll()
     {
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
@@ -22,7 +22,7 @@ internal class SelectionViewModel : SubViewModel<ViewModelMain>
         doc.Operations.SelectAll();
     }
 
-    [Command.Basic("PixiEditor.Selection.Clear", "Clear selection", "Clear selection", CanExecute = "PixiEditor.Selection.IsNotEmpty", Key = Key.D, Modifiers = ModifierKeys.Control)]
+    [Command.Basic("PixiEditor.Selection.Clear", "CLEAR_SELECTION", "CLEAR_SELECTION", CanExecute = "PixiEditor.Selection.IsNotEmpty", Key = Key.D, Modifiers = ModifierKeys.Control)]
     public void ClearSelection()
     {
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
@@ -31,7 +31,7 @@ internal class SelectionViewModel : SubViewModel<ViewModelMain>
         doc.Operations.ClearSelection();
     }
 
-    [Command.Basic("PixiEditor.Selection.InvertSelection", "Invert selection", "Invert the selected area", CanExecute = "PixiEditor.Selection.IsNotEmpty", Key = Key.I, Modifiers = ModifierKeys.Control)]
+    [Command.Basic("PixiEditor.Selection.InvertSelection", "INVERT_SELECTION", "INVERT_SELECTION_DESCRIPTIVE", CanExecute = "PixiEditor.Selection.IsNotEmpty", Key = Key.I, Modifiers = ModifierKeys.Control)]
     public void InvertSelection()
     {
         Owner.DocumentManagerSubViewModel.ActiveDocument?.Operations.InvertSelection();
@@ -49,27 +49,27 @@ internal class SelectionViewModel : SubViewModel<ViewModelMain>
         return SelectionIsNotEmpty() && (Owner.DocumentManagerSubViewModel.ActiveDocument?.SelectedStructureMember?.HasMaskBindable ?? false);
     }
 
-    [Command.Basic("PixiEditor.Selection.TransformArea", "Transform selected area", "Transform selected area", CanExecute = "PixiEditor.Selection.IsNotEmpty", Key = Key.T, Modifiers = ModifierKeys.Control)]
+    [Command.Basic("PixiEditor.Selection.TransformArea", "TRANSFORM_SELECTED_AREA", "TRANSFORM_SELECTED_AREA", CanExecute = "PixiEditor.Selection.IsNotEmpty", Key = Key.T, Modifiers = ModifierKeys.Control)]
     public void TransformSelectedArea()
     {
         Owner.DocumentManagerSubViewModel.ActiveDocument?.Operations.TransformSelectedArea(false);
     }
 
-    [Command.Basic("PixiEditor.Selection.NudgeSelectedObjectLeft", "Nudge selected object left", "Nudge selected object left", Key = Key.Left, Parameter = new int[] { -1, 0 }, IconPath = "E76B", IconEvaluator = "PixiEditor.FontIcon", CanExecute = "PixiEditor.Selection.CanNudgeSelectedObject")]
-    [Command.Basic("PixiEditor.Selection.NudgeSelectedObjectRight", "Nudge selected object right", "Nudge selected object right", Key = Key.Right, Parameter = new int[] { 1, 0 }, IconPath = "E76C", IconEvaluator = "PixiEditor.FontIcon", CanExecute = "PixiEditor.Selection.CanNudgeSelectedObject")]
-    [Command.Basic("PixiEditor.Selection.NudgeSelectedObjectUp", "Nudge selected object up", "Nudge selected object up", Key = Key.Up, Parameter = new int[] { 0, -1 }, IconPath = "E70E", IconEvaluator = "PixiEditor.FontIcon", CanExecute = "PixiEditor.Selection.CanNudgeSelectedObject")]
-    [Command.Basic("PixiEditor.Selection.NudgeSelectedObjectDown", "Nudge selected object down", "Nudge selected object down", Key = Key.Down, Parameter = new int[] { 0, 1 }, IconPath = "E70D", IconEvaluator = "PixiEditor.FontIcon", CanExecute = "PixiEditor.Selection.CanNudgeSelectedObject")]
+    [Command.Basic("PixiEditor.Selection.NudgeSelectedObjectLeft", "NUDGE_SELECTED_LEFT", "NUDGE_SELECTED_LEFT", Key = Key.Left, Parameter = new int[] { -1, 0 }, IconPath = "E76B", IconEvaluator = "PixiEditor.FontIcon", CanExecute = "PixiEditor.Selection.CanNudgeSelectedObject")]
+    [Command.Basic("PixiEditor.Selection.NudgeSelectedObjectRight", "NUDGE_SELECTED_RIGHT", "NUDGE_SELECTED_RIGHT", Key = Key.Right, Parameter = new int[] { 1, 0 }, IconPath = "E76C", IconEvaluator = "PixiEditor.FontIcon", CanExecute = "PixiEditor.Selection.CanNudgeSelectedObject")]
+    [Command.Basic("PixiEditor.Selection.NudgeSelectedObjectUp", "NUDGE_SELECTED_UP", "NUDGE_SELECTED_UP", Key = Key.Up, Parameter = new int[] { 0, -1 }, IconPath = "E70E", IconEvaluator = "PixiEditor.FontIcon", CanExecute = "PixiEditor.Selection.CanNudgeSelectedObject")]
+    [Command.Basic("PixiEditor.Selection.NudgeSelectedObjectDown", "NUDGE_SELECTED_DOWN", "NUDGE_SELECTED_DOWN", Key = Key.Down, Parameter = new int[] { 0, 1 }, IconPath = "E70D", IconEvaluator = "PixiEditor.FontIcon", CanExecute = "PixiEditor.Selection.CanNudgeSelectedObject")]
     public void NudgeSelectedObject(int[] dist)
     {
         VecI distance = new(dist[0], dist[1]);
         Owner.DocumentManagerSubViewModel.ActiveDocument?.Operations.NudgeSelectedObject(distance);
     }
 
-    [Command.Basic("PixiEditor.Selection.NewToMask", SelectionMode.New, "New mask from selection", "Selection to new mask", CanExecute = "PixiEditor.Selection.IsNotEmpty")]
-    [Command.Basic("PixiEditor.Selection.AddToMask", SelectionMode.Add, "Add selection to mask", "Add selection to mask", CanExecute = "PixiEditor.Selection.IsNotEmpty")]
-    [Command.Basic("PixiEditor.Selection.SubtractFromMask", SelectionMode.Subtract, "Subtract selection from mask", "Subtract selection from mask", CanExecute = "PixiEditor.Selection.IsNotEmptyAndHasMask")]
-    [Command.Basic("PixiEditor.Selection.IntersectSelectionMask", SelectionMode.Intersect, "Intersect selection with mask", "Intersect selection with mask", CanExecute = "PixiEditor.Selection.IsNotEmptyAndHasMask")]
-    [Command.Filter("PixiEditor.Selection.ToMaskMenu", "Selection to mask", "Selection to mask", Key = Key.M, Modifiers = ModifierKeys.Control)]
+    [Command.Basic("PixiEditor.Selection.NewToMask", SelectionMode.New, "MASK_FROM_SELECTION", "MASK_FROM_SELECTION_DESCRIPTIVE", CanExecute = "PixiEditor.Selection.IsNotEmpty")]
+    [Command.Basic("PixiEditor.Selection.AddToMask", SelectionMode.Add, "ADD_SELECTION_TO_MASK", "ADD_SELECTION_TO_MASK", CanExecute = "PixiEditor.Selection.IsNotEmpty")]
+    [Command.Basic("PixiEditor.Selection.SubtractFromMask", SelectionMode.Subtract, "SUBTRACT_SELECTION_FROM_MASK", "SUBTRACT_SELECTION_FROM_MASK", CanExecute = "PixiEditor.Selection.IsNotEmptyAndHasMask")]
+    [Command.Basic("PixiEditor.Selection.IntersectSelectionMask", SelectionMode.Intersect, "INTERSECT_SELECTION_MASK", "INTERSECT_SELECTION_MASK", CanExecute = "PixiEditor.Selection.IsNotEmptyAndHasMask")]
+    [Command.Filter("PixiEditor.Selection.ToMaskMenu", "SELECTION_TO_MASK", "SELECTION_TO_MASK", Key = Key.M, Modifiers = ModifierKeys.Control)]
     public void SelectionToMask(SelectionMode mode)
     {
         Owner.DocumentManagerSubViewModel.ActiveDocument?.Operations.SelectionToMask(mode);

+ 2 - 2
src/PixiEditor/ViewModels/SubViewModels/Main/StylusViewModel.cs

@@ -7,7 +7,7 @@ using PixiEditor.ViewModels.SubViewModels.Tools.Tools;
 
 namespace PixiEditor.ViewModels.SubViewModels.Main;
 
-[Command.Group("PixiEditor.Stylus", "Stylus")]
+[Command.Group("PixiEditor.Stylus", "STYLUS")]
 internal class StylusViewModel : SubViewModel<ViewModelMain>
 {
     private bool isPenModeEnabled;
@@ -48,7 +48,7 @@ internal class StylusViewModel : SubViewModel<ViewModelMain>
         UpdateUseTouchGesture();
     }
 
-    [Command.Basic("PixiEditor.Stylus.TogglePenMode", "Toggle Pen Mode", "Toggle Pen Mode", IconPath = "penMode.png")]
+    [Command.Basic("PixiEditor.Stylus.TogglePenMode", "TOGGLE_PEN_MODE", "TOGGLE_PEN_MODE", IconPath = "penMode.png")]
     public void TogglePenMode()
     {
         IsPenModeEnabled = !IsPenModeEnabled;

+ 4 - 4
src/PixiEditor/ViewModels/SubViewModels/Main/ToolsViewModel.cs

@@ -13,7 +13,7 @@ using PixiEditor.ViewModels.SubViewModels.Tools.ToolSettings.Toolbars;
 
 namespace PixiEditor.ViewModels.SubViewModels.Main;
 #nullable enable
-[Command.Group("PixiEditor.Tools", "Tools")]
+[Command.Group("PixiEditor.Tools", "TOOLS")]
 internal class ToolsViewModel : SubViewModel<ViewModelMain>
 {
     public ZoomToolViewModel? ZoomTool => GetTool<ZoomToolViewModel>();
@@ -83,7 +83,7 @@ internal class ToolsViewModel : SubViewModel<ViewModelMain>
         SetActiveTool(typeof(T), transient);
     }
 
-    [Command.Basic("PixiEditor.Tools.ApplyTransform", "Apply transform", "", Key = Key.Enter)]
+    [Command.Basic("PixiEditor.Tools.ApplyTransform", "APPLY_TRANSFORM", "", Key = Key.Enter)]
     public void ApplyTransform()
     {
         DocumentViewModel? doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
@@ -157,8 +157,8 @@ internal class ToolsViewModel : SubViewModel<ViewModelMain>
         SetActiveTool(tool.GetType(), false);
     }
 
-    [Command.Basic("PixiEditor.Tools.IncreaseSize", 1, "Increase Tool Size", "Increase Tool Size", Key = Key.OemCloseBrackets)]
-    [Command.Basic("PixiEditor.Tools.DecreaseSize", -1, "Decrease Tool Size", "Decrease Tool Size", Key = Key.OemOpenBrackets)]
+    [Command.Basic("PixiEditor.Tools.IncreaseSize", 1, "INCREASE_TOOL_SIZE", "INCREASE_TOOL_SIZE", Key = Key.OemCloseBrackets)]
+    [Command.Basic("PixiEditor.Tools.DecreaseSize", -1, "DECREASE_TOOL_SIZE", "DECREASE_TOOL_SIZE", Key = Key.OemOpenBrackets)]
     public void ChangeToolSize(int increment)
     {
         if (ActiveTool?.Toolbar is not BasicToolbar toolbar)

+ 3 - 3
src/PixiEditor/ViewModels/SubViewModels/Main/UndoViewModel.cs

@@ -5,7 +5,7 @@ using PixiEditor.Models.Commands.Attributes.Evaluators;
 
 namespace PixiEditor.ViewModels.SubViewModels.Main;
 
-[Command.Group("PixiEditor.Undo", "Undo")]
+[Command.Group("PixiEditor.Undo", "UNDO")]
 internal class UndoViewModel : SubViewModel<ViewModelMain>
 {
     public UndoViewModel(ViewModelMain owner)
@@ -16,7 +16,7 @@ internal class UndoViewModel : SubViewModel<ViewModelMain>
     /// <summary>
     ///     Redo last action.
     /// </summary>
-    [Command.Basic("PixiEditor.Undo.Redo", "Redo", "Redo next step", CanExecute = "PixiEditor.Undo.CanRedo", Key = Key.Y, Modifiers = ModifierKeys.Control,
+    [Command.Basic("PixiEditor.Undo.Redo", "REDO", "REDO_DESCRIPTIVE", CanExecute = "PixiEditor.Undo.CanRedo", Key = Key.Y, Modifiers = ModifierKeys.Control,
         IconPath = "E7A6", IconEvaluator = "PixiEditor.FontIcon")]
     public void Redo()
     {
@@ -29,7 +29,7 @@ internal class UndoViewModel : SubViewModel<ViewModelMain>
     /// <summary>
     ///     Undo last action.
     /// </summary>
-    [Command.Basic("PixiEditor.Undo.Undo", "Undo", "Undo previous step", CanExecute = "PixiEditor.Undo.CanUndo", Key = Key.Z, Modifiers = ModifierKeys.Control,
+    [Command.Basic("PixiEditor.Undo.Undo", "UNDO", "UNDO_DESCRIPTIVE", CanExecute = "PixiEditor.Undo.CanUndo", Key = Key.Z, Modifiers = ModifierKeys.Control,
         IconPath = "E7A7", IconEvaluator = "PixiEditor.FontIcon")]
     public void Undo()
     {

+ 9 - 8
src/PixiEditor/ViewModels/SubViewModels/Main/UpdateViewModel.cs

@@ -6,6 +6,7 @@ using System.Linq;
 using System.Threading.Tasks;
 using System.Windows;
 using PixiEditor.Helpers;
+using PixiEditor.Localization;
 using PixiEditor.Models.Commands.Attributes;
 using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.Models.Dialogs;
@@ -43,7 +44,7 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
             RaisePropertyChanged(nameof(UpdateReadyToInstall));
             if (value)
             {
-                VersionText = $"to install update ({UpdateChecker.LatestReleaseInfo.TagName})"; // Button shows "Restart" before this text
+                VersionText = new LocalizedString("TO_INSTALL_UPDATE", UpdateChecker.LatestReleaseInfo.TagName); // Button shows "Restart" before this text
             }
         }
     }
@@ -75,7 +76,7 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
         if (updateAvailable && updateFileDoesNotExists && updateExeDoesNotExists)
         {
             UpdateReadyToInstall = false;
-            VersionText = "Downloading update...";
+            VersionText = new LocalizedString("DOWNLOADING_UPDATE");
             if (updateCompatible)
             {
                 await UpdateDownloader.DownloadReleaseZip(UpdateChecker.LatestReleaseInfo);
@@ -119,7 +120,7 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
                 if (updateFileExists || updateExeExists)
                 {
                     ViewModelMain.Current.UpdateSubViewModel.UpdateReadyToInstall = true;
-                    var result = ConfirmationDialog.Show("Update is ready to be installed. Do you want to install it now?", "New update");
+                    var result = ConfirmationDialog.Show("UPDATE_READY", "NEW_UPDATE");
                     if (result == Models.Enums.ConfirmationType.Yes)
                     {
                         if (updateFileExists && File.Exists(updaterPath))
@@ -146,8 +147,8 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
         catch (Win32Exception)
         {
             NoticeDialog.Show(
-                "Couldn't update without administrator rights.",
-                "Insufficient permissions");
+                "COULD_NOT_UPDATE_WITHOUT_ADMIN",
+                "INSUFFICIENT_PERMISSIONS");
         }
     }
 
@@ -182,7 +183,7 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
         }
         catch (Win32Exception)
         {
-            NoticeDialog.Show("Couldn't update without administrator rights.", "Insufficient permissions");
+            NoticeDialog.Show("COULD_NOT_UPDATE_WITHOUT_ADMIN", "INSUFFICIENT_PERMISSIONS");
         }
     }
 
@@ -202,7 +203,7 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
             }
             catch (System.Net.Http.HttpRequestException)
             {
-                NoticeDialog.Show("Could not check if there is an update available", "Update check failed");
+                NoticeDialog.Show("COULD_NOT_CHECK_FOR_UPDATES", "UPDATE_CHECK_FAILED");
             }
 
             AskToInstall();
@@ -229,7 +230,7 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
 
         string version = VersionHelpers.GetCurrentAssemblyVersionString();
         UpdateChecker = new UpdateChecker(version, GetUpdateChannel(updateChannel));
-        VersionText = $"Version {version}";
+        VersionText = new LocalizedString("VERSION", version);
     }
 
     private UpdateChannel GetUpdateChannel(string channelName)

+ 3 - 3
src/PixiEditor/ViewModels/SubViewModels/Main/ViewOptionsViewModel.cs

@@ -18,14 +18,14 @@ internal class ViewOptionsViewModel : SubViewModel<ViewModelMain>
     {
     }
 
-    [Command.Basic("PixiEditor.View.ToggleGrid", "Toggle gridlines", "Toggle gridlines", Key = Key.OemTilde, Modifiers = ModifierKeys.Control)]
+    [Command.Basic("PixiEditor.View.ToggleGrid", "TOGGLE_GRIDLINES", "TOGGLE_GRIDLINES", Key = Key.OemTilde, Modifiers = ModifierKeys.Control)]
     public void ToggleGridLines()
     {
         GridLinesEnabled = !GridLinesEnabled;
     }
 
-    [Command.Basic("PixiEditor.View.ZoomIn", 1, "Zoom in", "Zoom in", CanExecute = "PixiEditor.HasDocument", Key = Key.OemPlus)]
-    [Command.Basic("PixiEditor.View.Zoomout", -1, "Zoom out", "Zoom out", CanExecute = "PixiEditor.HasDocument", Key = Key.OemMinus)]
+    [Command.Basic("PixiEditor.View.ZoomIn", 1, "ZOOM_IN", "ZOOM_IN", CanExecute = "PixiEditor.HasDocument", Key = Key.OemPlus)]
+    [Command.Basic("PixiEditor.View.Zoomout", -1, "ZOOM_OUT", "ZOOM_OUT", CanExecute = "PixiEditor.HasDocument", Key = Key.OemMinus)]
     public void ZoomViewport(double zoom)
     {
         ViewportWindowViewModel? viewport = Owner.WindowSubViewModel.ActiveWindow as ViewportWindowViewModel;

+ 13 - 13
src/PixiEditor/ViewModels/SubViewModels/Main/WindowViewModel.cs

@@ -12,7 +12,7 @@ using Command = PixiEditor.Models.Commands.Attributes.Commands.Command;
 namespace PixiEditor.ViewModels.SubViewModels.Main;
 
 #nullable enable
-[Command.Group("PixiEditor.Window", "Windows")]
+[Command.Group("PixiEditor.Window", "WINDOWS")]
 internal class WindowViewModel : SubViewModel<ViewModelMain>
 {
     private CommandController commandController;
@@ -44,7 +44,7 @@ internal class WindowViewModel : SubViewModel<ViewModelMain>
         this.commandController = commandController;
     }
 
-    [Command.Basic("PixiEditor.Window.CreateNewViewport", "New window for current image", "New window for current image", CanExecute = "PixiEditor.HasDocument")]
+    [Command.Basic("PixiEditor.Window.CreateNewViewport", "NEW_WINDOW_FOR_IMG", "NEW_WINDOW_FOR_IMG", CanExecute = "PixiEditor.HasDocument")]
     public void CreateNewViewport()
     {
         var doc = ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument;
@@ -53,14 +53,14 @@ internal class WindowViewModel : SubViewModel<ViewModelMain>
         CreateNewViewport(doc);
     }
     
-    [Command.Basic("PixiEditor.Window.CenterActiveViewport", "Center active viewport", "Center active viewport", CanExecute = "PixiEditor.HasDocument")]
+    [Command.Basic("PixiEditor.Window.CenterActiveViewport", "CENTER_ACTIVE_VIEWPORT", "CENTER_ACTIVE_VIEWPORT", CanExecute = "PixiEditor.HasDocument")]
     public void CenterCurrentViewport()
     {
         if (ActiveWindow is ViewportWindowViewModel viewport)
             viewport.CenterViewportTrigger.Execute(this, viewport.Document.SizeBindable);
     }
     
-    [Command.Basic("PixiEditor.Window.FlipHorizontally", "Flip Viewport Horizontally", "Flip Viewport Horizontally", CanExecute = "PixiEditor.HasDocument", IconPath = "FlipHorizontal.png")]
+    [Command.Basic("PixiEditor.Window.FlipHorizontally", "FLIP_VIEWPORT_HORIZONTALLY", "FLIP_VIEWPORT_HORIZONTALLY", CanExecute = "PixiEditor.HasDocument", IconPath = "FlipHorizontal.png")]
     public void FlipViewportHorizontally()
     {
         if (ActiveWindow is ViewportWindowViewModel viewport)
@@ -69,7 +69,7 @@ internal class WindowViewModel : SubViewModel<ViewModelMain>
         }
     }
     
-    [Command.Basic("PixiEditor.Window.FlipVertically", "Flip Viewport Vertically", "Flip Viewport Vertically", CanExecute = "PixiEditor.HasDocument", IconPath = "FlipVertical.png")]
+    [Command.Basic("PixiEditor.Window.FlipVertically", "FLIP_VIEWPORT_VERTICALLY", "FLIP_VIEWPORT_VERTICALLY", CanExecute = "PixiEditor.HasDocument", IconPath = "FlipVertical.png")]
     public void FlipViewportVertically()
     {
         if (ActiveWindow is ViewportWindowViewModel viewport)
@@ -132,25 +132,25 @@ internal class WindowViewModel : SubViewModel<ViewModelMain>
         }
     }
 
-    [Command.Basic("PixiEditor.Window.OpenSettingsWindow", "Open Settings", "Open Settings Window", Key = Key.OemComma, Modifiers = ModifierKeys.Control)]
-    public static void OpenSettingsWindow(string page)
+    [Command.Basic("PixiEditor.Window.OpenSettingsWindow", "OPEN_SETTINGS", "OPEN_SETTINGS_DESCRIPTIVE", Key = Key.OemComma, Modifiers = ModifierKeys.Control)]
+    public static void OpenSettingsWindow(int page)
     {
-        if (string.IsNullOrWhiteSpace(page))
+        if (page < 0)
         {
-            page = "General";
+            page = 0;
         }
 
         var settings = new SettingsWindow(page);
         settings.Show();
     }
 
-    [Command.Basic("PixiEditor.Window.OpenStartupWindow", "Open Startup Window", "Open Startup Window")]
+    [Command.Basic("PixiEditor.Window.OpenStartupWindow", "OPEN_STARTUP_WINDOW", "OPEN_STARTUP_WINDOW")]
     public void OpenHelloThereWindow()
     {
         new HelloTherePopup(Owner.FileSubViewModel).Show();
     }
 
-    [Command.Basic("PixiEditor.Window.OpenShortcutWindow", "Open Shortcut Window", "Open Shortcut Window", Key = Key.F1)]
+    [Command.Basic("PixiEditor.Window.OpenShortcutWindow", "OPEN_SHORTCUT_WINDOW", "OPEN_SHORTCUT_WINDOW", Key = Key.F1)]
     public void ShowShortcutWindow()
     {
         ShortcutPopup.Show();
@@ -158,13 +158,13 @@ internal class WindowViewModel : SubViewModel<ViewModelMain>
     }
     
         
-    [Command.Basic("PixiEditor.Window.OpenAboutWindow", "Open About Window", "Open About Window")]
+    [Command.Basic("PixiEditor.Window.OpenAboutWindow", "OPEN_ABOUT_WINDOW", "OPEN_ABOUT_WINDOW")]
     public void OpenAboutWindow()
     {
         new AboutPopup().Show();
     }
 
-    [Command.Basic("PixiEditor.Window.OpenNavigationWindow", "navigation", "Open Navigation Window", "Open Navigation Window")]
+    [Command.Basic("PixiEditor.Window.OpenNavigationWindow", "navigation", "OPEN_NAVIGATION_WINDOW", "OPEN_NAVIGATION_WINDOW")]
     public static void ShowAvalonDockWindow(string id)
     {
         if (MainWindow.Current?.LayoutRoot?.Manager?.Layout == null) return;

+ 1 - 0
src/PixiEditor/ViewModels/SubViewModels/Tools/ToolSettings/Settings/BoolSetting.cs

@@ -2,6 +2,7 @@
 using System.Windows.Controls;
 using System.Windows.Controls.Primitives;
 using System.Windows.Data;
+using PixiEditor.Localization;
 
 namespace PixiEditor.ViewModels.SubViewModels.Tools.ToolSettings.Settings;
 

+ 1 - 0
src/PixiEditor/ViewModels/SubViewModels/Tools/ToolSettings/Settings/ColorSetting.cs

@@ -4,6 +4,7 @@ using System.Windows.Data;
 using System.Windows.Media;
 using Microsoft.Xaml.Behaviors;
 using PixiEditor.Helpers.Behaviours;
+using PixiEditor.Localization;
 using PixiEditor.Views.UserControls;
 
 namespace PixiEditor.ViewModels.SubViewModels.Tools.ToolSettings.Settings;

+ 4 - 1
src/PixiEditor/ViewModels/SubViewModels/Tools/ToolSettings/Settings/EnumSetting.cs

@@ -3,6 +3,8 @@ using System.Windows.Controls;
 using System.Windows.Controls.Primitives;
 using System.Windows.Data;
 using PixiEditor.Helpers.Extensions;
+using PixiEditor.Localization;
+using PixiEditor.Views;
 
 namespace PixiEditor.ViewModels.SubViewModels.Tools.ToolSettings.Settings;
 
@@ -93,10 +95,11 @@ internal sealed class EnumSetting<TEnum> : Setting<TEnum, ComboBox>
         {
             var item = new ComboBoxItem
             {
-                Content = value.GetDescription(),
                 Tag = value
             };
 
+            Translator.SetKey(item, value.GetDescription());
+
             comboBox.Items.Add(item);
         }
     }

+ 1 - 0
src/PixiEditor/ViewModels/SubViewModels/Tools/ToolSettings/Settings/FloatSetting.cs

@@ -1,5 +1,6 @@
 using System.Windows.Controls;
 using System.Windows.Data;
+using PixiEditor.Localization;
 using PixiEditor.Views.UserControls;
 
 namespace PixiEditor.ViewModels.SubViewModels.Tools.ToolSettings.Settings;

+ 2 - 1
src/PixiEditor/ViewModels/SubViewModels/Tools/ToolSettings/Settings/Setting.cs

@@ -1,4 +1,5 @@
 using System.Windows.Controls;
+using PixiEditor.Localization;
 
 #pragma warning disable SA1402 // File may only contain a single type, Justification: "Same class with generic value"
 
@@ -59,7 +60,7 @@ internal abstract class Setting : NotifyableObject
 
     public string Name { get; }
 
-    public string Label { get; set; }
+    public LocalizedString Label { get; set; }
 
     public bool HasLabel => !string.IsNullOrEmpty(Label);
 

+ 0 - 1
src/PixiEditor/ViewModels/SubViewModels/Tools/ToolSettings/Settings/SizeSetting.cs

@@ -18,7 +18,6 @@ internal sealed class SizeSetting : Setting<int>
     {
         SizeInput tb = new SizeInput
         {
-            Width = 65,
             Height = 20,
             VerticalAlignment = VerticalAlignment.Center,
             MaxSize = 9999,

+ 2 - 2
src/PixiEditor/ViewModels/SubViewModels/Tools/ToolSettings/Toolbars/BasicShapeToolbar.cs

@@ -10,7 +10,7 @@ internal class BasicShapeToolbar : BasicToolbar
 
     public BasicShapeToolbar()
     {
-        Settings.Add(new BoolSetting(nameof(Fill), "Fill shape: "));
-        Settings.Add(new ColorSetting(nameof(FillColor), "Fill color"));
+        Settings.Add(new BoolSetting(nameof(Fill), "FILL_SHAPE_LABEL"));
+        Settings.Add(new ColorSetting(nameof(FillColor), "FILL_COLOR_LABEL"));
     }
 }

+ 1 - 1
src/PixiEditor/ViewModels/SubViewModels/Tools/ToolSettings/Toolbars/BasicToolbar.cs

@@ -14,7 +14,7 @@ internal class BasicToolbar : Toolbar
     }
     public BasicToolbar()
     {
-        var setting = new SizeSetting(nameof(ToolSize), "Tool size:");
+        var setting = new SizeSetting(nameof(ToolSize), "TOOL_SIZE_LABEL");
         setting.ValueChanged += (_, _) => RaisePropertyChanged(nameof(ToolSize));
         Settings.Add(setting);
     }

+ 15 - 15
src/PixiEditor/ViewModels/SubViewModels/Tools/ToolSettings/Toolbars/SettingAttributes.cs

@@ -10,9 +10,9 @@ public static class Settings
     /// </summary>
     public class BoolAttribute : SettingsAttribute
     {
-        public BoolAttribute(string label) : base(label) { }
+        public BoolAttribute(string labelKey) : base(labelKey) { }
 
-        public BoolAttribute(string label, object defaultValue) : base(label, defaultValue) { }
+        public BoolAttribute(string labelKey, object defaultValue) : base(labelKey, defaultValue) { }
     }
 
     /// <summary>
@@ -20,9 +20,9 @@ public static class Settings
     /// </summary>
     public class EnumAttribute : SettingsAttribute
     {
-        public EnumAttribute(string label) : base(label) { }
+        public EnumAttribute(string labelKey) : base(labelKey) { }
 
-        public EnumAttribute(string label, object defaultValue) : base(label, defaultValue) { }
+        public EnumAttribute(string labelKey, object defaultValue) : base(labelKey, defaultValue) { }
     }
 
     /// <summary>
@@ -30,11 +30,11 @@ public static class Settings
     /// </summary>
     public class ColorAttribute : SettingsAttribute
     {
-        public ColorAttribute(string label) : base(label) { }
+        public ColorAttribute(string labelKey) : base(labelKey) { }
 
-        public ColorAttribute(string label, byte r, byte g, byte b) : base(label, new Color(r, g, b)) { }
+        public ColorAttribute(string labelKey, byte r, byte g, byte b) : base(labelKey, new Color(r, g, b)) { }
         
-        public ColorAttribute(string label, byte r, byte g, byte b, byte a) : base(label, new Color(r, g, b, a)) { }
+        public ColorAttribute(string labelKey, byte r, byte g, byte b, byte a) : base(labelKey, new Color(r, g, b, a)) { }
     }
 
     /// <summary>
@@ -42,9 +42,9 @@ public static class Settings
     /// </summary>
     public class FloatAttribute : SettingsAttribute
     {
-        public FloatAttribute(string label) : base(label) { }
+        public FloatAttribute(string labelKey) : base(labelKey) { }
 
-        public FloatAttribute(string label, float defaultValue) : base(label, defaultValue) { }
+        public FloatAttribute(string labelKey, float defaultValue) : base(labelKey, defaultValue) { }
     }
 
     /// <summary>
@@ -52,7 +52,7 @@ public static class Settings
     /// </summary>
     public class SizeAttribute : SettingsAttribute
     {
-        public SizeAttribute(string label) : base(label) { }
+        public SizeAttribute(string labelKey) : base(labelKey) { }
     }
 
     /// <summary>
@@ -67,18 +67,18 @@ public static class Settings
 
         public SettingsAttribute() { }
         
-        public SettingsAttribute(string label)
+        public SettingsAttribute(string labelKey)
         {
-            Label = label;
+            LabelKey = labelKey;
         }
 
-        public SettingsAttribute(string label, object defaultValue)
+        public SettingsAttribute(string labelKey, object defaultValue)
         {
-            Label = label;
+            LabelKey = labelKey;
             DefaultValue = defaultValue;
         }
         
-        public readonly string Label;
+        public readonly string LabelKey;
 
         public readonly object DefaultValue;
     }

+ 3 - 3
src/PixiEditor/ViewModels/SubViewModels/Tools/ToolSettings/Toolbars/ToolbarFactory.cs

@@ -39,14 +39,14 @@ internal static class ToolbarFactory
                 continue;
             }
             
-            var label = attribute.Label ?? name;
+            var label = attribute.LabelKey ?? name;
 
             var setting = attribute switch
             {
                 Settings.BoolAttribute => new BoolSetting(name, (bool)(attribute.DefaultValue ?? false), label),
                 Settings.ColorAttribute => new ColorSetting(name, ((Color)(attribute.DefaultValue ?? Colors.White)).ToColor(), label),
                 Settings.EnumAttribute => GetEnumSetting(property.PropertyType, name, attribute),
-                Settings.FloatAttribute => new FloatSetting(name, (float)(attribute.DefaultValue ?? 0f), attribute.Label),
+                Settings.FloatAttribute => new FloatSetting(name, (float)(attribute.DefaultValue ?? 0f), attribute.LabelKey),
                 Settings.SizeAttribute => new SizeSetting(name, label),
                 _ => throw new NotImplementedException($"SettingsAttribute of type '{attribute.GetType().FullName}' has not been implemented")
             };
@@ -67,6 +67,6 @@ internal static class ToolbarFactory
         return (Setting)typeof(EnumSetting<>)
             .MakeGenericType(enumType)
             .GetConstructor(new[] { typeof(string), typeof(string), enumType })!
-            .Invoke(new[] { name, attribute.Label ?? name, attribute.DefaultValue });
+            .Invoke(new[] { name, attribute.LabelKey ?? name, attribute.DefaultValue });
     }
 }

+ 16 - 4
src/PixiEditor/ViewModels/SubViewModels/Tools/ToolViewModel.cs

@@ -3,6 +3,7 @@ using System.Runtime.CompilerServices;
 using System.Windows.Input;
 using ChunkyImageLib.DataHolders;
 using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.Localization;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.ViewModels.SubViewModels.Tools.ToolSettings.Settings;
 using PixiEditor.ViewModels.SubViewModels.Tools.ToolSettings.Toolbars;
@@ -17,7 +18,8 @@ internal abstract class ToolViewModel : NotifyableObject
 
     public virtual string ToolName => GetType().Name.Replace("Tool", string.Empty).Replace("ViewModel", string.Empty);
 
-    public virtual string DisplayName => ToolName.AddSpacesBeforeUppercaseLetters();
+    public abstract string ToolNameLocalizationKey { get; }
+    public virtual LocalizedString DisplayName => new LocalizedString(ToolNameLocalizationKey);
 
     public virtual string ImagePath => $"/Images/Tools/{ToolName}Image.png";
 
@@ -25,10 +27,10 @@ internal abstract class ToolViewModel : NotifyableObject
 
     public virtual bool HideHighlight { get; }
 
-    public abstract string Tooltip { get; }
+    public abstract LocalizedString Tooltip { get; }
 
-    private string actionDisplay = string.Empty;
-    public string ActionDisplay
+    private LocalizedString actionDisplay = string.Empty;
+    public LocalizedString ActionDisplay
     {
         get => actionDisplay;
         set
@@ -53,6 +55,16 @@ internal abstract class ToolViewModel : NotifyableObject
 
     public Toolbar Toolbar { get; set; } = new EmptyToolbar();
 
+    internal ToolViewModel()
+    {
+        ILocalizationProvider.Current.OnLanguageChanged += OnLanguageChanged;
+    }
+
+    private void OnLanguageChanged(Language obj)
+    {
+        ActionDisplay = new LocalizedString(ActionDisplay.Key);
+    }
+
     public virtual void ModifierKeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown) { }
     public virtual void OnLeftMouseButtonDown(VecD pos) { }
     public virtual void OnSelected() 

+ 7 - 5
src/PixiEditor/ViewModels/SubViewModels/Tools/Tools/BrightnessToolViewModel.cs

@@ -1,6 +1,7 @@
 using System.Windows.Input;
 using ChunkyImageLib.DataHolders;
 using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.Localization;
 using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.Models.Enums;
 using PixiEditor.ViewModels.SubViewModels.Tools.ToolSettings.Settings;
@@ -12,7 +13,8 @@ namespace PixiEditor.ViewModels.SubViewModels.Tools.Tools;
 [Command.Tool(Key = Key.U)]
 internal class BrightnessToolViewModel : ToolViewModel
 {
-    private readonly string defaultActionDisplay = "Draw on pixels to make them brighter. Hold Ctrl to darken.";
+    private readonly string defaultActionDisplay = "BRIGHTNESS_TOOL_ACTION_DISPLAY_DEFAULT";
+    public override string ToolNameLocalizationKey => "BRIGHTNESS_TOOL";
 
     public BrightnessToolViewModel()
     {
@@ -20,17 +22,17 @@ internal class BrightnessToolViewModel : ToolViewModel
         Toolbar = ToolbarFactory.Create<BrightnessToolViewModel, BasicToolbar>();
     }
 
-    public override string Tooltip => $"Makes pixels brighter or darker ({Shortcut}). Hold Ctrl to make pixels darker.";
+    public override LocalizedString Tooltip => new LocalizedString("BRIGHTNESS_TOOL_TOOLTIP", Shortcut);
 
     public override BrushShape BrushShape => BrushShape.Circle;
 
     [Settings.Inherited]
     public int ToolSize => GetValue<int>();
     
-    [Settings.Float("Strength", 5)]
+    [Settings.Float("STRENGTH_LABEL", 5)]
     public float CorrectionFactor => GetValue<float>();
 
-    [Settings.Enum("Mode")]
+    [Settings.Enum("MODE_LABEL")]
     public BrightnessMode BrightnessMode => GetValue<BrightnessMode>();
     
     public bool Darken { get; private set; } = false;
@@ -44,7 +46,7 @@ internal class BrightnessToolViewModel : ToolViewModel
         }
         else
         {
-            ActionDisplay = "Draw on pixels to make them darker. Release Ctrl to brighten.";
+            ActionDisplay = "BRIGHTNESS_TOOL_ACTION_DISPLAY_CTRL";
             Darken = true;
         }
     }

+ 7 - 5
src/PixiEditor/ViewModels/SubViewModels/Tools/Tools/ColorPickerToolViewModel.cs

@@ -1,6 +1,7 @@
 using System.Windows.Input;
 using ChunkyImageLib.DataHolders;
 using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.Localization;
 using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.Models.Enums;
 using PixiEditor.ViewModels.SubViewModels.Tools.ToolSettings.Toolbars;
@@ -11,13 +12,14 @@ namespace PixiEditor.ViewModels.SubViewModels.Tools.Tools;
 [Command.Tool(Key = Key.O, Transient = Key.LeftAlt)]
 internal class ColorPickerToolViewModel : ToolViewModel
 {
-    private readonly string defaultActionDisplay = "Click to pick colors. Hold Ctrl to hide the canvas. Hold Shift to hide the reference layer";
+    private readonly string defaultActionDisplay = "COLOR_PICKER_ACTION_DISPLAY_DEFAULT";
 
     public override bool HideHighlight => true;
 
+    public override string ToolNameLocalizationKey => "COLOR_PICKER_TOOL";
     public override BrushShape BrushShape => BrushShape.Pixel;
 
-    public override string Tooltip => $"Picks the primary color from the canvas. ({Shortcut})";
+    public override LocalizedString Tooltip => new("COLOR_PICKER_TOOLTIP", Shortcut);
 
     private bool pickFromCanvas = true;
     public bool PickFromCanvas
@@ -33,7 +35,7 @@ internal class ColorPickerToolViewModel : ToolViewModel
         private set => SetProperty(ref pickFromReferenceLayer, value);
     }
 
-    [Settings.Enum("Scope", DocumentScope.AllLayers)]
+    [Settings.Enum("SCOPE_LABEL", DocumentScope.AllLayers)]
     public DocumentScope Mode => GetValue<DocumentScope>();
 
     public ColorPickerToolViewModel()
@@ -53,13 +55,13 @@ internal class ColorPickerToolViewModel : ToolViewModel
         {
             PickFromCanvas = false;
             PickFromReferenceLayer = true;
-            ActionDisplay = "Click to pick colors from the reference layer.";
+            ActionDisplay = "COLOR_PICKER_ACTION_DISPLAY_CTRL";
         }
         else if (shiftIsDown)
         {
             PickFromCanvas = true;
             PickFromReferenceLayer = false;
-            ActionDisplay = "Click to pick colors from the canvas.";
+            ActionDisplay = "COLOR_PICKER_ACTION_DISPLAY_SHIFT";
             return;
         }
         else

+ 5 - 3
src/PixiEditor/ViewModels/SubViewModels/Tools/Tools/EllipseToolViewModel.cs

@@ -1,6 +1,7 @@
 using System.Windows.Input;
 using ChunkyImageLib.DataHolders;
 using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.Localization;
 using PixiEditor.Models.Commands.Attributes.Commands;
 
 namespace PixiEditor.ViewModels.SubViewModels.Tools.Tools;
@@ -8,21 +9,22 @@ namespace PixiEditor.ViewModels.SubViewModels.Tools.Tools;
 [Command.Tool(Key = Key.C)]
 internal class EllipseToolViewModel : ShapeTool
 {
-    private string defaultActionDisplay = "Click and move mouse to draw an ellipse. Hold Shift to draw a circle.";
+    private string defaultActionDisplay = "ELLIPSE_TOOL_ACTION_DISPLAY_DEFAULT";
+    public override string ToolNameLocalizationKey => "ELLIPSE_TOOL";
 
     public EllipseToolViewModel()
     {
         ActionDisplay = defaultActionDisplay;
     }
 
-    public override string Tooltip => $"Draws an ellipse on canvas ({Shortcut}). Hold Shift to draw a circle.";
+    public override LocalizedString Tooltip => new LocalizedString("ELLIPSE_TOOL_TOOLTIP", Shortcut);
     public bool DrawCircle { get; private set; }
 
     public override void ModifierKeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
     {
         if (shiftIsDown)
         {
-            ActionDisplay = "Click and move mouse to draw a circle.";
+            ActionDisplay = "ELLIPSE_TOOL_ACTION_DISPLAY_SHIFT";
             DrawCircle = true;
         }
         else

+ 4 - 2
src/PixiEditor/ViewModels/SubViewModels/Tools/Tools/EraserToolViewModel.cs

@@ -1,6 +1,7 @@
 using System.Windows.Input;
 using ChunkyImageLib.DataHolders;
 using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.Localization;
 using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.ViewModels.SubViewModels.Tools.ToolSettings.Toolbars;
 using PixiEditor.Views.UserControls.Overlays.BrushShapeOverlay;
@@ -12,16 +13,17 @@ internal class EraserToolViewModel : ToolViewModel
 {
     public EraserToolViewModel()
     {
-        ActionDisplay = "Draw to remove color from a pixel.";
+        ActionDisplay = "ERASER_TOOL_ACTION_DISPLAY";
         Toolbar = ToolbarFactory.Create<EraserToolViewModel, BasicToolbar>();
     }
 
     [Settings.Inherited]
     public int ToolSize => GetValue<int>();
 
+    public override string ToolNameLocalizationKey => "ERASER_TOOL";
     public override BrushShape BrushShape => BrushShape.Circle;
 
-    public override string Tooltip => $"Erasers color from pixel. ({Shortcut})";
+    public override LocalizedString Tooltip => new LocalizedString("ERASER_TOOL_TOOLTIP", Shortcut);
 
     public override void OnLeftMouseButtonDown(VecD pos)
     {

+ 5 - 3
src/PixiEditor/ViewModels/SubViewModels/Tools/Tools/FloodFillToolViewModel.cs

@@ -1,6 +1,7 @@
 using System.Windows.Input;
 using ChunkyImageLib.DataHolders;
 using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.Localization;
 using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.Views.UserControls.Overlays.BrushShapeOverlay;
 
@@ -9,11 +10,12 @@ namespace PixiEditor.ViewModels.SubViewModels.Tools.Tools;
 [Command.Tool(Key = Key.G)]
 internal class FloodFillToolViewModel : ToolViewModel
 {
-    private readonly string defaultActionDisplay = "Press on an area to fill it. Hold down Ctrl to consider all layers.";
+    private readonly string defaultActionDisplay = "FLOOD_FILL_TOOL_ACTION_DISPLAY_DEFAULT";
 
+    public override string ToolNameLocalizationKey => "FLOOD_FILL_TOOL";
     public override BrushShape BrushShape => BrushShape.Pixel;
 
-    public override string Tooltip => $"Fills area with color. ({Shortcut})";
+    public override LocalizedString Tooltip => new LocalizedString("FLOOD_FILL_TOOL_TOOLTIP", Shortcut);
 
     public bool ConsiderAllLayers { get; private set; }
 
@@ -27,7 +29,7 @@ internal class FloodFillToolViewModel : ToolViewModel
         if (ctrlIsDown)
         {
             ConsiderAllLayers = true;
-            ActionDisplay = "Press on an area to fill it. Release Ctrl to only consider the current layers.";
+            ActionDisplay = "FLOOD_FILL_TOOL_ACTION_DISPLAY_CTRL";
         }
         else
         {

+ 8 - 6
src/PixiEditor/ViewModels/SubViewModels/Tools/Tools/LassoToolViewModel.cs

@@ -1,6 +1,7 @@
 using System.Windows.Input;
 using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.Localization;
 using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.ViewModels.SubViewModels.Tools.ToolSettings.Toolbars;
 using PixiEditor.Views.UserControls.Overlays.BrushShapeOverlay;
@@ -10,7 +11,7 @@ namespace PixiEditor.ViewModels.SubViewModels.Tools.Tools;
 [Command.ToolAttribute(Key = Key.Q)]
 internal class LassoToolViewModel : ToolViewModel
 {
-    private string defaultActionDisplay = "Click and move to select pixels inside of the lasso. Hold Shift to add to existing selection. Hold Ctrl to subtract from it.";
+    private string defaultActionDisplay = "LASSO_TOOL_ACTION_DISPLAY_DEFAULT";
 
     public LassoToolViewModel()
     {
@@ -25,12 +26,12 @@ internal class LassoToolViewModel : ToolViewModel
     {
         if (shiftIsDown)
         {
-            ActionDisplay = "Click and move to add pixels inside of the lasso to the selection.";
+            ActionDisplay = "LASSO_TOOL_ACTION_DISPLAY_SHIFT";
             modifierKeySelectionMode = SelectionMode.Add;
         }
         else if (ctrlIsDown)
         {
-            ActionDisplay = "Click and move to subtract pixels inside of the lasso from the selection.";
+            ActionDisplay = "LASSO_TOOL_ACTION_DISPLAY_CTRL";
             modifierKeySelectionMode = SelectionMode.Subtract;
         }
         else
@@ -40,11 +41,12 @@ internal class LassoToolViewModel : ToolViewModel
         }
     }
 
-    public override string Tooltip => $"Lasso. ({Shortcut})";
-    
+    public override LocalizedString Tooltip => new LocalizedString("LASSO_TOOL_TOOLTIP", Shortcut);
+
+    public override string ToolNameLocalizationKey => "LASSO_TOOL";
     public override BrushShape BrushShape => BrushShape.Pixel;
 
-    [Settings.Enum("Mode")]
+    [Settings.Enum("MODE_LABEL")]
     public SelectionMode SelectMode => GetValue<SelectionMode>();
     
     public override void OnLeftMouseButtonDown(VecD pos)

+ 5 - 3
src/PixiEditor/ViewModels/SubViewModels/Tools/Tools/LineToolViewModel.cs

@@ -1,6 +1,7 @@
 using System.Windows.Input;
 using ChunkyImageLib.DataHolders;
 using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.Localization;
 using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.ViewModels.SubViewModels.Tools.ToolSettings.Toolbars;
 
@@ -9,7 +10,7 @@ namespace PixiEditor.ViewModels.SubViewModels.Tools.Tools;
 [Command.Tool(Key = Key.L)]
 internal class LineToolViewModel : ShapeTool
 {
-    private string defaultActionDisplay = "Click and move to draw a line. Hold Shift to enable snapping.";
+    private string defaultActionDisplay = "LINE_TOOL_ACTION_DISPLAY_DEFAULT";
 
     public LineToolViewModel()
     {
@@ -17,7 +18,8 @@ internal class LineToolViewModel : ShapeTool
         Toolbar = ToolbarFactory.Create<LineToolViewModel, BasicToolbar>();
     }
 
-    public override string Tooltip => $"Draws line on canvas ({Shortcut}). Hold Shift to enable snapping.";
+    public override string ToolNameLocalizationKey => "LINE_TOOL";
+    public override LocalizedString Tooltip => new LocalizedString("LINE_TOOL_TOOLTIP", Shortcut);
 
     [Settings.Inherited]
     public int ToolSize => GetValue<int>();
@@ -28,7 +30,7 @@ internal class LineToolViewModel : ShapeTool
     {
         if (shiftIsDown)
         {
-            ActionDisplay = "Click and move mouse to draw a line with snapping enabled.";
+            ActionDisplay = "LINE_TOOL_ACTION_DISPLAY_SHIFT";
             Snap = true;
         }
         else

Some files were not shown because too many files changed in this diff