diff --git a/src/Android/Android.csproj b/src/Android/Android.csproj
index 99a728ad2..bdb15c440 100644
--- a/src/Android/Android.csproj
+++ b/src/Android/Android.csproj
@@ -80,6 +80,7 @@
+
diff --git a/src/Android/MainApplication.cs b/src/Android/MainApplication.cs
index 5fee117ff..638550c8f 100644
--- a/src/Android/MainApplication.cs
+++ b/src/Android/MainApplication.cs
@@ -41,7 +41,11 @@ namespace Bit.Droid
var documentsPath = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
var liteDbStorage = new LiteDbStorageService(Path.Combine(documentsPath, "bitwarden.db"));
var deviceActionService = new DeviceActionService();
+ var localizeService = new LocalizeService();
+ ServiceContainer.Register("localizeService", localizeService);
+ ServiceContainer.Register("i18nService",
+ new MobileI18nService(localizeService.GetCurrentCultureInfo()));
ServiceContainer.Register("cryptoPrimitiveService", new CryptoPrimitiveService());
ServiceContainer.Register("storageService",
new MobileStorageService(preferencesStorage, liteDbStorage));
diff --git a/src/Android/Services/LocalizeService.cs b/src/Android/Services/LocalizeService.cs
new file mode 100644
index 000000000..1521b629c
--- /dev/null
+++ b/src/Android/Services/LocalizeService.cs
@@ -0,0 +1,97 @@
+using System;
+using System.Globalization;
+using Bit.App.Abstractions;
+using Bit.App.Models;
+
+namespace Bit.Droid.Services
+{
+ public class LocalizeService : ILocalizeService
+ {
+ 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;
+ if(androidLanguage.StartsWith("zh"))
+ {
+ if(androidLanguage.Contains("Hant") || androidLanguage.Contains("TW") ||
+ androidLanguage.Contains("HK") || androidLanguage.Contains("MO"))
+ {
+ netLanguage = "zh-Hant";
+ }
+ else
+ {
+ netLanguage = "zh-Hans";
+ }
+ }
+ else
+ {
+ // 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/ILocalizeService.cs b/src/App/Abstractions/ILocalizeService.cs
new file mode 100644
index 000000000..e5d009c4b
--- /dev/null
+++ b/src/App/Abstractions/ILocalizeService.cs
@@ -0,0 +1,9 @@
+using System.Globalization;
+
+namespace Bit.App.Abstractions
+{
+ public interface ILocalizeService
+ {
+ CultureInfo GetCurrentCultureInfo();
+ }
+}
diff --git a/src/App/App.xaml.cs b/src/App/App.xaml.cs
index 186155371..18af7b7b8 100644
--- a/src/App/App.xaml.cs
+++ b/src/App/App.xaml.cs
@@ -1,8 +1,11 @@
using Bit.App.Models;
using Bit.App.Pages;
+using Bit.App.Resources;
+using Bit.App.Services;
using Bit.App.Utilities;
+using Bit.Core.Abstractions;
+using Bit.Core.Utilities;
using System;
-using System.Reflection;
using Xamarin.Forms;
using Xamarin.Forms.StyleSheets;
using Xamarin.Forms.Xaml;
@@ -12,18 +15,23 @@ namespace Bit.App
{
public partial class App : Application
{
+ private readonly MobileI18nService _i18nService;
+
public App()
{
- InitializeComponent();
+ _i18nService = ServiceContainer.Resolve("i18nService") as MobileI18nService;
+ InitializeComponent();
+ SetCulture();
ThemeManager.SetTheme("light");
MainPage = new TabsPage();
+ ServiceContainer.Resolve("platformUtilsService").Init();
MessagingCenter.Subscribe(Current, "ShowDialog", async (sender, details) =>
{
var confirmed = true;
- // TODO: ok text
- var confirmText = string.IsNullOrWhiteSpace(details.ConfirmText) ? "Ok" : details.ConfirmText;
+ var confirmText = string.IsNullOrWhiteSpace(details.ConfirmText) ?
+ AppResources.Ok : details.ConfirmText;
if(!string.IsNullOrWhiteSpace(details.CancelText))
{
confirmed = await MainPage.DisplayAlert(details.Title, details.Text, confirmText,
@@ -51,5 +59,14 @@ namespace Bit.App
{
// Handle when your app resumes
}
+
+ private void SetCulture()
+ {
+ _i18nService.Init();
+ // Calendars are removed by linker. ref https://bugzilla.xamarin.com/show_bug.cgi?id=59077
+ new System.Globalization.ThaiBuddhistCalendar();
+ new System.Globalization.HijriCalendar();
+ new System.Globalization.UmAlQuraCalendar();
+ }
}
}
diff --git a/src/App/Models/PlatformCulture.cs b/src/App/Models/PlatformCulture.cs
new file mode 100644
index 000000000..2b1dc671d
--- /dev/null
+++ b/src/App/Models/PlatformCulture.cs
@@ -0,0 +1,39 @@
+using System;
+
+namespace Bit.App.Models
+{
+ 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 = string.Empty;
+ }
+ }
+
+ 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/App/Services/MobileI18nService.cs b/src/App/Services/MobileI18nService.cs
new file mode 100644
index 000000000..5bb9990b1
--- /dev/null
+++ b/src/App/Services/MobileI18nService.cs
@@ -0,0 +1,67 @@
+using Bit.App.Resources;
+using Bit.Core.Abstractions;
+using System;
+using System.Globalization;
+using System.Reflection;
+using System.Resources;
+using System.Threading;
+
+namespace Bit.App.Services
+{
+ public class MobileI18nService : II18nService
+ {
+ private const string ResourceId = "UsingResxLocalization.Resx.AppResources";
+
+ private static readonly Lazy _resourceManager = new Lazy(() =>
+ new ResourceManager(ResourceId, IntrospectionExtensions.GetTypeInfo(typeof(MobileI18nService)).Assembly));
+
+ private readonly CultureInfo _defaultCulture = new CultureInfo("en-US");
+ private bool _inited;
+
+ public MobileI18nService(CultureInfo systemCulture)
+ {
+ Culture = systemCulture;
+ }
+
+ public CultureInfo Culture { get; set; }
+
+ public void Init(CultureInfo culture = null)
+ {
+ if(_inited)
+ {
+ throw new Exception("I18n already inited.");
+ }
+ _inited = true;
+ if(culture != null)
+ {
+ Culture = culture;
+ }
+ AppResources.Culture = Culture;
+ Thread.CurrentThread.CurrentCulture = Culture;
+ Thread.CurrentThread.CurrentUICulture = Culture;
+ }
+
+ public string T(string id, params string[] p)
+ {
+ return Translate(id, p);
+ }
+
+ public string Translate(string id, params string[] p)
+ {
+ if(string.IsNullOrWhiteSpace(id))
+ {
+ return string.Empty;
+ }
+ var result = _resourceManager.Value.GetString(id, Culture);
+ if(result == null)
+ {
+ result = _resourceManager.Value.GetString(id, _defaultCulture);
+ if(result == null)
+ {
+ result = $"{{{id}}}";
+ }
+ }
+ return string.Format(result, p);
+ }
+ }
+}
diff --git a/src/App/Utilities/TranslateExtension.cs b/src/App/Utilities/TranslateExtension.cs
new file mode 100644
index 000000000..a8d8a940a
--- /dev/null
+++ b/src/App/Utilities/TranslateExtension.cs
@@ -0,0 +1,29 @@
+using Bit.Core.Abstractions;
+using Bit.Core.Utilities;
+using System;
+using Xamarin.Forms;
+using Xamarin.Forms.Xaml;
+
+namespace Bit.App.Utilities
+{
+ [ContentProperty("Text")]
+ public class TranslateExtension : IMarkupExtension
+ {
+ private II18nService _i18nService;
+
+ public TranslateExtension()
+ {
+ _i18nService = ServiceContainer.Resolve("i18nService");
+ }
+
+ public string Id { get; set; }
+ public string P1 { get; set; }
+ public string P2 { get; set; }
+ public string P3 { get; set; }
+
+ public object ProvideValue(IServiceProvider serviceProvider)
+ {
+ return _i18nService.T(Id, P1, P2, P3);
+ }
+ }
+}
diff --git a/src/Core/Abstractions/II18nService.cs b/src/Core/Abstractions/II18nService.cs
new file mode 100644
index 000000000..8eabf1f13
--- /dev/null
+++ b/src/Core/Abstractions/II18nService.cs
@@ -0,0 +1,11 @@
+using System.Globalization;
+
+namespace Bit.Core.Abstractions
+{
+ public interface II18nService
+ {
+ CultureInfo Culture { get; set; }
+ string T(string id, params string[] p);
+ string Translate(string id, params string[] p);
+ }
+}
\ No newline at end of file
diff --git a/src/iOS.Core/Services/LocalizeService.cs b/src/iOS.Core/Services/LocalizeService.cs
new file mode 100644
index 000000000..0c0bef664
--- /dev/null
+++ b/src/iOS.Core/Services/LocalizeService.cs
@@ -0,0 +1,101 @@
+using System;
+using System.Globalization;
+using Bit.App.Abstractions;
+using Bit.App.Models;
+using Foundation;
+
+namespace Bit.iOS.Core.Services
+{
+ public class LocalizeService : ILocalizeService
+ {
+ 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;
+ if(iOSLanguage.StartsWith("zh-Hant") || iOSLanguage.StartsWith("zh-HK"))
+ {
+ netLanguage = "zh-Hant";
+ }
+ else if(iOSLanguage.StartsWith("zh"))
+ {
+ netLanguage = "zh-Hans";
+ }
+ else
+ {
+ // 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);
+ // Use the first part of the identifier (two chars, usually);
+ var netLanguage = platCulture.LanguageCode;
+ 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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/iOS.Core/iOS.Core.csproj b/src/iOS.Core/iOS.Core.csproj
index 89667d36b..dafb4a06d 100644
--- a/src/iOS.Core/iOS.Core.csproj
+++ b/src/iOS.Core/iOS.Core.csproj
@@ -49,9 +49,14 @@
+
+
+ {ee44c6a1-2a85-45fe-8d9b-bf1d5f88809c}
+ App
+
{4b8a8c41-9820-4341-974c-41e65b7f4366}
Core