Browse Source

Revamp the localization framework

Antony Male 9 years ago
parent
commit
d14ff87209

+ 0 - 166
src/SyncTrayzor/Localization/CustomPluralLocalizationFormatter.cs

@@ -1,166 +0,0 @@
-using SmartFormat.Core.Extensions;
-using SmartFormat.Core.Formatting;
-using SmartFormat.Utilities;
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-
-namespace SyncTrayzor.Localization
-{
-    public class PluralLocalizationFormatter : IFormatter
-    {
-        private string[] names = { "plural", "p", "" };
-        public string[] Names { get { return names; } set { names = value; } }
-
-        /// <summary>
-        /// Initializes the plugin with rules for many common languages.
-        /// If no CultureInfo is supplied to the formatter, the
-        /// default language rules will be used by default.
-        /// </summary>
-        public PluralLocalizationFormatter(string defaultTwoLetterISOLanguageName)
-        {
-            this.DefaultTwoLetterISOLanguageName = defaultTwoLetterISOLanguageName;
-        }
-
-        private PluralRules.PluralRuleDelegate defaultPluralRule;
-        private string defaultTwoLetterISOLanguageName;
-        public string DefaultTwoLetterISOLanguageName
-        {
-            get
-            {
-                return this.defaultTwoLetterISOLanguageName;
-            }
-            set
-            {
-                this.defaultTwoLetterISOLanguageName = value;
-                this.defaultPluralRule = PluralRules.GetPluralRule(value);
-            }
-        }
-
-        private PluralRules.PluralRuleDelegate GetPluralRule(IFormattingInfo formattingInfo)
-        {
-            // See if the language was explicitly passed:
-            var pluralOptions = formattingInfo.FormatterOptions;
-            if (pluralOptions.Length != 0)
-            {
-                return PluralRules.GetPluralRule(pluralOptions);
-            }
-
-            // See if a CustomPluralRuleProvider is available from the FormatProvider:
-            var provider = formattingInfo.FormatDetails.Provider;
-            if (provider != null)
-            {
-                var pluralRuleProvider = (CustomPluralRuleProvider)provider.GetFormat(typeof(CustomPluralRuleProvider));
-                if (pluralRuleProvider != null)
-                {
-                    return pluralRuleProvider.GetPluralRule();
-                }
-            }
-
-            // Use the CultureInfo, if provided:
-            var cultureInfo = provider as CultureInfo;
-            if (cultureInfo != null)
-            {
-                var culturePluralRule = PluralRules.GetPluralRule(cultureInfo.TwoLetterISOLanguageName);
-                return culturePluralRule;
-            }
-
-
-            // Use the default, if provided:
-            if (this.defaultPluralRule != null)
-            {
-                return this.defaultPluralRule;
-            }
-
-            return null;
-        }
-
-        public bool TryEvaluateFormat(IFormattingInfo formattingInfo)
-        {
-            var format = formattingInfo.Format;
-            var current = formattingInfo.CurrentValue;
-
-            // Ignore formats that start with "?" (this can be used to bypass this extension)
-            if (format == null || format.baseString[format.startIndex] == ':')
-            {
-                return false;
-            }
-
-            // Extract the plural words from the format string:
-            var pluralWords = format.Split('|');
-            // This extension requires at least two plural words:
-            if (pluralWords.Count == 1) return false;
-
-            decimal value;
-
-            // We can format numbers, and IEnumerables. For IEnumerables we look at the number of items
-            // in the collection: this means the user can e.g. use the same parameter for both plural and list, for example
-            // 'Smart.Format("The following {0:plural:person is|people are} impressed: {0:list:{}|, |, and}", new[] { "bob", "alice" });'
-            if (current is byte || current is short || current is int || current is long
-                || current is float || current is double || current is decimal)
-            {
-                // Normalize the number to decimal:
-                value = Convert.ToDecimal(current);
-            }
-            else if (current is IEnumerable<object>)
-            {
-                // Relay on IEnumerable covariance, but don't care about non-generic IEnumerable
-                value = ((IEnumerable<object>)current).Count();
-            }
-            else
-            {
-                // This extension only permits numbers and IEnumerables
-                return false;
-            }
-
-
-            // Get the plural rule:
-            var pluralRule = GetPluralRule(formattingInfo);
-
-            if (pluralRule == null)
-            {
-                // Not a supported language.
-                return false;
-            }
-
-            var pluralCount = pluralWords.Count;
-            var pluralIndex = pluralRule(value, pluralCount);
-
-            if (pluralIndex < 0 || pluralWords.Count <= pluralIndex)
-            {
-                // The plural rule should always return a value in-range!
-                throw new FormattingException(format, "Invalid number of plural parameters", pluralWords.Last().endIndex);
-            }
-
-            // Output the selected word (allowing for nested formats):
-            var pluralForm = pluralWords[pluralIndex];
-            formattingInfo.Write(pluralForm, current);
-            return true;
-        }
-
-    }
-
-    /// <summary>
-    /// Use this class to provide custom plural rules to Smart.Format
-    /// </summary>
-    public class CustomPluralRuleProvider : IFormatProvider
-    {
-        public object GetFormat(Type formatType)
-        {
-            return (formatType == typeof(CustomPluralRuleProvider)) ? this : null;
-        }
-
-        private readonly PluralRules.PluralRuleDelegate pluralRule;
-        public CustomPluralRuleProvider(PluralRules.PluralRuleDelegate pluralRule)
-        {
-            this.pluralRule = pluralRule;
-        }
-
-        public PluralRules.PluralRuleDelegate GetPluralRule()
-        {
-            return pluralRule;
-        }
-    }
-
-}

