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())