i18n service

This commit is contained in:
Kyle Spearrin 2019-04-11 15:33:10 -04:00
parent 6a65b6d735
commit 6ee109dc80
11 changed files with 384 additions and 4 deletions

View File

@ -80,6 +80,7 @@
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Services\CryptoPrimitiveService.cs" />
<Compile Include="Services\DeviceActionService.cs" />
<Compile Include="Services\LocalizeService.cs" />
</ItemGroup>
<ItemGroup>
<AndroidAsset Include="Assets\FontAwesome.ttf" />

View File

@ -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<ILocalizeService>("localizeService", localizeService);
ServiceContainer.Register<II18nService>("i18nService",
new MobileI18nService(localizeService.GetCurrentCultureInfo()));
ServiceContainer.Register<ICryptoPrimitiveService>("cryptoPrimitiveService", new CryptoPrimitiveService());
ServiceContainer.Register<IStorageService>("storageService",
new MobileStorageService(preferencesStorage, liteDbStorage));

View File

@ -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;
}
}
}

View File

@ -0,0 +1,9 @@
using System.Globalization;
namespace Bit.App.Abstractions
{
public interface ILocalizeService
{
CultureInfo GetCurrentCultureInfo();
}
}

View File

@ -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<II18nService>("i18nService") as MobileI18nService;
InitializeComponent();
SetCulture();
ThemeManager.SetTheme("light");
MainPage = new TabsPage();
ServiceContainer.Resolve<MobilePlatformUtilsService>("platformUtilsService").Init();
MessagingCenter.Subscribe<Application, DialogDetails>(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();
}
}
}

View File

@ -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;
}
}
}

View File

@ -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> _resourceManager = new Lazy<ResourceManager>(() =>
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);
}
}
}

View File

@ -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<II18nService>("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);
}
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}
}

View File

@ -49,9 +49,14 @@
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Services\CryptoPrimitiveService.cs" />
<Compile Include="Services\KeyChainStorageService.cs" />
<Compile Include="Services\LocalizeService.cs" />
<Compile Include="Views\Toast.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\App\App.csproj">
<Project>{ee44c6a1-2a85-45fe-8d9b-bf1d5f88809c}</Project>
<Name>App</Name>
</ProjectReference>
<ProjectReference Include="..\Core\Core.csproj">
<Project>{4b8a8c41-9820-4341-974c-41e65b7f4366}</Project>
<Name>Core</Name>