Browse Source

Localization and languages in extensions

Krzysztof Krysiński 2 years ago
parent
commit
cb66ee777a
25 changed files with 230 additions and 58 deletions
  1. 1 3
      src/PixiEditor.Extensions/Common/Localization/ILocalizationProvider.cs
  2. 10 1
      src/PixiEditor.Extensions/Common/Localization/LanguageData.cs
  3. 21 3
      src/PixiEditor.Extensions/Common/Localization/LocalizationData.cs
  4. 6 4
      src/PixiEditor.Extensions/Extension.cs
  5. 9 6
      src/PixiEditor.Extensions/Metadata/ExtensionMetadata.cs
  6. 7 1
      src/PixiEditor.Extensions/Palettes/PaletteListDataSource.cs
  7. 4 1
      src/PixiEditor/App.xaml.cs
  8. 8 1
      src/PixiEditor/Data/Localization/Languages/en.json
  9. 1 1
      src/PixiEditor/Data/Localization/LocalizationDataSchema.json
  10. 4 3
      src/PixiEditor/Helpers/Extensions/ServiceCollectionHelpers.cs
  11. 7 0
      src/PixiEditor/Models/AppExtensions/ExtensionException.cs
  12. 20 10
      src/PixiEditor/Models/AppExtensions/ExtensionLoader.cs
  13. 5 0
      src/PixiEditor/Models/DataProviders/LocalPalettesFetcher.cs
  14. 71 11
      src/PixiEditor/Models/Localization/LocalizationProvider.cs
  15. 2 5
      src/PixiEditor/ViewModels/SubViewModels/Main/ExtensionsViewModel.cs
  16. 1 1
      src/PixiEditor/Views/Dialogs/SettingsWindow.xaml
  17. 10 0
      src/PixiEditor/Views/ICustomTranslatorElement.cs
  18. 6 3
      src/PixiEditor/Views/MainWindow.xaml.cs
  19. 5 1
      src/PixiEditor/Views/Translator.cs
  20. 12 1
      src/PixiEditor/Views/UserControls/Chip.xaml.cs
  21. 3 0
      src/SampleExtension/Localization/en.json
  22. 2 1
      src/SampleExtension/SampleExtension.cs
  23. 5 0
      src/SampleExtension/SampleExtension.csproj
  24. 1 1
      src/SampleExtension/TestPaletteDataSource.cs
  25. 9 0
      src/SampleExtension/extension.json

+ 1 - 3
src/PixiEditor.Extensions/Common/Localization/ILocalizationProvider.cs

