From 9938fdd4a262c407823d5feeb24f11372587469c Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 26 Nov 2016 10:51:04 -0500 Subject: [PATCH] Localization services for setting culture --- src/Android/Android.csproj | 1 + src/Android/MainActivity.cs | 3 +- src/Android/MainApplication.cs | 1 + src/Android/Services/LocalizeService.cs | 97 +++++++++++++++++ .../Abstractions/Services/ILocalizeService.cs | 31 ++++++ src/App/App.cs | 27 ++++- src/App/App.csproj | 2 + src/App/Models/PlatformCulture.cs | 43 ++++++++ src/iOS.Core/Services/LocalizeService.cs | 101 ++++++++++++++++++ src/iOS.Core/iOS.Core.csproj | 1 + src/iOS.Extension/LoadingViewController.cs | 10 ++ src/iOS/AppDelegate.cs | 5 +- 12 files changed, 319 insertions(+), 3 deletions(-) create mode 100644 src/Android/Services/LocalizeService.cs create mode 100644 src/App/Abstractions/Services/ILocalizeService.cs create mode 100644 src/App/Models/PlatformCulture.cs create mode 100644 src/iOS.Core/Services/LocalizeService.cs diff --git a/src/Android/Android.csproj b/src/Android/Android.csproj index 1292bc1f4..794743037 100644 --- a/src/Android/Android.csproj +++ b/src/Android/Android.csproj @@ -293,6 +293,7 @@ + diff --git a/src/Android/MainActivity.cs b/src/Android/MainActivity.cs index 4079965c4..9f3715fca 100644 --- a/src/Android/MainActivity.cs +++ b/src/Android/MainActivity.cs @@ -61,7 +61,8 @@ namespace Bit.Android Resolver.Resolve(), Resolver.Resolve(), Resolver.Resolve(), - Resolver.Resolve())); + Resolver.Resolve(), + Resolver.Resolve())); MessagingCenter.Subscribe(Xamarin.Forms.Application.Current, "RateApp", (sender) => { diff --git a/src/Android/MainApplication.cs b/src/Android/MainApplication.cs index 7f5ebc40a..e0961b2e0 100644 --- a/src/Android/MainApplication.cs +++ b/src/Android/MainApplication.cs @@ -203,6 +203,7 @@ namespace Bit.Android .RegisterType(new ContainerControlledLifetimeManager()) .RegisterType(new ContainerControlledLifetimeManager()) .RegisterType(new ContainerControlledLifetimeManager()) + .RegisterType(new ContainerControlledLifetimeManager()) // Repositories .RegisterType(new ContainerControlledLifetimeManager()) .RegisterType(new ContainerControlledLifetimeManager()) diff --git a/src/Android/Services/LocalizeService.cs b/src/Android/Services/LocalizeService.cs new file mode 100644 index 000000000..349fc3fb8 --- /dev/null +++ b/src/Android/Services/LocalizeService.cs @@ -0,0 +1,97 @@ +using System; +using System.Threading; +using System.Globalization; +using Bit.App.Models; + +namespace Bit.Android.Services +{ + public class LocalizeService : App.Abstractions.ILocalizeService + { + public void SetLocale(CultureInfo ci) + { + Thread.CurrentThread.CurrentCulture = ci; + Thread.CurrentThread.CurrentUICulture = ci; + Console.WriteLine("CurrentCulture set: " + ci.Name); + } + + public CultureInfo GetCurrentCultureInfo() + { + var netLanguage = "en"; + var androidLocale = Java.Util.Locale.Default; + netLanguage = AndroidToDotnetLanguage(androidLocale.ToString().Replace("_", "-")); + + // this gets called a lot - try/catch can be expensive so consider caching or something + CultureInfo ci = null; + try + { + ci = new CultureInfo(netLanguage); + } + catch(CultureNotFoundException e1) + { + // iOS locale not valid .NET culture (eg. "en-ES" : English in Spain) + // fallback to first characters, in this case "en" + try + { + var fallback = ToDotnetFallbackLanguage(new PlatformCulture(netLanguage)); + Console.WriteLine(netLanguage + " failed, trying " + fallback + " (" + e1.Message + ")"); + ci = new CultureInfo(fallback); + } + catch(CultureNotFoundException e2) + { + // iOS language not valid .NET culture, falling back to English + Console.WriteLine(netLanguage + " couldn't be set, using 'en' (" + e2.Message + ")"); + ci = new CultureInfo("en"); + } + } + + return ci; + } + + private string AndroidToDotnetLanguage(string androidLanguage) + { + Console.WriteLine("Android Language:" + androidLanguage); + var netLanguage = androidLanguage; + + // certain languages need to be converted to CultureInfo equivalent + switch(androidLanguage) + { + case "ms-BN": // "Malaysian (Brunei)" not supported .NET culture + case "ms-MY": // "Malaysian (Malaysia)" not supported .NET culture + case "ms-SG": // "Malaysian (Singapore)" not supported .NET culture + netLanguage = "ms"; // closest supported + break; + case "in-ID": // "Indonesian (Indonesia)" has different code in .NET + netLanguage = "id-ID"; // correct code for .NET + break; + case "gsw-CH": // "Schwiizertüütsch (Swiss German)" not supported .NET culture + netLanguage = "de-CH"; // closest supported + break; + // add more application-specific cases here (if required) + // ONLY use cultures that have been tested and known to work + } + + Console.WriteLine(".NET Language/Locale:" + netLanguage); + return netLanguage; + } + + private string ToDotnetFallbackLanguage(PlatformCulture platCulture) + { + Console.WriteLine(".NET Fallback Language:" + platCulture.LanguageCode); + var netLanguage = platCulture.LanguageCode; // use the first part of the identifier (two chars, usually); + + switch(platCulture.LanguageCode) + { + case "gsw": + netLanguage = "de-CH"; // equivalent to German (Switzerland) for this app + break; + // add more application-specific cases here (if required) + // ONLY use cultures that have been tested and known to work + } + + Console.WriteLine(".NET Fallback Language/Locale:" + netLanguage + " (application-specific)"); + return netLanguage; + } + + + } +} \ No newline at end of file diff --git a/src/App/Abstractions/Services/ILocalizeService.cs b/src/App/Abstractions/Services/ILocalizeService.cs new file mode 100644 index 000000000..3d12b9614 --- /dev/null +++ b/src/App/Abstractions/Services/ILocalizeService.cs @@ -0,0 +1,31 @@ +using System; +using System.Globalization; + +namespace Bit.App.Abstractions +{ + /// + /// Implementations of this interface MUST convert iOS and Android + /// platform-specific locales to a value supported in .NET because + /// ONLY valid .NET cultures can have their RESX resources loaded and used. + /// + /// + /// Lists of valid .NET cultures can be found here: + /// http://www.localeplanet.com/dotnet/ + /// http://www.csharp-examples.net/culture-names/ + /// You should always test all the locales implemented in your application. + /// + public interface ILocalizeService + { + /// + /// This method must evaluate platform-specific locale settings + /// and convert them (when necessary) to a valid .NET locale. + /// + CultureInfo GetCurrentCultureInfo(); + + /// + /// CurrentCulture and CurrentUICulture must be set in the platform project, + /// because the Thread object can't be accessed in a PCL. + /// + void SetLocale(CultureInfo ci); + } +} diff --git a/src/App/App.cs b/src/App/App.cs index 6fb94a646..6f226120b 100644 --- a/src/App/App.cs +++ b/src/App/App.cs @@ -12,6 +12,8 @@ using Plugin.Connectivity.Abstractions; using System.Net; using Acr.UserDialogs; using XLabs.Ioc; +using System.Reflection; +using Bit.App.Resources; namespace Bit.App { @@ -26,6 +28,7 @@ namespace Bit.App private readonly ISettings _settings; private readonly ILockService _lockService; private readonly IGoogleAnalyticsService _googleAnalyticsService; + private readonly ILocalizeService _localizeService; public App( IAuthService authService, @@ -36,7 +39,8 @@ namespace Bit.App IFingerprint fingerprint, ISettings settings, ILockService lockService, - IGoogleAnalyticsService googleAnalyticsService) + IGoogleAnalyticsService googleAnalyticsService, + ILocalizeService localizeService) { _databaseService = databaseService; _connectivity = connectivity; @@ -47,7 +51,9 @@ namespace Bit.App _settings = settings; _lockService = lockService; _googleAnalyticsService = googleAnalyticsService; + _localizeService = localizeService; + SetCulture(); SetStyles(); if(authService.IsAuthenticated) @@ -347,5 +353,24 @@ namespace Bit.App } }); } + + private void SetCulture() + { + Debug.WriteLine("====== resource debug info ========="); + var assembly = typeof(App).GetTypeInfo().Assembly; + foreach(var res in assembly.GetManifestResourceNames()) + { + Debug.WriteLine("found resource: " + res); + } + Debug.WriteLine("===================================="); + + // This lookup NOT required for Windows platforms - the Culture will be automatically set + if(Device.OS == TargetPlatform.iOS || Device.OS == TargetPlatform.Android) + { + var ci = _localizeService.GetCurrentCultureInfo(); + AppResources.Culture = ci; + _localizeService.SetLocale(ci); + } + } } } diff --git a/src/App/App.csproj b/src/App/App.csproj index f927ea411..0c8c7080d 100644 --- a/src/App/App.csproj +++ b/src/App/App.csproj @@ -77,6 +77,7 @@ + @@ -109,6 +110,7 @@ + diff --git a/src/App/Models/PlatformCulture.cs b/src/App/Models/PlatformCulture.cs new file mode 100644 index 000000000..925a6be51 --- /dev/null +++ b/src/App/Models/PlatformCulture.cs @@ -0,0 +1,43 @@ +using System; + +namespace Bit.App.Models +{ + /// + /// Helper class for splitting locales like + /// iOS: ms_MY, gsw_CH + /// Android: in-ID + /// into parts so we can create a .NET culture (or fallback culture) + /// + public class PlatformCulture + { + public PlatformCulture(string platformCultureString) + { + if(string.IsNullOrWhiteSpace(platformCultureString)) + { + throw new ArgumentException("Expected culture identifier", nameof(platformCultureString)); + } + + // .NET expects dash, not underscore + PlatformString = platformCultureString.Replace("_", "-"); + var dashIndex = PlatformString.IndexOf("-", StringComparison.Ordinal); + if(dashIndex > 0) + { + var parts = PlatformString.Split('-'); + LanguageCode = parts[0]; + LocaleCode = parts[1]; + } + else + { + LanguageCode = PlatformString; + LocaleCode = ""; + } + } + public string PlatformString { get; private set; } + public string LanguageCode { get; private set; } + public string LocaleCode { get; private set; } + public override string ToString() + { + return PlatformString; + } + } +} diff --git a/src/iOS.Core/Services/LocalizeService.cs b/src/iOS.Core/Services/LocalizeService.cs new file mode 100644 index 000000000..95574e8ff --- /dev/null +++ b/src/iOS.Core/Services/LocalizeService.cs @@ -0,0 +1,101 @@ +using System; +using System.Globalization; +using System.Threading; +using Foundation; +using Bit.App.Abstractions; +using Bit.App.Models; + +namespace Bit.iOS.Core.Services +{ + public class LocalizeService : ILocalizeService + { + public void SetLocale(CultureInfo ci) + { + Thread.CurrentThread.CurrentCulture = ci; + Thread.CurrentThread.CurrentUICulture = ci; + Console.WriteLine("CurrentCulture set: " + ci.Name); + } + + public CultureInfo GetCurrentCultureInfo() + { + var netLanguage = "en"; + if(NSLocale.PreferredLanguages.Length > 0) + { + var pref = NSLocale.PreferredLanguages[0]; + + netLanguage = iOSToDotnetLanguage(pref); + } + + // this gets called a lot - try/catch can be expensive so consider caching or something + CultureInfo ci = null; + try + { + ci = new CultureInfo(netLanguage); + } + catch(CultureNotFoundException e1) + { + // iOS locale not valid .NET culture (eg. "en-ES" : English in Spain) + // fallback to first characters, in this case "en" + try + { + var fallback = ToDotnetFallbackLanguage(new PlatformCulture(netLanguage)); + Console.WriteLine(netLanguage + " failed, trying " + fallback + " (" + e1.Message + ")"); + ci = new CultureInfo(fallback); + } + catch(CultureNotFoundException e2) + { + // iOS language not valid .NET culture, falling back to English + Console.WriteLine(netLanguage + " couldn't be set, using 'en' (" + e2.Message + ")"); + ci = new CultureInfo("en"); + } + } + + return ci; + } + + private string iOSToDotnetLanguage(string iOSLanguage) + { + Console.WriteLine("iOS Language:" + iOSLanguage); + var netLanguage = iOSLanguage; + + //certain languages need to be converted to CultureInfo equivalent + switch(iOSLanguage) + { + case "ms-MY": // "Malaysian (Malaysia)" not supported .NET culture + case "ms-SG": // "Malaysian (Singapore)" not supported .NET culture + netLanguage = "ms"; // closest supported + break; + case "gsw-CH": // "Schwiizertüütsch (Swiss German)" not supported .NET culture + netLanguage = "de-CH"; // closest supported + break; + // add more application-specific cases here (if required) + // ONLY use cultures that have been tested and known to work + } + + Console.WriteLine(".NET Language/Locale:" + netLanguage); + return netLanguage; + } + + private string ToDotnetFallbackLanguage(PlatformCulture platCulture) + { + Console.WriteLine(".NET Fallback Language:" + platCulture.LanguageCode); + var netLanguage = platCulture.LanguageCode; // use the first part of the identifier (two chars, usually); + + switch(platCulture.LanguageCode) + { + // + case "pt": + netLanguage = "pt-PT"; // fallback to Portuguese (Portugal) + break; + case "gsw": + netLanguage = "de-CH"; // equivalent to German (Switzerland) for this app + break; + // add more application-specific cases here (if required) + // ONLY use cultures that have been tested and known to work + } + + Console.WriteLine(".NET Fallback Language/Locale:" + netLanguage + " (application-specific)"); + return netLanguage; + } + } +} diff --git a/src/iOS.Core/iOS.Core.csproj b/src/iOS.Core/iOS.Core.csproj index 6fbad0486..4b7e5cfe4 100644 --- a/src/iOS.Core/iOS.Core.csproj +++ b/src/iOS.Core/iOS.Core.csproj @@ -92,6 +92,7 @@ + diff --git a/src/iOS.Extension/LoadingViewController.cs b/src/iOS.Extension/LoadingViewController.cs index 71bdbb12e..3aba5c3fc 100644 --- a/src/iOS.Extension/LoadingViewController.cs +++ b/src/iOS.Extension/LoadingViewController.cs @@ -38,6 +38,7 @@ namespace Bit.iOS.Extension public override void ViewDidLoad() { SetIoc(); + SetCulture(); base.ViewDidLoad(); View.BackgroundColor = new UIColor(red: 0.94f, green: 0.94f, blue: 0.96f, alpha: 1.0f); @@ -276,6 +277,7 @@ namespace Bit.iOS.Extension .RegisterType(new ContainerControlledLifetimeManager()) .RegisterType(new ContainerControlledLifetimeManager()) .RegisterType(new ContainerControlledLifetimeManager()) + .RegisterType(new ContainerControlledLifetimeManager()) // Repositories .RegisterType(new ContainerControlledLifetimeManager()) .RegisterType(new ContainerControlledLifetimeManager()) @@ -292,6 +294,14 @@ namespace Bit.iOS.Extension Resolver.ResetResolver(new UnityResolver(container)); } + private void SetCulture() + { + var localizeService = Resolver.Resolve(); + var ci = localizeService.GetCurrentCultureInfo(); + AppResources.Culture = ci; + localizeService.SetLocale(ci); + } + private bool ProcessItemProvider(NSItemProvider itemProvider, string type, Action action) { if(!itemProvider.HasItemConformingTo(type)) diff --git a/src/iOS/AppDelegate.cs b/src/iOS/AppDelegate.cs index 52450d74c..c7842db42 100644 --- a/src/iOS/AppDelegate.cs +++ b/src/iOS/AppDelegate.cs @@ -64,7 +64,8 @@ namespace Bit.iOS Resolver.Resolve(), Resolver.Resolve(), Resolver.Resolve(), - Resolver.Resolve())); + Resolver.Resolve(), + Resolver.Resolve())); // Appearance stuff @@ -259,6 +260,8 @@ namespace Bit.iOS .RegisterType(new ContainerControlledLifetimeManager()) .RegisterType(new ContainerControlledLifetimeManager()) .RegisterType(new ContainerControlledLifetimeManager()) + .RegisterType(new ContainerControlledLifetimeManager()) + // Repositories // Repositories .RegisterType(new ContainerControlledLifetimeManager()) .RegisterType(new ContainerControlledLifetimeManager())