translation using NGettext.Wpf

This commit is contained in:
stax76
2023-12-08 06:23:17 +01:00
parent 8997a2eacb
commit 5dd3716012
73 changed files with 3001 additions and 17699 deletions

View File

@@ -0,0 +1,32 @@

using System.Globalization;
using System.Windows.Input;
namespace NGettext.Wpf
{
public class ChangeCultureCommand : ICommand
{
public bool CanExecute(object? parameter)
{
return CultureInfo.GetCultures(CultureTypes.SpecificCultures)
.Any(cultureInfo => cultureInfo.Name == (string)parameter);
}
public void Execute(object? parameter)
{
if (CultureTracker is null)
{
CompositionRoot.WriteMissingInitializationErrorMessage();
return;
}
CultureTracker.CurrentCulture =
CultureInfo.GetCultures(CultureTypes.SpecificCultures)
.Single(cultureInfo => cultureInfo.Name == (string)parameter);
}
public event EventHandler? CanExecuteChanged;
public static ICultureTracker? CultureTracker { get; set; }
}
}

View File

@@ -0,0 +1,28 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace NGettext.Wpf.Common
{
public class GettextStringFormatConverter : IValueConverter
{
public string MsgId { get; private set; }
public GettextStringFormatConverter(string msgId)
{
this.MsgId = msgId;
}
public static ILocalizer Localizer { get; set; }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return Localizer.Gettext(MsgId, value);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,29 @@

using System.Globalization;
using NGettext.Wpf.Common;
using NGettext.Wpf.EnumTranslation;
namespace NGettext.Wpf
{
public static class CompositionRoot
{
public static void Compose(string domainName, CultureInfo cultureInfo, string localeFolder)
{
var cultureTracker = new CultureTracker();
cultureTracker.CurrentCulture = cultureInfo;
var localizer = new Localizer(cultureTracker, domainName, localeFolder);
ChangeCultureCommand.CultureTracker = cultureTracker;
GettextExtension.Localizer = localizer;
TrackCurrentCultureBehavior.CultureTracker = cultureTracker;
LocalizeEnumConverter.EnumLocalizer = new EnumLocalizer(localizer);
Translation.Localizer = localizer;
GettextStringFormatConverter.Localizer = localizer;
}
internal static void WriteMissingInitializationErrorMessage()
{
Console.Error.WriteLine("NGettext.Wpf: NGettext.Wpf.CompositionRoot.Compose() must be called at the entry point of the application for localization to work");
}
}
}

View File

@@ -0,0 +1,16 @@
using System;
using System.Globalization;
namespace NGettext.Wpf
{
public class CultureEventArgs : EventArgs
{
public CultureEventArgs(CultureInfo cultureInfo)
{
if (cultureInfo == null) throw new ArgumentNullException(nameof(cultureInfo));
CultureInfo = cultureInfo;
}
public CultureInfo CultureInfo { get; }
}
}

View File

@@ -0,0 +1,64 @@

using System.Globalization;
namespace NGettext.Wpf;
public interface ICultureTracker
{
[Obsolete("Use AddWeakCultureObserver() instead. Otherwise the culture tracker (which is probably a singleton) will keep your object alive for longer than it needs to. This method will be removed in 2.x")]
event EventHandler<CultureEventArgs> CultureChanged;
event EventHandler<CultureEventArgs> CultureChanging;
CultureInfo CurrentCulture { get; set; }
void AddWeakCultureObserver(IWeakCultureObserver weakCultureObserver);
}
public class CultureTracker : ICultureTracker
{
private CultureInfo _currentCulture = CultureInfo.CurrentUICulture;
private List<WeakReference<IWeakCultureObserver>> _weakObservers = new List<WeakReference<IWeakCultureObserver>>();
public event EventHandler<CultureEventArgs> CultureChanged;
public CultureInfo CurrentCulture
{
get => _currentCulture;
set
{
if (_currentCulture == value)
return;
CultureChanging?.Invoke(this, new CultureEventArgs(value));
_currentCulture = value;
RaiseCultureChanged();
}
}
protected virtual void RaiseCultureChanged()
{
var cultureEventArgs = new CultureEventArgs(CurrentCulture);
CultureChanged?.Invoke(this, cultureEventArgs);
var weakObserversStillAlive = new List<WeakReference<IWeakCultureObserver>>();
foreach (var weakReference in _weakObservers)
{
if (!weakReference.TryGetTarget(out var observer))
continue;
observer.HandleCultureChanged(this, cultureEventArgs);
weakObserversStillAlive.Add(weakReference);
}
_weakObservers = weakObserversStillAlive;
}
public event EventHandler<CultureEventArgs> CultureChanging;
public void AddWeakCultureObserver(IWeakCultureObserver weakCultureObserver)
{
_weakObservers.Add(new WeakReference<IWeakCultureObserver>(weakCultureObserver));
}
}

View File

@@ -0,0 +1,36 @@
using System;
using System.Linq;
using System.Reflection;
namespace NGettext.Wpf.EnumTranslation
{
public interface IEnumLocalizer
{
string LocalizeEnum(Enum value);
}
public class EnumLocalizer : IEnumLocalizer
{
private readonly ILocalizer _localizer;
public EnumLocalizer(ILocalizer localizer)
{
_localizer = localizer ?? throw new ArgumentNullException(nameof(localizer));
}
public string LocalizeEnum(Enum value)
{
var type = value.GetType();
var enumMemberName = value.ToString();
var msgIdAttribute = (EnumMsgIdAttribute)type.GetMember(enumMemberName).SingleOrDefault()?.GetCustomAttribute(typeof(EnumMsgIdAttribute), true);
if (msgIdAttribute is null)
{
Console.Error.WriteLine($"{type}.{enumMemberName} lacks the [MsgId(\"...\")] attribute.");
return enumMemberName;
}
return _localizer.Gettext(msgIdAttribute.MsgId);
}
}
}

View File

@@ -0,0 +1,14 @@
using System;
namespace NGettext.Wpf.EnumTranslation
{
public class EnumMsgIdAttribute : Attribute
{
public EnumMsgIdAttribute(string msgId)
{
MsgId = msgId;
}
public string MsgId { get; }
}
}

View File

@@ -0,0 +1,55 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace NGettext.Wpf.EnumTranslation
{
public class LocalizeEnumConverter : IValueConverter
{
private IEnumLocalizer _enumLocalizer;
public LocalizeEnumConverter()
{
}
public LocalizeEnumConverter(IEnumLocalizer enumLocalizer)
{
_enumLocalizer = enumLocalizer;
}
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var enumLocalizer = GetEnumLocalizer();
if (enumLocalizer is null)
{
return value;
}
if (value is Enum enumValue)
{
return enumLocalizer.LocalizeEnum(enumValue);
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
public static IEnumLocalizer EnumLocalizer { get; set; }
private IEnumLocalizer GetEnumLocalizer()
{
var result = _enumLocalizer ?? EnumLocalizer;
if (result is null)
{
CompositionRoot.WriteMissingInitializationErrorMessage();
}
return result;
}
}
}

View File

@@ -0,0 +1,96 @@

using System.ComponentModel;
using System.Windows;
using System.Windows.Markup;
namespace NGettext.Wpf
{
[MarkupExtensionReturnType(typeof(string))]
public class GettextExtension : MarkupExtension, IWeakCultureObserver
{
private DependencyObject _dependencyObject;
private DependencyProperty _dependencyProperty;
[ConstructorArgument("params")] public object[] Params { get; set; }
[ConstructorArgument("msgId")] public string MsgId { get; set; }
public GettextExtension(string msgId)
{
MsgId = msgId;
Params = new object[] { };
}
public GettextExtension(string msgId, params object[] @params)
{
MsgId = msgId;
Params = @params;
}
public static ILocalizer Localizer { get; set; }
public override object ProvideValue(IServiceProvider serviceProvider)
{
var provideValueTarget = (IProvideValueTarget)serviceProvider.GetService(typeof(IProvideValueTarget));
if (provideValueTarget.TargetObject is DependencyObject dependencyObject)
{
_dependencyObject = dependencyObject;
if (DesignerProperties.GetIsInDesignMode(_dependencyObject))
{
return Gettext();
}
AttachToCultureChangedEvent();
_dependencyProperty = (DependencyProperty)provideValueTarget.TargetProperty;
KeepGettextExtensionAliveForAsLongAsDependencyObject();
}
else
{
System.Console.WriteLine("NGettext.Wpf: Target object of type {0} is not yet implemented", provideValueTarget.TargetObject?.GetType());
}
return Gettext();
}
private string Gettext()
{
return Params.Any() ? Localizer.Gettext(MsgId, Params) : Localizer.Gettext(MsgId);
}
void KeepGettextExtensionAliveForAsLongAsDependencyObject()
{
SetGettextExtension(_dependencyObject, this);
}
void AttachToCultureChangedEvent()
{
if (Localizer is null)
{
Console.Error.WriteLine("NGettext.WPF.GettextExtension.Localizer not set. Localization is disabled.");
return;
}
Localizer.CultureTracker.AddWeakCultureObserver(this);
}
public void HandleCultureChanged(ICultureTracker sender, CultureEventArgs eventArgs)
{
_dependencyObject.SetValue(_dependencyProperty, Gettext());
}
public static readonly DependencyProperty GettextExtensionProperty = DependencyProperty.RegisterAttached(
"GettextExtension", typeof(GettextExtension), typeof(GettextExtension), new PropertyMetadata(default(GettextExtension)));
public static void SetGettextExtension(DependencyObject element, GettextExtension value)
{
element.SetValue(GettextExtensionProperty, value);
}
public static GettextExtension GetGettextExtension(DependencyObject element)
{
return (GettextExtension)element.GetValue(GettextExtensionProperty);
}
}
}

View File

@@ -0,0 +1,21 @@
using System;
using System.Windows.Markup;
using NGettext.Wpf.Common;
namespace NGettext.Wpf
{
public class GettextFormatConverterExtension : MarkupExtension
{
public GettextFormatConverterExtension(string msgId)
{
MsgId = msgId;
}
[ConstructorArgument("msgId")] public string MsgId { get; set; }
public override object ProvideValue(IServiceProvider serviceProvider)
{
return new GettextStringFormatConverter(MsgId);
}
}
}

View File

@@ -0,0 +1,7 @@
namespace NGettext.Wpf
{
public interface IWeakCultureObserver
{
void HandleCultureChanged(ICultureTracker sender, CultureEventArgs eventArgs);
}
}

View File

@@ -0,0 +1,117 @@

using System.Globalization;
using System.IO;
namespace NGettext.Wpf
{
public interface ILocalizer
{
ICatalog Catalog { get; }
ICatalog GetCatalog(CultureInfo cultureInfo);
ICultureTracker CultureTracker { get; }
}
public class Localizer : IDisposable, ILocalizer
{
string _domainName;
string _localeFolder;
public Localizer(ICultureTracker cultureTracker, string domainName, string localeFolder)
{
_domainName = domainName;
_localeFolder = localeFolder;
CultureTracker = cultureTracker;
if (cultureTracker == null)
throw new ArgumentNullException(nameof(cultureTracker));
cultureTracker.CultureChanging += ResetCatalog;
ResetCatalog(cultureTracker.CurrentCulture);
}
void ResetCatalog(object sender, CultureEventArgs e)
{
ResetCatalog(e.CultureInfo);
}
void ResetCatalog(CultureInfo cultureInfo)
{
Catalog = GetCatalog(cultureInfo);
}
public ICatalog GetCatalog(CultureInfo cultureInfo) =>
new Catalog(_domainName, _localeFolder, cultureInfo);
public ICatalog Catalog { get; private set; }
public ICultureTracker CultureTracker { get; }
public void Dispose()
{
CultureTracker.CultureChanging -= ResetCatalog;
}
}
public static class LocalizerExtensions
{
internal struct MsgIdWithContext
{
internal string Context { get; set; }
internal string MsgId { get; set; }
}
internal static MsgIdWithContext ConvertToMsgIdWithContext(string msgId)
{
var result = new MsgIdWithContext { MsgId = msgId };
if (msgId.Contains("|"))
{
var pipePosition = msgId.IndexOf('|');
result.Context = msgId.Substring(0, pipePosition);
result.MsgId = msgId.Substring(pipePosition + 1);
}
return result;
}
internal static string Gettext(this ILocalizer localizer, string msgId, params object[] values)
{
if (msgId == null)
return "";
var msgIdWithContext = ConvertToMsgIdWithContext(msgId);
if (localizer is null)
{
CompositionRoot.WriteMissingInitializationErrorMessage();
return string.Format(msgIdWithContext.MsgId, values);
}
if (msgIdWithContext.Context != null)
{
return localizer.Catalog.GetParticularString(msgIdWithContext.Context, msgIdWithContext.MsgId, values);
}
return localizer.Catalog.GetString(msgIdWithContext.MsgId, values);
}
internal static string? Gettext(this ILocalizer localizer, string msgId)
{
if (msgId is null)
return null;
var msgIdWithContext = ConvertToMsgIdWithContext(msgId);
if (localizer is null)
{
CompositionRoot.WriteMissingInitializationErrorMessage();
return msgIdWithContext.MsgId;
}
if (msgIdWithContext.Context != null)
return localizer.Catalog.GetParticularString(msgIdWithContext.Context, msgIdWithContext.MsgId);
return localizer.Catalog.GetString(msgIdWithContext.MsgId);
}
}
}

View File

@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.77" />
<PackageReference Include="NGettext" Version="0.6.7" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,24 @@
<?xml version="1.0"?>
<package>
<metadata>
<id>NGettext.Wpf</id>
<version>1.2.6-alpha</version>
<title>WPF support for NGettext</title>
<authors>Robert Jørgensgaard Engdahl</authors>
<owners>Robert Jørgensgaard Engdahl</owners>
<license type="expression">LGPL-3.0-or-later</license>
<projectUrl>https://github.com/robert-j-engdahl/ngettext-wpf</projectUrl>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<description>Proper internationalization support for WPF (via NGettext). In particular a GetTextMarkupExtension is included, which is what everyone uses anyway.</description>
<copyright>Copyright 2017, 2018, 2019 Accuratech ApS</copyright>
<tags>gettext wpf ngettext gettextmarkupextension xgettext-xaml</tags>
<dependencies>
<dependency id="NGettext" version="0.6.3" />
<dependency id="Expression.Blend.Sdk" version="1.0.2" />
</dependencies>
</metadata>
<files>
<file src="../XGetText.Xaml/XGetText-Xaml.ps1" target="tools" />
<file src="../XGetText.Xaml/Init.ps1" target="tools" />
</files>
</package>

View File

@@ -0,0 +1,36 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("NGettext.Wpf")]
[assembly: AssemblyDescription("Proper internationalizations upport for WPF (via NGettext).")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("NGettext.Wpf")]
[assembly: AssemblyCopyright("Copyright © 2017, 2018, 2019")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("4bb0eb35-fe32-4295-bf39-4da1c9b67bb8")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.2.2.0")]
[assembly: AssemblyFileVersion("1.2.2.0")]

View File

@@ -0,0 +1,38 @@
Releasing
=========
- Update the `.nuspec` file with a new version number.
- Build in release mode
- Invoke `PM> nuget pack`; no options needed. But it will release the debug version, so make sure your build configuration match! Say `PM> nuget pack -Prop Configuration=Release`.
- Sign the created package, for example by
```
PM> nuget sign .\NGettext.Wpf.1.1.0-alpha.nupkg -Timestamper http://timestamp.digicert.com -CertificateFingerprint 79a047643b02e7b677d5d0a962bc02ac19e63ca8
```
- Verify signing with
```
PM> nuget verify -Signatures .\NGettext.Wpf.1.1.0-alpha.nupkg
Verifying NGettext.Wpf.1.1.0-alpha
C:\Git\ngettext-wpf\NGettext.Wpf\.\NGettext.Wpf.1.1.0-alpha.nupkg
Signature Hash Algorithm: SHA256
Signature type: Author
Verifying the author primary signature with certificate:
Subject Name: CN=ACCURATECH ApS, O=ACCURATECH ApS, L=Holstebro, C=DK, SERIALNUMBER=27635652, OID.2.5.4.15=Private Organization, OID.1.3.6.1.4.1.311.60.2.1.3=DK
SHA1 hash: 79A047643B02E7B677D5D0A962BC02AC19E63CA8
SHA256 hash: A66690776D4B00270DAA40F0336E4EE8288D2B2F9F77E6B132B63D18F0F408FF
Issued by: CN=DigiCert EV Code Signing CA (SHA2), OU=www.digicert.com, O=DigiCert Inc, C=US
Valid from: 06-02-2018 01:00:00 to 09-02-2021 13:00:00
Timestamp: 19-02-2019 12:32:52
Verifying author primary signature's timestamp with timestamping service certificate:
Subject Name: CN=DigiCert SHA2 Timestamp Responder, O=DigiCert, C=US
SHA1 hash: 400191475C98891DEBA104AF47091B5EB6D4CBCB
SHA256 hash: FC834D5BFFDE31DBA5B79BF95F573F7953BCBF9156E8525163E828EB92EA8A93
Issued by: CN=DigiCert SHA2 Assured ID Timestamping CA, OU=www.digicert.com, O=DigiCert Inc, C=US
Valid from: 04-01-2017 01:00:00 to 18-01-2028 01:00:00
Successfully verified package 'NGettext.Wpf.1.1.0-alpha'
```

View File

@@ -0,0 +1,47 @@
using System.ComponentModel;
using System.Windows;
using System.Windows.Markup;
using Microsoft.Xaml.Behaviors;
namespace NGettext.Wpf
{
/// <summary>
/// Makes sure that the CultureInfo used for all binding operations inside the associated
/// FrameworkElement follows the CurrentCulture of the CultureTracker injected to the static
/// CultureTracker property.
///
/// For instance, dates and numbers bound with a culture specific StringFormat will be formatted
/// according to the tracked culture and even reformatted on culture changed.
/// </summary>
public class TrackCurrentCultureBehavior : Behavior<FrameworkElement>, IWeakCultureObserver
{
public static ICultureTracker CultureTracker { get; set; }
protected override void OnAttached()
{
if (!DesignerProperties.GetIsInDesignMode(AssociatedObject))
{
if (CultureTracker is null)
{
CompositionRoot.WriteMissingInitializationErrorMessage();
return;
}
CultureTracker.AddWeakCultureObserver(this);
UpdateAssociatedObjectCulture();
}
base.OnAttached();
}
void UpdateAssociatedObjectCulture()
{
if (AssociatedObject is null) return;
AssociatedObject.Language = XmlLanguage.GetLanguage(CultureTracker.CurrentCulture.IetfLanguageTag);
}
public void HandleCultureChanged(ICultureTracker sender, CultureEventArgs eventArgs)
{
UpdateAssociatedObjectCulture();
}
}
}

View File

@@ -0,0 +1,65 @@

using System.Globalization;
namespace NGettext.Wpf
{
public static class Translation
{
public static string _(string msgId) => Localizer?.Gettext(msgId) ?? "";
public static string _(string msgId, params object[] parameters)
{
return parameters.Any()
? Localizer?.Gettext(msgId, parameters) ?? ""
: Localizer?.Gettext(msgId) ?? "";
}
public static ILocalizer? Localizer { get; set; }
public static string Noop(string msgId) => msgId;
[Obsolete("Use GetPluralString() instead. This method will be removed in 2.x")]
public static string PluralGettext(int n, string singularMsgId, string pluralMsgId, params object[] @params)
{
return GetPluralString(singularMsgId, pluralMsgId, n, @params);
}
public static string GetPluralString(string singularMsgId, string pluralMsgId, int n, params object[] args)
{
if (Localizer is null)
{
CompositionRoot.WriteMissingInitializationErrorMessage();
return string.Format(CultureInfo.InvariantCulture, n == 1 ? singularMsgId : pluralMsgId, args);
}
return args.Any()
? Localizer.Catalog.GetPluralString(singularMsgId, pluralMsgId, n, args)
: Localizer.Catalog.GetPluralString(singularMsgId, pluralMsgId, n);
}
public static string GetParticularPluralString(string context, string text, string pluralText, int n, params object[] args)
{
if (Localizer is null)
{
CompositionRoot.WriteMissingInitializationErrorMessage();
return string.Format(CultureInfo.InvariantCulture, n == 1 ? text : pluralText, args);
}
return args.Any()
? Localizer.Catalog.GetParticularPluralString(context, text, pluralText, n, args)
: Localizer.Catalog.GetParticularPluralString(context, text, pluralText, n);
}
public static string GetParticularString(string context, string text, params object[] args)
{
if (Localizer is null)
{
CompositionRoot.WriteMissingInitializationErrorMessage();
return (args.Any() ? string.Format(CultureInfo.InvariantCulture, text, args) : text);
}
return args.Any()
? Localizer.Catalog.GetParticularString(context, text, args)
: Localizer.Catalog.GetParticularString(context, text);
}
}
}

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Expression.Blend.Sdk" version="1.0.2" targetFramework="net45" />
<package id="NGettext" version="0.6.3" targetFramework="net45" />
<package id="NuGet.CommandLine" version="4.7.1" targetFramework="net45" developmentDependency="true" />
</packages>