@@ -1,6 +1,4 @@
-using PixiEditor.Models.Localization;
-
-namespace PixiEditor.Extensions.Common.Localization;
+namespace PixiEditor.Extensions.Common.Localization;
 
 public interface ILocalizationProvider
 {

+ 10 - 1
src/PixiEditor.Extensions/Common/Localization/LanguageData.cs

@@ -11,7 +11,13 @@ public class LanguageData
     
     // https://icons8.com/icon/set/flags/color
     public string IconFileName { get; set; }
-    public string IconPath => $"pack://application:,,,/PixiEditor;component/Images/LanguageFlags/{IconFileName}";
+    public string IconPath = $"pack://application:,,,/PixiEditor;component/Images/LanguageFlags/";
+
+    [JsonIgnore]
+    public List<string> AdditionalLocalePaths { get; set; }
+
+    [JsonIgnore]
+    public string IconFullPath => $"{IconPath}{IconFileName}";
     public bool RightToLeft { get; set; }
     
     [JsonIgnore]
@@ -19,6 +25,9 @@ public class LanguageData
     
     [JsonProperty(nameof(LastUpdated))]
     private string LastUpdatedString { get; set; }
+
+    [JsonIgnore]
+    public string? CustomLocaleAssemblyPath { get; set; }
     
     public override string ToString()
     {

+ 21 - 3
src/PixiEditor.Extensions/Common/Localization/LocalizationData.cs

@@ -1,10 +1,28 @@
 using System.Diagnostics;
-using PixiEditor.Extensions.Common.Localization;
+using System.IO;
 
-namespace PixiEditor.Models.Localization;
+namespace PixiEditor.Extensions.Common.Localization;
 
 [DebuggerDisplay("{Languages.Count} Language(s)")]
 public class LocalizationData
 {
-    public List<LanguageData> Languages { get; set; }
+    public List<LanguageData> Languages { get; set; } = new();
+
+    public void MergeWith(List<LanguageData> toMerge, string assemblyLocation)
+    {
+        foreach (LanguageData language in toMerge)
+        {
+            LanguageData existing = Languages.Find(x => x.Code == language.Code);
+            if (existing is null)
+            {
+                language.CustomLocaleAssemblyPath = assemblyLocation;
+                Languages.Add(language);
+            }
+            else
+            {
+                existing.AdditionalLocalePaths ??= new List<string>();
+                existing.AdditionalLocalePaths.Add(Path.Combine(assemblyLocation, language.LocaleFileName));
+            }
+        }
+    }
 }

+ 6 - 4
src/PixiEditor.Extensions/Extension.cs

@@ -11,6 +11,7 @@ public abstract class Extension
 {
     public ExtensionServices Api { get; private set; }
     public ExtensionMetadata Metadata { get; private set; }
+    public Assembly Assembly => GetType().Assembly;
 
     public void ProvideMetadata(ExtensionMetadata metadata)
     {
@@ -22,19 +23,20 @@ public abstract class Extension
         Metadata = metadata;
     }
 
-    public void Load(ExtensionServices api)
+    public void Load()
     {
-        Api = api;
         OnLoaded();
     }
 
-    public void Initialize()
+    public void Initialize(ExtensionServices api)
     {
+        Api = api;
         OnInitialized();
     }
 
     /// <summary>
-    ///     Called right after the extension is loaded. Not all extensions are initialized at this point.
+    ///     Called right after the extension is loaded. Not all extensions are initialized at this point. PixiEditor API at this point is not available.
+    ///     Use this method to load resources, patch language files, etc.
     /// </summary>
     protected virtual void OnLoaded()
     {

+ 9 - 6
src/PixiEditor.Extensions/Metadata/ExtensionMetadata.cs

@@ -1,14 +1,17 @@
-namespace PixiEditor.Extensions.Metadata;
+using PixiEditor.Extensions.Common.Localization;
+
+namespace PixiEditor.Extensions.Metadata;
 
 public class ExtensionMetadata
 {
     public string UniqueName { get; init; }
     public string DisplayName { get; init; }
     public string Description { get; init; }
-    public Author Author { get; init; }
-    public Author Publisher { get; init; }
-    public Author[] Contributors { get; init; }
+    public Author? Author { get; init; }
+    public Author? Publisher { get; init; }
+    public Author[]? Contributors { get; init; }
     public string Version { get; init; }
-    public string License { get; init; }
-    public string[] Categories { get; init; }
+    public string? License { get; init; }
+    public string[]? Categories { get; init; }
+    public LocalizationData? Localization { get; init; }
 }

+ 7 - 1
src/PixiEditor.Extensions/Palettes/PaletteListDataSource.cs

@@ -1,12 +1,18 @@
 using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Extensions.Palettes.Parsers;
-using PixiEditor.Models.Localization;
 
 namespace PixiEditor.Extensions.Palettes;
 
 public abstract class PaletteListDataSource
 {
     public LocalizedString Name { get; set; }
+
+    public PaletteListDataSource(LocalizedString name)
+    {
+        Name = name;
+        AvailableParsers = new List<PaletteFileParser>();
+    }
+
     public virtual void Initialize() { }
 
     /// <summary>

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

@@ -52,7 +52,10 @@ internal partial class App : Application
 
         AddNativeAssets();
 
-        MainWindow = new MainWindow();
+        ExtensionLoader extensionLoader = new ExtensionLoader();
+        extensionLoader.LoadExtensions();
+
+        MainWindow = new MainWindow(extensionLoader);
         MainWindow.Show();
     }
 

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

@@ -562,5 +562,12 @@
   "TRANSFORM_ACTION_DISPLAY_SCALE_ROTATE_SHEAR_PERSPECTIVE": "Drag handles to scale transform. Hold Ctrl and drag a handle to move the handle freely. Hold Shift to scale proportionally. Hold Alt and drag a side handle to shear. Drag outside handles to rotate.",
   "TRANSFORM_ACTION_DISPLAY_SCALE_ROTATE_SHEAR_NOPERSPECTIVE": "Drag handles to scale transform. Hold Shift to scale proportionally. Hold Alt and drag a side handle to shear. Drag outside handles to rotate.",
   "TRANSFORM_ACTION_DISPLAY_SCALE_ROTATE_NOSHEAR_NOPERSPECTIVE": "Drag handles to scale transform. Hold Shift to scale proportionally. Drag outside handles to rotate.",
-  "TRANSFORM_ACTION_DISPLAY_SCALE_NOROTATE_NOSHEAR_NOPERSPECTIVE": "Drag handles to scale transform. Hold Shift to scale proportionally."
+  "TRANSFORM_ACTION_DISPLAY_SCALE_NOROTATE_NOSHEAR_NOPERSPECTIVE": "Drag handles to scale transform. Hold Shift to scale proportionally.",
+
+  "LOCAL_PALETTE_SOURCE_NAME": "Local",
+
+  "ERROR_FORBIDDEN_UNIQUE_NAME": "Extension unique name cannot start with 'pixieditor'.",
+  "ERROR_MISSING_METADATA": "Extension metadata key '{0}' is missing.",
+  "ERROR_NO_CLASS_ENTRY": "Extension class entry is missing on path '{0}'.",
+  "ERROR_NO_ENTRY_ASSEMBLY": "Extension entry assembly is missing on path '{0}'."
 }

+ 1 - 1
src/PixiEditor/Data/Localization/LocalizationDataSchema.json

@@ -18,7 +18,7 @@
           },
           "localeFileName": {
             "type": "string",
-            "description": "The name of the key-value json file found in Data/Localization/Languages",
+            "description": "The name of the key-value json file found in Data/Localization/Languages. Must be prepended with extension unique name and : (e.g. pixieditor.sampleExtension:en.json)",
             "pattern": ".*\\.json",
             "format": "uri",
             "default": ".json"

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

@@ -4,6 +4,7 @@ using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Extensions.Palettes;
 using PixiEditor.Extensions.Palettes.Parsers;
 using PixiEditor.Extensions.Windowing;
+using PixiEditor.Models.AppExtensions;
 using PixiEditor.Models.AppExtensions.Services;
 using PixiEditor.Models.Commands;
 using PixiEditor.Models.Controllers;
@@ -27,10 +28,10 @@ internal static class ServiceCollectionHelpers
     /// <summary>
     /// Adds all the services required to fully run PixiEditor's MainWindow
     /// </summary>
-    public static IServiceCollection AddPixiEditor(this IServiceCollection collection) => collection
+    public static IServiceCollection AddPixiEditor(this IServiceCollection collection, ExtensionLoader extensionLoader) => collection
         .AddSingleton<ViewModelMain>()
         .AddSingleton<IPreferences, PreferencesSettings>()
-        .AddSingleton<ILocalizationProvider, LocalizationProvider>()
+        .AddSingleton<ILocalizationProvider, LocalizationProvider>(x => new LocalizationProvider(extensionLoader))
         // View Models
         .AddSingleton<StylusViewModel>()
         .AddSingleton<WindowViewModel>()
@@ -49,7 +50,7 @@ internal static class ServiceCollectionHelpers
         .AddSingleton<DebugViewModel>()
         .AddSingleton<SearchViewModel>()
         .AddSingleton<AdditionalContentViewModel>()
-        .AddSingleton<ExtensionsViewModel>()
+        .AddSingleton(x => new ExtensionsViewModel(x.GetService<ViewModelMain>(), extensionLoader))
         // Controllers
         .AddSingleton<ShortcutController>()
         .AddSingleton<CommandController>()

+ 7 - 0
src/PixiEditor/Models/AppExtensions/ExtensionException.cs

@@ -32,3 +32,10 @@ public class MissingMetadataException : ExtensionException
     }
 }
 
+public class ForbiddenUniqueNameExtension : ExtensionException
+{
+    public ForbiddenUniqueNameExtension() : base(new LocalizedString("ERROR_FORBIDDEN_UNIQUE_NAME"))
+    {
+    }
+}
+

+ 20 - 10
src/PixiEditor/Models/AppExtensions/ExtensionLoader.cs

@@ -1,24 +1,21 @@
 using System.IO;
 using System.Reflection;
 using System.Windows;
-using Microsoft.Extensions.DependencyInjection;
 using Newtonsoft.Json;
 using PixiEditor.Extensions;
 using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Extensions.Metadata;
-using PixiEditor.Models.Dialogs;
 using PixiEditor.Models.IO;
-using PixiEditor.Models.Localization;
+using PixiEditor.Platform;
 
 namespace PixiEditor.Models.AppExtensions;
 
 internal class ExtensionLoader
 {
-    public ExtensionServices Api { get; }
-    private List<Extension> LoadedExtensions { get; } = new();
-    public ExtensionLoader(ExtensionServices pixiEditorApi)
+    public List<Extension> LoadedExtensions { get; } = new();
+
+    public ExtensionLoader()
     {
-        Api = pixiEditorApi;
         ValidateExtensionFolder();
     }
 
@@ -36,11 +33,11 @@ internal class ExtensionLoader
         }
     }
 
