Localization services for setting culture

This commit is contained in:
Kyle Spearrin 2016-11-26 10:51:04 -05:00
parent 320d2c5c96
commit 9938fdd4a2
12 changed files with 319 additions and 3 deletions

View file

@ -293,6 +293,7 @@
<Compile Include="Controls\ExtendedTextCellRenderer.cs" /> <Compile Include="Controls\ExtendedTextCellRenderer.cs" />
<Compile Include="Controls\ExtendedPickerRenderer.cs" /> <Compile Include="Controls\ExtendedPickerRenderer.cs" />
<Compile Include="Controls\ExtendedEntryRenderer.cs" /> <Compile Include="Controls\ExtendedEntryRenderer.cs" />
<Compile Include="Services\LocalizeService.cs" />
<Compile Include="MainApplication.cs" /> <Compile Include="MainApplication.cs" />
<Compile Include="Resources\Resource.Designer.cs" /> <Compile Include="Resources\Resource.Designer.cs" />
<Compile Include="Services\DeviceInfoService.cs" /> <Compile Include="Services\DeviceInfoService.cs" />

View file

@ -61,7 +61,8 @@ namespace Bit.Android
Resolver.Resolve<IFingerprint>(), Resolver.Resolve<IFingerprint>(),
Resolver.Resolve<ISettings>(), Resolver.Resolve<ISettings>(),
Resolver.Resolve<ILockService>(), Resolver.Resolve<ILockService>(),
Resolver.Resolve<IGoogleAnalyticsService>())); Resolver.Resolve<IGoogleAnalyticsService>(),
Resolver.Resolve<ILocalizeService>()));
MessagingCenter.Subscribe<Xamarin.Forms.Application>(Xamarin.Forms.Application.Current, "RateApp", (sender) => MessagingCenter.Subscribe<Xamarin.Forms.Application>(Xamarin.Forms.Application.Current, "RateApp", (sender) =>
{ {

View file

@ -203,6 +203,7 @@ namespace Bit.Android
.RegisterType<IAppInfoService, AppInfoService>(new ContainerControlledLifetimeManager()) .RegisterType<IAppInfoService, AppInfoService>(new ContainerControlledLifetimeManager())
.RegisterType<IGoogleAnalyticsService, GoogleAnalyticsService>(new ContainerControlledLifetimeManager()) .RegisterType<IGoogleAnalyticsService, GoogleAnalyticsService>(new ContainerControlledLifetimeManager())
.RegisterType<IDeviceInfoService, DeviceInfoService>(new ContainerControlledLifetimeManager()) .RegisterType<IDeviceInfoService, DeviceInfoService>(new ContainerControlledLifetimeManager())
.RegisterType<ILocalizeService, LocalizeService>(new ContainerControlledLifetimeManager())
// Repositories // Repositories
.RegisterType<IFolderRepository, FolderRepository>(new ContainerControlledLifetimeManager()) .RegisterType<IFolderRepository, FolderRepository>(new ContainerControlledLifetimeManager())
.RegisterType<IFolderApiRepository, FolderApiRepository>(new ContainerControlledLifetimeManager()) .RegisterType<IFolderApiRepository, FolderApiRepository>(new ContainerControlledLifetimeManager())

View file

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

View file

@ -0,0 +1,31 @@
using System;
using System.Globalization;
namespace Bit.App.Abstractions
{
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public interface ILocalizeService
{
/// <summary>
/// This method must evaluate platform-specific locale settings
/// and convert them (when necessary) to a valid .NET locale.
/// </summary>
CultureInfo GetCurrentCultureInfo();
/// <summary>
/// CurrentCulture and CurrentUICulture must be set in the platform project,
/// because the Thread object can't be accessed in a PCL.
/// </summary>
void SetLocale(CultureInfo ci);
}
}

View file

@ -12,6 +12,8 @@ using Plugin.Connectivity.Abstractions;
using System.Net; using System.Net;
using Acr.UserDialogs; using Acr.UserDialogs;
using XLabs.Ioc; using XLabs.Ioc;
using System.Reflection;
using Bit.App.Resources;
namespace Bit.App namespace Bit.App
{ {
@ -26,6 +28,7 @@ namespace Bit.App
private readonly ISettings _settings; private readonly ISettings _settings;
private readonly ILockService _lockService; private readonly ILockService _lockService;
private readonly IGoogleAnalyticsService _googleAnalyticsService; private readonly IGoogleAnalyticsService _googleAnalyticsService;
private readonly ILocalizeService _localizeService;
public App( public App(
IAuthService authService, IAuthService authService,
@ -36,7 +39,8 @@ namespace Bit.App
IFingerprint fingerprint, IFingerprint fingerprint,
ISettings settings, ISettings settings,
ILockService lockService, ILockService lockService,
IGoogleAnalyticsService googleAnalyticsService) IGoogleAnalyticsService googleAnalyticsService,
ILocalizeService localizeService)
{ {
_databaseService = databaseService; _databaseService = databaseService;
_connectivity = connectivity; _connectivity = connectivity;
@ -47,7 +51,9 @@ namespace Bit.App
_settings = settings; _settings = settings;
_lockService = lockService; _lockService = lockService;
_googleAnalyticsService = googleAnalyticsService; _googleAnalyticsService = googleAnalyticsService;
_localizeService = localizeService;
SetCulture();
SetStyles(); SetStyles();
if(authService.IsAuthenticated) 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);
}
}
} }
} }

View file

@ -77,6 +77,7 @@
<Compile Include="Enums\CipherType.cs" /> <Compile Include="Enums\CipherType.cs" />
<Compile Include="Enums\PushType.cs" /> <Compile Include="Enums\PushType.cs" />
<Compile Include="Enums\ReturnType.cs" /> <Compile Include="Enums\ReturnType.cs" />
<Compile Include="Abstractions\Services\ILocalizeService.cs" />
<Compile Include="Models\Api\ApiError.cs" /> <Compile Include="Models\Api\ApiError.cs" />
<Compile Include="Models\Api\ApiResult.cs" /> <Compile Include="Models\Api\ApiResult.cs" />
<Compile Include="Models\Api\FolderDataModel.cs" /> <Compile Include="Models\Api\FolderDataModel.cs" />
@ -109,6 +110,7 @@
<Compile Include="Models\Page\SettingsFolderPageModel.cs" /> <Compile Include="Models\Page\SettingsFolderPageModel.cs" />
<Compile Include="Models\Page\PinPageModel.cs" /> <Compile Include="Models\Page\PinPageModel.cs" />
<Compile Include="Models\Page\PasswordGeneratorPageModel.cs" /> <Compile Include="Models\Page\PasswordGeneratorPageModel.cs" />
<Compile Include="Models\PlatformCulture.cs" />
<Compile Include="Models\PushNotification.cs" /> <Compile Include="Models\PushNotification.cs" />
<Compile Include="Models\Site.cs" /> <Compile Include="Models\Site.cs" />
<Compile Include="Models\Page\VaultViewSitePageModel.cs" /> <Compile Include="Models\Page\VaultViewSitePageModel.cs" />

View file

@ -0,0 +1,43 @@
using System;
namespace Bit.App.Models
{
/// <summary>
/// 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)
/// </summary>
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;
}
}
}