+ 31 - 44
src/SyncTrayzor/Localization/LocExtension.cs

@@ -10,8 +10,6 @@ namespace SyncTrayzor.Localization
 
         public Binding KeyBinding { get; set; }
 
-        public string StringFormat { get; set; }
-
         public Binding ValueBinding { get; set; }
 
         public MultiBinding ValueBindings { get; set; }
@@ -34,66 +32,55 @@ namespace SyncTrayzor.Localization
             if (this.ValueBinding != null && this.ValueBindings != null)
                 throw new ArgumentException("ValueBinding and ValueBindings may not be set at the same time");
 
-            // If we've got no bindings, return a string. If we've got 1 binding, return it. If we've got 2 bindings,
-            // return a new MultiBinding.
-            // Unfortunately there's no nice way to generalise this...
+            // Most of these conditions are redundent, according to the assertions above. However I'll still state them,
+            // for clarity.
 
-            if (this.KeyBinding == null && this.ValueBinding == null && this.ValueBindings == null)
+            // A static key, and no values
+            if (this.Key != null && this.KeyBinding == null && this.ValueBinding == null && this.ValueBindings == null)
             {
                 // Just returning a string!
-                return String.Format(this.StringFormat ?? "{0}", Localizer.Translate(this.Key));
-            }
-
-            var converter = new LocalizeConverter();
-            converter.StringFormat = this.StringFormat;
-
-            // Single binding case
-            if (this.KeyBinding != null && this.ValueBinding == null && this.ValueBindings == null)
-            {
-                // Don't set the key, so it'll assume the binding is the key
-                converter.Converter = this.KeyBinding.Converter;
-                this.KeyBinding.Converter = converter;
-                return this.KeyBinding.ProvideValue(serviceProvider);
+                return Localizer.Translate(this.Key);
             }
-            if (this.KeyBinding == null && this.ValueBinding != null && this.ValueBindings == null)
+            // A static key, and a single value
+            if (this.Key != null && this.KeyBinding == null && this.ValueBinding != null && this.ValueBindings == null)
             {
-                // Set the key, it'll interpret the binding as the value
-                converter.Key = this.Key;
-                converter.Converter = this.ValueBinding.Converter;
+                var converter = new StaticKeySingleValueConverter() { Key = this.Key, Converter = this.ValueBinding.Converter };
                 this.ValueBinding.Converter = converter;
                 return this.ValueBinding.ProvideValue(serviceProvider);
             }
-            if (this.KeyBinding == null && this.ValueBinding == null && this.ValueBindings != null)
+            // A static key, and multiple values
+            if (this.Key != null && this.KeyBinding == null && this.ValueBinding == null && this.ValueBindings != null)
             {
-                converter.Key = this.Key;
-                // No converter allowed here
+                var converter = new StaticKeyMultipleValuesConverter() { Key = this.Key, Converter = this.ValueBindings.Converter };
                 this.ValueBindings.Converter = converter;
                 return this.ValueBindings.ProvideValue(serviceProvider);
             }