-    public void InitializeExtensions()
+    public void InitializeExtensions(ExtensionServices pixiEditorApi)
     {
         foreach (var extension in LoadedExtensions)
         {
-            extension.Initialize();
+            extension.Initialize(pixiEditorApi);
         }
     }
 
@@ -52,7 +49,7 @@ internal class ExtensionLoader
             var metadata = JsonConvert.DeserializeObject<ExtensionMetadata>(json);
             ValidateMetadata(metadata);
             var extension = LoadExtensionEntry(Path.GetDirectoryName(packageJsonPath), metadata);
-            extension.Load(Api);
+            extension.Load();
             LoadedExtensions.Add(extension);
         }
         catch (JsonException)
@@ -75,6 +72,14 @@ internal class ExtensionLoader
         {
             throw new MissingMetadataException("Description");
         }
+
+        if (metadata.UniqueName.StartsWith("pixieditor".Trim(), StringComparison.OrdinalIgnoreCase))
+        {
+            if(!IsOfficialAssemblyLegit(metadata.UniqueName))
+            {
+                throw new ForbiddenUniqueNameExtension();
+            }
+        }
         // TODO: Validate if unique name is unique
 
         if (string.IsNullOrEmpty(metadata.DisplayName))
@@ -88,6 +93,11 @@ internal class ExtensionLoader
         }
     }
 