View file

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

View file

@ -92,6 +92,7 @@
<Compile Include="Services\CommonCryptoKeyDerivationService.cs" /> <Compile Include="Services\CommonCryptoKeyDerivationService.cs" />
<Compile Include="Services\Settings.cs" /> <Compile Include="Services\Settings.cs" />
<Compile Include="Services\GoogleAnalyticsService.cs" /> <Compile Include="Services\GoogleAnalyticsService.cs" />
<Compile Include="Services\LocalizeService.cs" />
<Compile Include="Services\SqlService.cs" /> <Compile Include="Services\SqlService.cs" />
<Compile Include="Utilities\Dialogs.cs" /> <Compile Include="Utilities\Dialogs.cs" />
<Compile Include="Views\ISelectable.cs" /> <Compile Include="Views\ISelectable.cs" />

View file

@ -38,6 +38,7 @@ namespace Bit.iOS.Extension
public override void ViewDidLoad() public override void ViewDidLoad()
{ {
SetIoc(); SetIoc();
SetCulture();
base.ViewDidLoad(); base.ViewDidLoad();
View.BackgroundColor = new UIColor(red: 0.94f, green: 0.94f, blue: 0.96f, alpha: 1.0f); 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<IAppIdService, AppIdService>(new ContainerControlledLifetimeManager()) .RegisterType<IAppIdService, AppIdService>(new ContainerControlledLifetimeManager())
.RegisterType<ILockService, LockService>(new ContainerControlledLifetimeManager()) .RegisterType<ILockService, LockService>(new ContainerControlledLifetimeManager())
.RegisterType<IGoogleAnalyticsService, GoogleAnalyticsService>(new ContainerControlledLifetimeManager()) .RegisterType<IGoogleAnalyticsService, GoogleAnalyticsService>(new ContainerControlledLifetimeManager())
.RegisterType<ILocalizeService, LocalizeService>(new ContainerControlledLifetimeManager())
// Repositories // Repositories
.RegisterType<IFolderRepository, FolderRepository>(new ContainerControlledLifetimeManager()) .RegisterType<IFolderRepository, FolderRepository>(new ContainerControlledLifetimeManager())
.RegisterType<IFolderApiRepository, FolderApiRepository>(new ContainerControlledLifetimeManager()) .RegisterType<IFolderApiRepository, FolderApiRepository>(new ContainerControlledLifetimeManager())
@ -292,6 +294,14 @@ namespace Bit.iOS.Extension
Resolver.ResetResolver(new UnityResolver(container)); Resolver.ResetResolver(new UnityResolver(container));
} }
private void SetCulture()
{
var localizeService = Resolver.Resolve<ILocalizeService>();
var ci = localizeService.GetCurrentCultureInfo();
AppResources.Culture = ci;
localizeService.SetLocale(ci);
}
private bool ProcessItemProvider(NSItemProvider itemProvider, string type, Action<NSDictionary> action) private bool ProcessItemProvider(NSItemProvider itemProvider, string type, Action<NSDictionary> action)
{ {
if(!itemProvider.HasItemConformingTo(type)) if(!itemProvider.HasItemConformingTo(type))

View file

@ -64,7 +64,8 @@ namespace Bit.iOS
Resolver.Resolve<IFingerprint>(), Resolver.Resolve<IFingerprint>(),
Resolver.Resolve<ISettings>(), Resolver.Resolve<ISettings>(),
Resolver.Resolve<ILockService>(), Resolver.Resolve<ILockService>(),
Resolver.Resolve<IGoogleAnalyticsService>())); Resolver.Resolve<IGoogleAnalyticsService>(),
Resolver.Resolve<ILocalizeService>()));
// Appearance stuff // Appearance stuff
@ -259,6 +260,8 @@ namespace Bit.iOS
.RegisterType<IAppInfoService, AppInfoService>(new ContainerControlledLifetimeManager()) .RegisterType<IAppInfoService, AppInfoService>(new ContainerControlledLifetimeManager())
.RegisterType<IGoogleAnalyticsService, GoogleAnalyticsService>(new ContainerControlledLifetimeManager()) .RegisterType<IGoogleAnalyticsService, GoogleAnalyticsService>(new ContainerControlledLifetimeManager())
.RegisterType<IDeviceInfoService, DeviceInfoService>(new ContainerControlledLifetimeManager()) .RegisterType<IDeviceInfoService, DeviceInfoService>(new ContainerControlledLifetimeManager())
.RegisterType<ILocalizeService, LocalizeService>(new ContainerControlledLifetimeManager())
// Repositories
// Repositories // Repositories
.RegisterType<IFolderRepository, FolderRepository>(new ContainerControlledLifetimeManager()) .RegisterType<IFolderRepository, FolderRepository>(new ContainerControlledLifetimeManager())
.RegisterType<IFolderApiRepository, FolderApiRepository>(new ContainerControlledLifetimeManager()) .RegisterType<IFolderApiRepository, FolderApiRepository>(new ContainerControlledLifetimeManager())