-
-            MultiBinding multiBinding;
-
-            // OK, multibinding cases
-            // If this.ValueBindings is set, we'll hijack that
-            // Otherwise, we'll create our own
-            if (this.ValueBindings != null)
+            // A bound key, no values
+            if (this.Key == null && this.KeyBinding != null && this.ValueBinding == null && this.ValueBindings == null)
             {
-                // Not setting converter.Converter - no support yet
-                multiBinding = this.ValueBindings;
+                var converter = new BoundKeyNoValuesConverter() { Converter = this.KeyBinding.Converter };
+                this.KeyBinding.Converter = converter;
+                return this.KeyBinding.ProvideValue(serviceProvider);
             }
-            else // this.ValueBinding != null, according to preconditions
+            // A bound key, and one value
+            if (this.Key == null && this.KeyBinding != null && this.ValueBinding != null && this.ValueBindings == null)
             {
-                multiBinding = new MultiBinding();
+                var converter = new BoundKeyWithValuesConverter();
+                var multiBinding = new MultiBinding() { Converter = converter };
+                multiBinding.Bindings.Add(this.KeyBinding);
                 multiBinding.Bindings.Add(this.ValueBinding);
+                return multiBinding.ProvideValue(serviceProvider);
+            }
+            // A bound key, and multiple values
+            if (this.Key == null && this.KeyBinding != null && this.ValueBinding == null && this.ValueBindings != null)
+            {
+                var converter = new BoundKeyWithValuesConverter() { ValuesConverter = this.ValueBindings.Converter };
+                this.ValueBindings.Bindings.Insert(0, this.KeyBinding);
+                this.ValueBindings.Converter = converter;
+                return this.ValueBindings.ProvideValue(serviceProvider);
             }
 
-            multiBinding.Converter = converter;
-            if (this.Key != null) // Can't hit this case if ValueBinding != null
-                converter.Key = this.Key;
-            else
-                multiBinding.Bindings.Insert(0, this.KeyBinding);
-
-            return multiBinding.ProvideValue(serviceProvider);
+            throw new Exception("Should never get here");
         }
     }
 }

+ 0 - 105
src/SyncTrayzor/Localization/LocalizeConverter.cs

@@ -1,105 +0,0 @@
-using System;
-using System.Linq;
-using System.Windows;
-using System.Windows.Data;
-
-namespace SyncTrayzor.Localization
-{
-    public class LocalizeConverter : DependencyObject, IValueConverter, IMultiValueConverter
-    {
-        /// <summary>
-        /// This singleton avoid the need to declare a resource before using the class. Instead use {x:Static ...}
-        /// </summary>
-        public static readonly LocalizeConverter Singleton = new LocalizeConverter();
-
-        /// <summary>
-        /// Can be set, in which case it is used as the key for the property given to Convert
-        /// </summary>
-        public string Key
-        {
-            get { return (string)GetValue(KeyProperty); }
-            set { SetValue(KeyProperty, value); }
-        }
-
-        public static readonly DependencyProperty KeyProperty =
-            DependencyProperty.Register("Key", typeof(string), typeof(LocalizeConverter), null);
-
-        public IValueConverter Converter
-        {
-            get { return (IValueConverter)GetValue(ConverterProperty); }
-            set { SetValue(ConverterProperty, value); }
-        }
-
-        public static readonly DependencyProperty ConverterProperty =
-            DependencyProperty.Register("Converter", typeof(IValueConverter), typeof(LocalizeConverter), null);
-
-
-        public string StringFormat
-        {
-            get { return (string)GetValue(StringFormatProperty); }
-            set { SetValue(StringFormatProperty, value); }
-        }
-
-        public static readonly DependencyProperty StringFormatProperty =
-            DependencyProperty.Register("StringFormat", typeof(string), typeof(LocalizeConverter), new PropertyMetadata(null));
-
-        
-        /// <summary>
-        /// If Key is specified, Takes a resource key in Key, and binds to the argument. Else uses value as the key
-        /// </summary>
-        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
-        {
-            // If this.Key is set, we're the value. If not, we're the key
-            string result;
-
-            if (this.Converter != null)
-                value = this.Converter.Convert(value, targetType, parameter, culture);
-
-            if (this.Key != null)
-                result = Localizer.Translate(this.Key, value);
-            else if (value is string)
-                result = Localizer.Translate((string)value);
-            else
-                result = null;
-
-            if (this.StringFormat != null)
-                result = String.Format(this.StringFormat, result);
-
-            return result;
-        }
-
-        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
-        {
-            throw new NotImplementedException();
-        }
-
-        /// <summary>
-        /// As a MultiValueConverter, if this Key is set, then we're the values. If it isn't, then values[0] is the key
-        /// </summary>
-        public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
-        {
-            string result;
-
-            if (this.Key == null)
-            {
-                if (values.Length < 1)
-                    result = null;
-                else if (values[0] is string)
-                    result = Localizer.Translate((string)values[0], values.Skip(1).ToArray());
-                else
-                    return null;
-            }
-            else
-            {
-                result = Localizer.Translate(this.Key, values);
-            }
-
-            return result;
-        }
-
-        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
-        {
-            throw new NotImplementedException();
-        }
-    }
-}