+    private bool IsOfficialAssemblyLegit(string metadataUniqueName)
+    {
+        return true; //TODO: Perform assembly secret number check
+    }
+
     private Extension LoadExtensionEntry(string assemblyFolder, ExtensionMetadata metadata)
     {
         string[] dlls = Directory.GetFiles(assemblyFolder, "*.dll");

+ 5 - 0
src/PixiEditor/Models/DataProviders/LocalPalettesFetcher.cs

@@ -1,5 +1,6 @@
 using System.IO;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
+using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Extensions.Palettes;
 using PixiEditor.Extensions.Palettes.Parsers;
 using PixiEditor.Models.DataHolders;
@@ -22,6 +23,10 @@ internal class LocalPalettesFetcher : PaletteListDataSource
 
     private FileSystemWatcher watcher;
 
+    public LocalPalettesFetcher() : base("LOCAL_PALETTE_SOURCE_NAME")
+    {
+    }
+
     public override void Initialize()
     {
         InitDir();

+ 71 - 11
src/PixiEditor/Models/Localization/LocalizationProvider.cs

@@ -1,7 +1,9 @@
 using System.Globalization;
 using System.IO;
 using Newtonsoft.Json;
+using PixiEditor.Extensions;
 using PixiEditor.Extensions.Common.Localization;
+using PixiEditor.Models.AppExtensions;
 using PixiEditor.Models.IO;
 using PixiEditor.Models.UserPreferences;
 
@@ -17,11 +19,13 @@ internal class LocalizationProvider : ILocalizationProvider
     public LanguageData FollowSystem { get; } = new() { Name = "Follow system", Code = "system" };
     public event Action<Language> OnLanguageChanged;
     public void ReloadLanguage() => OnLanguageChanged?.Invoke(CurrentLanguage);
-
     public Language DefaultLanguage { get; private set; }
 
-    public LocalizationProvider()
+    private ExtensionLoader extensionLoader;
+
+    public LocalizationProvider(ExtensionLoader extensionLoader)
     {
+        this.extensionLoader = extensionLoader;
         ILocalizationProvider.SetAsCurrent(this);
     }
 
@@ -36,12 +40,14 @@ internal class LocalizationProvider : ILocalizationProvider
         
         using StreamReader reader = new(LocalizationDataPath);
         LocalizationData = serializer.Deserialize<LocalizationData>(new JsonTextReader(reader) { Culture = CultureInfo.InvariantCulture, DateTimeZoneHandling = DateTimeZoneHandling.Utc });
-            
+
         if (LocalizationData is null)
         {
             throw new InvalidDataException("Localization data is null.");
         }
-        
+
+        LoadExtensionLocalizationData(LocalizationData);
+
         if (LocalizationData.Languages is null || LocalizationData.Languages.Count == 0)
         {
             throw new InvalidDataException("Localization data does not contain any languages.");
@@ -56,6 +62,29 @@ internal class LocalizationProvider : ILocalizationProvider
         LoadLanguage(LocalizationData.Languages.FirstOrDefault(x => x.Code == currentLanguageCode, FollowSystem));
     }
 
+    private void LoadExtensionLocalizationData(LocalizationData localizationData)
+    {
+        if(localizationData is null)
+        {
+            throw new InvalidDataException(nameof(localizationData));
+        }
+
+        if (extensionLoader?.LoadedExtensions is null)
+        {
+            return;
+        }
+
+        foreach (Extension extension in extensionLoader.LoadedExtensions)
+        {
+            if (extension.Metadata.Localization is null)
+            {
+                continue;
+            }
+
+            localizationData.MergeWith(extension.Metadata.Localization.Languages, Path.GetDirectoryName(extension.Assembly.Location));
+        }
+    }
+
     public void LoadLanguage(LanguageData languageData)
     {
         if (languageData is null)
@@ -102,17 +131,31 @@ internal class LocalizationProvider : ILocalizationProvider
 
     private Language LoadLanguageInternal(LanguageData languageData)
     {
-        string localePath = Path.Combine(Paths.DataFullPath, "Localization", "Languages", languageData.LocaleFileName);
+        string mainLocalePath = GetLocalePath(languageData);
 
-        if (!File.Exists(localePath))
+        if (!File.Exists(mainLocalePath))
         {
-            throw new FileNotFoundException("Locale file not found.", localePath);
+            throw new FileNotFoundException("Locale file not found.", mainLocalePath);
         }
 
-        Newtonsoft.Json.JsonSerializer serializer = new();
-        using StreamReader reader = new(localePath);
-        Dictionary<string, string> locale =
-            serializer.Deserialize<Dictionary<string, string>>(new Newtonsoft.Json.JsonTextReader(reader));
+        Dictionary<string, string> locale = new Dictionary<string, string>();
+
+        languageData.AdditionalLocalePaths ??= new List<string>();
+        int localesCount = 1 + languageData.AdditionalLocalePaths.Count;
+
+        string[] allLocalePaths = new string[localesCount];
+        allLocalePaths[0] = mainLocalePath;
+        languageData.AdditionalLocalePaths.CopyTo(allLocalePaths, 1);
+
+        foreach (string localePath in allLocalePaths)
+        {
+            if (!File.Exists(localePath))
+            {
+                continue;
+            }
+
+            locale.AddRangeOverride(ReadLocaleFile(localePath));
+        }
 
         if (locale is null)
         {
@@ -121,4 +164,21 @@ internal class LocalizationProvider : ILocalizationProvider
 
         return new(languageData, locale, languageData.RightToLeft);
     }
+
+    private IDictionary<string, string> ReadLocaleFile(string localePath)
+    {
+        JsonSerializer serializer = new();
+        using StreamReader reader = new(localePath);
+        return serializer.Deserialize<Dictionary<string, string>>(new JsonTextReader(reader));
+    }
+
+    private string GetLocalePath(LanguageData languageData)
+    {
+        if (languageData.CustomLocaleAssemblyPath is not null)
+        {
+            return Path.Combine(languageData.CustomLocaleAssemblyPath, languageData.LocaleFileName);
+        }
+
+        return Path.Combine(Paths.DataFullPath, "Localization", "Languages", languageData.LocaleFileName);
+    }
 }

+ 2 - 5
src/PixiEditor/ViewModels/SubViewModels/Main/ExtensionsViewModel.cs

@@ -9,17 +9,14 @@ namespace PixiEditor.ViewModels.SubViewModels.Main;
 internal class ExtensionsViewModel : SubViewModel<ViewModelMain>
 {
     public ExtensionLoader ExtensionLoader { get; }
-    public ExtensionsViewModel(ViewModelMain owner) : base(owner)
+    public ExtensionsViewModel(ViewModelMain owner, ExtensionLoader loader) : base(owner)
     {
-        ExtensionLoader loader = new ExtensionLoader(new ExtensionServices(owner.Services));
-        loader.LoadExtensions();
-
         ExtensionLoader = loader;
         Owner.OnStartupEvent += Owner_OnStartupEvent;
     }
 
     private void Owner_OnStartupEvent(object sender, EventArgs e)
     {
-        ExtensionLoader.InitializeExtensions();
+        ExtensionLoader.InitializeExtensions(new ExtensionServices(Owner.Services));
     }
 }

+ 1 - 1
src/PixiEditor/Views/Dialogs/SettingsWindow.xaml

@@ -73,7 +73,7 @@
                     <ComboBox.ItemTemplate>
                         <DataTemplate>
                             <StackPanel Orientation="Horizontal">
-                                <Image VerticalAlignment="Center" Margin="5 0" Source="{Binding IconPath}"/>
+                                <Image VerticalAlignment="Center" Margin="5 0" Source="{Binding IconFullPath}"/>
                                 <TextBlock VerticalAlignment="Center" Text="{Binding Name}"/>
                             </StackPanel>
                         </DataTemplate>

+ 10 - 0
src/PixiEditor/Views/ICustomTranslatorElement.cs

@@ -0,0 +1,10 @@
+using System.Windows;
+using System.Windows.Data;
+
+namespace PixiEditor.Views;
+
+public interface ICustomTranslatorElement
+{
+    public void SetTranslationBinding(DependencyProperty dependencyProperty, Binding binding);
+    public DependencyProperty GetDependencyProperty();
+}

+ 6 - 3
src/PixiEditor/Views/MainWindow.xaml.cs

@@ -9,6 +9,7 @@ using Microsoft.Extensions.DependencyInjection;
 using PixiEditor.DrawingApi.Core.Bridge;
 using PixiEditor.DrawingApi.Skia;
 using PixiEditor.Helpers;
+using PixiEditor.Models.AppExtensions;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.IO;
 using PixiEditor.Models.UserPreferences;
@@ -24,6 +25,7 @@ internal partial class MainWindow : Window
     private readonly IPreferences preferences;
     private readonly IPlatform platform;
     private readonly IServiceProvider services;
+    private static ExtensionLoader extLoader;
 
     public static MainWindow Current { get; private set; }
 
@@ -31,15 +33,16 @@ internal partial class MainWindow : Window
 
     public event Action OnDataContextInitialized;
 
-    public MainWindow()
+    public MainWindow(ExtensionLoader extensionLoader)
     {
+        extLoader = extensionLoader;
         Current = this;
 
         IPlatform.RegisterPlatform(GetActivePlatform());
 
         services = new ServiceCollection()
             .AddPlatform()
-            .AddPixiEditor()
+            .AddPixiEditor(extensionLoader)
             .AddExtensionServices()
             .BuildServiceProvider();
 
@@ -91,7 +94,7 @@ internal partial class MainWindow : Window
 
     public static MainWindow CreateWithDocuments(IEnumerable<(string? originalPath, byte[] dotPixiBytes)> documents)
     {
-        MainWindow window = new();
+        MainWindow window = new(extLoader);
         FileViewModel fileVM = window.services.GetRequiredService<FileViewModel>();
 
         foreach (var (path, bytes) in documents)

+ 5 - 1
src/PixiEditor/Views/Translator.cs

@@ -124,7 +124,11 @@ public class Translator : UIElement
             RelativeSource = new RelativeSource(RelativeSourceMode.Self)
         };
 
-        if (d is TextBox textBox)
+        if (d is ICustomTranslatorElement customTranslatorElement)
+        {
+            customTranslatorElement.SetTranslationBinding(customTranslatorElement.GetDependencyProperty(), binding);
+        }
+        else if (d is TextBox textBox)
         {
             textBox.SetBinding(TextBox.TextProperty, binding);
         }

+ 12 - 1
src/PixiEditor/Views/UserControls/Chip.xaml.cs

@@ -1,12 +1,13 @@
 using System.Windows;
 using System.Windows.Controls;
+using System.Windows.Data;
 using System.Windows.Media;
 using PixiEditor.Platform;
 using Brush = System.Drawing.Brush;
 
 namespace PixiEditor.Views.UserControls;
 
-internal partial class Chip : UserControl
+internal partial class Chip : UserControl, ICustomTranslatorElement
 {
     public static readonly DependencyProperty TextProperty = DependencyProperty.Register(
         nameof(Text), typeof(string), typeof(Chip), new PropertyMetadata(default(string)));
@@ -29,5 +30,15 @@ internal partial class Chip : UserControl
     {
         InitializeComponent();
     }
+
+    void ICustomTranslatorElement.SetTranslationBinding(DependencyProperty dependencyProperty, Binding binding)
+    {
+        SetBinding(dependencyProperty, binding);
+    }
+
+    DependencyProperty ICustomTranslatorElement.GetDependencyProperty()
+    {
+        return TextProperty;
+    }
 }
 

+ 3 - 0
src/SampleExtension/Localization/en.json

@@ -0,0 +1,3 @@
+{
+  "SE:DATA_SOURCE_NAME": "Sample Extension"
+}

+ 2 - 1
src/SampleExtension/SampleExtension.cs

@@ -1,4 +1,5 @@
-using System.Windows.Controls;
+using System.Reflection;
+using System.Windows.Controls;
 using PixiEditor.Extensions;
 using PixiEditor.Extensions.Palettes;
 

+ 5 - 0
src/SampleExtension/SampleExtension.csproj

@@ -18,4 +18,9 @@
       </Content>
     </ItemGroup>
 
+  <ItemGroup>
+    <Content Include="Localization\**">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </Content>
+  </ItemGroup>
 </Project>

+ 1 - 1
src/SampleExtension/TestPaletteDataSource.cs

@@ -6,7 +6,7 @@ public class TestPaletteDataSource : PaletteListDataSource
 {
     private List<ExtensionPalette> palettes = new();
 
-    public TestPaletteDataSource()
+    public TestPaletteDataSource() : base("SE:DATA_SOURCE_NAME") // SE: prefix (Sample Extension:) helps to avoid key collisions with other extensions
     {
         palettes.Add(new ExtensionPalette("Test Palette", new List<PaletteColor> { PaletteColor.Black, PaletteColor.White, }, this));
     }

+ 9 - 0
src/SampleExtension/extension.json

@@ -3,6 +3,15 @@
   "uniqueName": "PixiEditor.Samples.SampleExtension",
   "description": "Sample extension for PixiEditor",
   "version": "1.0.0",
+  "localization": {
+    "Languages": [
+      {
+        "name": "English",
+        "code": "en",
+        "localeFileName": "Localization/en.json"
+      }
+    ]
+  },
   "author": {
     "name": "PixiEditor",
     "email": "[email protected]",