Vue.js vue-18n ASP.NET Core
▌Introduction
We will
use the following stack for localization:
1. ASP.NET Core resource files
2. Vue-i18n
This
article shows how to localize with resource files in ASP.NET Core Web API
project.
▌Environment
▋.NET Core 2.2.104
▋Visual Studio 2017 Community
▌Implement
▋Use the resource files in the same project
When we define the ResourcesPath to "Resources"
as following in Startup.cs,
public void ConfigureServices(IServiceCollection services)
{
services.AddLocalization(options => options.ResourcesPath = "Resources");
};
the default structure should be related to the
namespace of Controllers (or Views):
./Resources/Controllers.HomeController.en-US.resx
Or
./Resources/Controllers/HomeController.en-US.resx
For example,
Enable the global localization on every requests in Startup.Configure method,
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseRequestLocalization();
}
Thus we can use the injected localizer and use it
like this,
[ApiController]
public partial class HomeController : ControllerBase
{
private readonly IStringLocalizer<HomeController> _localizer;
public LocaleController(
IStringLocalizer<HomeController>
localizer)
{
this._localizer = localizer;
var localizedStr = this._localizer["Name"];
}
}
However
in most case we would like to put the resource files in other project…
▋Use the resource files in other/original project
First we
have to use a non-specified ResourcesPath in Starup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddLocalization();
};
And
create a new empty class at other/current project to make ASP.NET Core can
catch the namespace and its own resource files.
For
example, create a ShareResource.cs
and ShareResource.<lang>.resx
namespace My.Other.Project.Resources
{
public class ShareResource
{
}
}
Then we can
inject the localizer with dictionaries from the above resource files as
following in the target ASP.NET Core project,
using My.Other.Project.Resources;
[ApiController]
public partial class LocaleController : ControllerBase
{
private readonly IStringLocalizer _shareLocalizer;
public HomeController(
IStringLocalizer<ShareResource>
shareLocalizer)
{
this._shareLocalizer = shareLocalizer;
}
}
▋Create a Localization API
Let’s
create the localization API which supports localization route.
More
specific, a url like “http://localhost/api/Locale/Get/en-US” will make the request’s culture as
“en-US”. And a url “http://localhost/api/Locale/Get/unknown” shall fallback it to the default
culture, such as “zh-TW” in this tutorial.
▋Startup.cs
public class Startup {
public Startup (IConfiguration configuration) {
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices (IServiceCollection services) {
services.AddLocalization();
// …
}
public void Configure (IApplicationBuilder app) {
// Localization
app.UseRequestLocalization();
// …
app.UseMvc ();
}
}
var supportedCultures = new[]
{
new CultureInfo("zh-TW"),
new CultureInfo("zh-CN"),
new CultureInfo("en-US"),
};
app.UseRequestLocalization (new RequestLocalizationOptions {
DefaultRequestCulture = new RequestCulture ("zh-TW"),
// Formatting numbers, dates, etc.
SupportedCultures = supportedCultures,
// UI strings that we have localized.
SupportedUICultures = supportedCultures
});
Notice
the DefaultRequestCulture will
be used for requests when a supported culture could not be determined by Microsoft.AspNetCore.Localization.IRequestCultureProviders.
And the default
value will be set to both:
Furthermore,
we will apply the localization configuration to ONLY one API in the next steps,
so it’s suggested to create a middleware class and only apply it to the target
Controller or Actions.
▋Localization Middleware
public class LocalizationMiddleware
{
private readonly string defaultCulture = "zh-TW";
public static CultureInfo[] SupportedCultures = new[]{
new CultureInfo("zh-TW"),
new CultureInfo("zh-CN"),
new CultureInfo("en-US"),
};
public void Configure(IApplicationBuilder app)
{
app.UseRequestLocalization(new RequestLocalizationOptions
{
DefaultRequestCulture = new RequestCulture(defaultCulture),
// Formatting
numbers, dates, etc.
SupportedCultures = SupportedCultures,
//
UI strings that we have localized.
SupportedUICultures = SupportedCultures
});
}
}
Don’t forget
to remove the global localization pipeline in Startup.Configure method,
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// app.UseRequestLocalization();
}
▋LocalizedString extension
Lets
create an Extension for LocalizedString to generate JSON string from IEnumerable<LocalizedString>.
public static class LocalizedStringExtension
{
public static async Task<string> ToJsonStringAsync(this IEnumerable<LocalizedString> source, bool isCamelLowerCaseForKey, string prefixKey="")
{
string json = string.Empty;
var dicsTask = localizedStrsToDictionary(source);
JsonSerializerSettings camelCaseFormatter = null;
if (isCamelLowerCaseForKey)
{
camelCaseFormatter = new JsonSerializerSettings();
camelCaseFormatter.ContractResolver = new CamelCasePropertyNamesContractResolver();
}
var dics = await dicsTask;
json = camelCaseFormatter == null ?
JsonConvert.SerializeObject(dics) :
JsonConvert.SerializeObject(dics, camelCaseFormatter);
if (!string.IsNullOrEmpty(prefixKey))
{
json = string.Concat("{\"",prefixKey,"\":", json, "}");
}
return json;
}
}
▋ApiController
We will
set a route parameter to determine which language (resource file) to use.
[ApiController]
public partial class LocaleController : ControllerBase
{
private readonly IStringLocalizer _shareLocalizer;
public LocaleController(
IStringLocalizer<ShareResource> shareLocalizer)
{
this._shareLocalizer = shareLocalizer;
}
[MiddlewareFilter(typeof(LocalizationMiddleware))]
[Route("Get/{locale}")]
[HttpGet]
public async Task<string> Get([FromRoute] string locale)
{
IEnumerable<LocalizedString>
localizedStrs = null;
Thread.CurrentThread.CurrentCulture = cultureInfo;
localizedStrs = this._shareLocalizer.GetAllStrings(includeParentCultures: true);
return await localizedStrs.ToJsonStringAsync(isCamelLowerCaseForKey: true);
}
}
Notice that if we don’t set the
value to Thread.CurrentThread.CurrentCulture/
CurrentUICulture, the default culture will be set the value of DefaultRequestCulture.
|
We can
rewrite the above codes by creating a custom
localizer,
var cultureInfo = new CultureInfo(locale);
var customLocalizer = this._shareLocalizer.WithCulture(cultureInfo);
localizedStrs = customLocalizer.GetAllStrings(includeParentCultures: true);
※Result
However
if we set a non-supported* culture (CultureInfo) to the localizer or System.CurrentThread.CurrentUICulture/CurrentCulture,
we will get Server Error when getting the localized string(s) as following,
PS.
Non-supported means we don’t create the mapping resource file for it.
Why not the localizer just returns the localized strings with DefaultRequestCulture when no resource files found… lol
Anyway
we have to deal with this case by checking the supported cultures(languages).
Update
the API as this,
[MiddlewareFilter(typeof(LocalizationMiddleware))]
[Route("Get/{locale}")]
[HttpGet]
public async Task<string> Get([FromRoute] string locale)
{
IEnumerable<LocalizedString>
localizedStrs = null;
IStringLocalizer customLocalizer = null;
if (LocalizationMiddleware.SupportedCultures.Any(x => x.Name.Equals(locale)))
{
var cultureInfo = new CultureInfo(locale);
customLocalizer = this._shareLocalizer.WithCulture(cultureInfo);
localizedStrs = customLocalizer.GetAllStrings(includeParentCultures: true);
}
else
{
localizedStrs = this._shareLocalizer.GetAllStrings(includeParentCultures: true);
}
return await localizedStrs.ToJsonStringAsync(isCamelLowerCaseForKey: true);
}
Then
the non-supported culture will fall back to DefaultRequestCulture now!
▋(Optional) Custom localization provider
RequestLocalizationOptions
provides three default culture providers that are automatically configured. (List in order, the first matched one will
determine the request’s culture)
We
can create a custom localization provider and put it into RequestLocalizations.
RequestCultureProviders with new orders as following in Startup.ConfigureServices:
Take
the following codes for example, we set MyCustomCultureProvider as
the first-priority rule:
services.Configure<RequestLocalizationOptions>(options
=>
{
//...skip
options.RequestCultureProviders.Insert(0, new MyCustomCultureProvider());
});
services.AddLocalization();
Now
we can create a custom provider: RouteCultureProvider,
to determine the request’s culture from the route.
This
new custom RequestCultureProvider won’t change the user’s behavior,
i.e. request url.
However,
it will replace the logic for culture-determination inside the API.
|
▋RouteCultureProvider
public class
RouteCultureProvider : RequestCultureProvider
{
private const string
defaultCulture = "zh-TW";
public override Task<ProviderCultureResult>
DetermineProviderCultureResult(HttpContext httpContext)
{
object finalCulture
= string.Empty;
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
try
{
using (var
routeMatcher = new RouteMatcher())
{
PathString path =
httpContext.Request.Path;
var template = "api/Locale/Get/{locale}";
var routeValues
= routeMatcher.Matches(template, path.Value);
routeValues.TryGetValue("locale", out
finalCulture);
}
}
catch (Exception)
{ finalCulture
= defaultCulture; }
finally
{ finalCulture
= finalCulture ?? defaultCulture; }
return
Task.FromResult(new ProviderCultureResult(finalCulture
as string));
}
Here
I created RouteMatcher to parse
the request’s url and get the value of “locale”.
▋RouteMatcher
public class RouteMatcher : IDisposable
{
/// Match
the request path with route template
public RouteValueDictionary Matches(string
routeTemplate, string requestPath)
{
var template =
TemplateParser.Parse(routeTemplate);
var matcher = new TemplateMatcher(template,
getDefaults(template));
var values = new RouteValueDictionary();
var moduleMatch
= matcher.TryMatch(requestPath, values);
return values;
}
// Extracts
the default argument values from the template
private RouteValueDictionary
getDefaults(RouteTemplate parsedTemplate)
{
var result = new RouteValueDictionary();
foreach (var parameter in
parsedTemplate.Parameters)
{
if
(parameter.DefaultValue != null)
{
result.Add(parameter.Name, parameter.DefaultValue);
}
}
return result;
}
}
▋Startup.cs
Update
Startup.ConfigureServices
as following.
Notice
that I clear all default culture providers and set RouteCultureProvider
as the only one.
public void
ConfigureServices(IServiceCollection services)
{
var
supportedCultures = new CultureInfo[] {
new CultureInfo("zh-TW"),
new CultureInfo("zh-CN"),
new CultureInfo("en-US"),
};
services.Configure<RequestLocalizationOptions>(options
=>
{
options.DefaultRequestCulture = new RequestCulture("zh-TW");
options.SupportedCultures =
supportedCultures;
options.SupportedUICultures
= supportedCultures;
options.RequestCultureProviders.Clear();
options.RequestCultureProviders.Add(new RouteCultureProvider());
});
services.AddLocalization();
}
Enable
the global localization pipeline in Startup.Configure,
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseRequestLocalization();
}
Then
we can simply the API with the custom culture provider.
▋ApiController
[Route("Get/{locale}")]
[HttpGet]
public async Task<string> Get([FromRoute] string locale)
{
var
localizedStrs = this._localizer.GetAllStrings(includeParentCultures:
true);
return await
localizedStrs.ToJsonStringAsync(isCamelLowerCaseForKey: true);
}
▋(Optional) Custom localization provider: Final version
To
optimize the codes, we can move the RequestLocalizationOptions
configuration from Startup.ConfigureServices to the Middleware
class: LocalizationMiddleware.
▋LocalizationMiddleware.cs
public class
LocalizationMiddleware
{
private readonly string
defaultCulture = "zh-TW";
public static CultureInfo[]
SupportedCultures = new[]{
new CultureInfo("zh-TW"),
new CultureInfo("zh-CN"),
new CultureInfo("en-US"),
};
public void Configure(IApplicationBuilder app)
{
var options = new RequestLocalizationOptions
{
DefaultRequestCulture = new RequestCulture(defaultCulture),
//
Formatting numbers, dates, etc.
SupportedCultures = SupportedCultures,
// UI
strings that we have localized.
SupportedUICultures =
SupportedCultures
//Clear
default providers
};
options.RequestCultureProviders.Clear();
options.RequestCultureProviders.Add(new RouteCultureProvider());
app.UseRequestLocalization(options);
}
}
And
remove the global localization pipeline and update ApiController,
▋Startup.cs
public void
ConfigureServices(IServiceCollection services)
{
services.AddLocalization();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
//This line
is no longer needed cus we use the LocalizationMiddleware
//app.UseRequestLocalization();
}
▋ApiController
[Route("Get/{locale}")]
[HttpGet]
[MiddlewareFilter(typeof(LocalizationMiddleware))]
public async Task<string> Get([FromRoute] string locale)
{
var localizedStrs
= this._localizer.GetAllStrings(includeParentCultures:
true);
return await
localizedStrs.ToJsonStringAsync(isCamelLowerCaseForKey: true);
}
※Result(GIF)
▌Reference
沒有留言:
張貼留言