+ 127 - 0
src/SyncTrayzor/Localization/LocalizeConverters.cs

@@ -0,0 +1,127 @@
+using System;
+using System.Globalization;
+using System.Linq;
+using System.Windows.Data;
+
+namespace SyncTrayzor.Localization
+{
+    /// <summary>
+    /// Localization converter which takes a static key and is applied to a single value
+    /// </summary>
+    public class StaticKeySingleValueConverter : IValueConverter
+    {
+        /// <summary>
+        ///  Key to use to look up a localized string
+        /// </summary>
+        public string Key { get; set; }
+
+        /// <summary>
+        ///  Optional converter to apply to the value
+        /// </summary>
+        public IValueConverter Converter { get; set; }
+
+        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            if (this.Key == null)
+                throw new InvalidOperationException("Key must not be null");
+
+            var convertedValue = (this.Converter == null) ? value : this.Converter.Convert(value, targetType, parameter, culture);
+            return Localizer.Translate(this.Key, convertedValue);
+        }
+
+        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            throw new NotSupportedException();
+        }
+    }
+
+    /// <summary>
+    ///  Localization converter which takes a static key and is applied to multiple values
+    /// </summary>
+    public class StaticKeyMultipleValuesConverter : IMultiValueConverter
+    {
+        /// <summary>
+        ///  Key to use to look up a localized string
+        /// </summary>
+        public string Key { get; set; }
+
+        /// <summary>
+        ///  Optional converter to apply to the values. May return a scalar or an array
+        /// </summary>
+        public IMultiValueConverter Converter { get; set; }
+
+        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
+        {
+            if (this.Key == null)
+                throw new InvalidOperationException("Key must not be null");
+
+            if (this.Converter == null)
+                return Localizer.Translate(this.Key, values);
+
+            var convertedValues = this.Converter.Convert(values, targetType, parameter, culture);
+            return Localizer.Translate(this.Key, convertedValues as object[] ?? new object[] { convertedValues });
+        }
+
+        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
+        {
+            throw new NotSupportedException();
+        }
+    }
+
+    /// <summary>
+    ///  Localization converter which is applied to a key binding
+    /// </summary>
+    public class BoundKeyNoValuesConverter : IValueConverter
+    {
+        /// <summary>
+        ///  Optional converter to apply to the key binding first
+        /// </summary>
+        public IValueConverter Converter { get; set; }
+
+        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            var stringValue = (this.Converter == null) ? value as string : this.Converter.Convert(value, targetType, parameter, culture) as string;
+            return (stringValue != null) ? Localizer.Translate(stringValue) : null;
+        }
+
+        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            throw new NotSupportedException();
+        }
+    }
+
+    /// <summary>
+    ///  Localization converter which is applied to a MultiBinding of { Key Binding, Value Binding, [Value Binding [...]]
+    /// </summary>
+    public class BoundKeyWithValuesConverter : IMultiValueConverter
+    {
+        /// <summary>
+        ///  Optional converter to apply to the value bindings (not the key binding)
+        /// </summary>
+        public IMultiValueConverter ValuesConverter { get; set; }
+
+        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
+        {
+            // The first item is the key. The rest are the values
+
+            if (values.Length < 1)
+                return null;
+
+            var key = values[0] as string;
+            if (key == null)
+                return null;
+
+            var parameters = values.Skip(1).ToArray();
+            if (this.ValuesConverter == null)
+                return Localizer.Translate(key, parameters);
+
+            var convertedParameters = this.ValuesConverter.Convert(parameters, targetType, parameter, culture);
+            return Localizer.Translate(key, convertedParameters as object[] ?? new object[] { convertedParameters });
+        }
+
+        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
+        {
+            throw new NotSupportedException();
+        }
+    }
+}

+ 6 - 2
src/SyncTrayzor/Localization/Localizer.cs

@@ -10,6 +10,7 @@ namespace SyncTrayzor.Localization
     public static class Localizer
     {
         private static readonly SmartFormatter formatter;
+        private static readonly CultureInfo baseCulture = new CultureInfo("en-US", false);
 
         static Localizer()
         {
@@ -35,7 +36,10 @@ namespace SyncTrayzor.Localization
             var culture = Thread.CurrentThread.CurrentUICulture;
 
             var format = Resources.ResourceManager.GetString(key, culture);
-            
+
+            if (format == null)
+                format = Resources.ResourceManager.GetString(key, baseCulture);
+
             if (format == null)
                 return "!" + key + (parameters.Length > 0 ? ":" + String.Join(",", parameters) : "") + "!";
 
@@ -49,7 +53,7 @@ namespace SyncTrayzor.Localization
 
         public static string OriginalTranslation(string key)
         {
-            return Resources.ResourceManager.GetString(key, new CultureInfo("en-US", false));
+            return Resources.ResourceManager.GetString(key, baseCulture);
         }
     }
 }

+ 8 - 7
src/SyncTrayzor/Pages/FileTransfersTrayView.xaml

@@ -113,14 +113,15 @@
                                                 <TextBlock DockPanel.Dock="Top" Text="{Binding Path}" TextTrimming="CharacterEllipsis">
                                                     <TextBlock.ToolTip>
                                                         <TextBlock>
-                                                            <TextBlock.Resources>
-                                                                <l:LocalizeConverter x:Key="converter" Key="FileTransfersTrayView_PathDetails"/>
-                                                            </TextBlock.Resources>
                                                             <TextBlock.Text>
-                                                                <MultiBinding Converter="{StaticResource converter}">
-                                                                    <Binding Path="FolderId"/>
-                                                                    <Binding Path="FullPath"/>
-                                                                </MultiBinding>
+                                                                <l:LocExtension Key="FileTransfersTrayView_PathDetails">
+                                                                    <l:LocExtension.ValueBindings>
+                                                                        <MultiBinding>
+                                                                            <Binding Path="FolderId"/>
+                                                                            <Binding Path="FullPath"/>
+                                                                        </MultiBinding>
+                                                                    </l:LocExtension.ValueBindings>
+                                                                </l:LocExtension>
                                                             </TextBlock.Text>
                                                         </TextBlock>
                                                     </TextBlock.ToolTip>

+ 1 - 2
src/SyncTrayzor/SyncTrayzor.csproj

@@ -166,7 +166,7 @@
       <SubType>Code</SubType>
     </Compile>
     <Compile Include="Design\DummyFileTransfersTrayViewModel.cs" />
-    <Compile Include="Localization\CustomPluralLocalizationFormatter.cs" />
+    <Compile Include="Localization\LocalizeConverters.cs" />
     <Compile Include="NotifyIcon\BalloonConductor.cs" />
     <Compile Include="NotifyIcon\NotifyIconResolutionUtilities.cs" />
     <Compile Include="Pages\BarAlerts\AlertSeverity.cs" />
@@ -270,7 +270,6 @@
     <Compile Include="Syncthing\SyncthingDidNotStartCorrectlyException.cs" />
     <Compile Include="Syncthing\SyncthingConnectionsWatcherFactory.cs" />
     <Compile Include="Syncthing\EventWatcher\SyncthingEventWatcherFactory.cs" />
-    <Compile Include="Localization\LocalizeConverter.cs" />
     <Compile Include="Localization\Localizer.cs" />
     <Compile Include="Localization\LocExtension.cs" />
     <Compile Include="Pages\NewVersionAlertToastViewModel.cs" />