.NET Coreの国際化リソースにデータベースを使う
前回に関連した内容です。
.NET Coreの国際化対応はデフォルトでは.resxを使いますが、それ以外の方法も使える仕組みになっています。
今回はデータベースのテーブルに保存した内容を用いて国際化対応をします。
その前にデフォルト実装の確認
デフォルトではLocalizationServiceCollectionExtensionsのAddLocalizationメソッドを実行することで.resxファイルを用いた国際化が可能です。
国際化関係のクラスは以下のような階層になっています。
IStringLocalizerインターフェースIStringLocalizer<T>インターフェースStringLocalizer<T>クラス
ResourceManagerStringLocalizerクラス
IStringLocalizerFactoryインターフェースResourceManagerStringLocalizerFactoryクラス
StringLocalizer<T>はコンストラクタで IStringLocalizerFactoryを受け取っており、ファクトリが生成したIStringLocalizerのインスタンスをフィールドに保持しています。
そしてIStringLocalizerインスタンスに処理を委譲する仕組みになっています。
なのでResourceManagerStringLocalizer, ResourceManagerStringLocalizerFactoryに相当するクラスを用意することで、データベースから取得するローカライザを作ることができそうです。
本題
上記の内容を踏まえた上で データベースを用いて国際化対応をしてみます。ソースは以下の場所に配置しています。
データベースはPostgreSQL, データベースアクセスを簡略化するためにDapperを使っています。
また動作確認はWebアプリで行いました。
ただしビューにあたる部分は.cshtmlではなく生HTML(Vue.js)を使っています。
つまりサーバー側はWebAPIとして動かしています。
テーブルの用意
テーブルの形状(タテ持ち、ヨコ持ち)は任意ですが今回は以下のようにしました。
| 列名 | 型 | 主キー | 説明 |
|---|---|---|---|
| category | varchar(20) | 1 | メッセージの種類 |
| key_name | varchar(20) | 2 | メッセージのキー |
| ja | varchar(200) | 日本語文字列 | |
| en | varchar(200) | 英語文字列 |
以下のデータを入れています。
-- カテゴリ:Iteminsertintolocalization_resourcevalues('Item','Item01','田中','Tanaka');-- カテゴリ:Messageinsertintolocalization_resourcevalues('Message','M0001','ようこそ{0}さん!','Hello. {0}!');insertintolocalization_resourcevalues('Message','M0002','おはようございます。','Good Morning.');実装
SQLの検索結果の1レコードを表すクラスを用意
namespaceDbStringLocalizerSample.Localizer{/// <summary>/// データベースで管理されている国際化リソースのレコード/// </summary>publicclassLocalizationRecord{publicstringKey{get;set;}publicstringJa{get;set;}publicstringEn{get;set;}}}データベースから取得したレコードを保持するクラス
usingSystem.Collections.Generic;usingSystem.Globalization;usingSystem.Linq;namespaceDbStringLocalizerSample.Localizer{/// <summary>/// データベースから取得した国際化リソースのソースを保持するクラス/// </summary>publicclassDbLocalizedStringSource{privatereadonlyIDictionary<string,LocalizationRecord>_records;publicDbLocalizedStringSource(IDictionary<string,LocalizationRecord>records){_records=records;}publicstaticDbLocalizedStringSourceFromEnumerable(IEnumerable<LocalizationRecord>src){IDictionary<string,LocalizationRecord>records=src.ToDictionary(x=>x.Key);returnnewDbLocalizedStringSource(records);}publicIEnumerable<string>GetAllKey(){return_records.Keys;}publicstringGetString(stringname,CultureInfocurrentUICulture){if(_records.TryGetValue(name,outLocalizationRecordrecord)){switch(currentUICulture.Name){case"ja":returnrecord.Ja;case"en":returnrecord.En;}}returnnull;}}}データベースからレコードを取得しDbLocalizedStringSourceを返すクラス
usingDapper;usingNpgsql;usingSystem;usingSystem.Collections.Generic;usingSystem.Data;namespaceDbStringLocalizerSample.Localizer{/// <summary>/// データベースから国際化リソースのソースを取得するクラス/// </summary>publicclassDbLocalizedStringSourceProvider{privateconststringconnectionString="Host=localhost;Database=test_db;Username=test_user;Password=test_user";publicDbLocalizedStringSourceGetLocalizedStrings(TyperesourceSource){usingIDbConnectioncon=newNpgsqlConnection(connectionString);con.Open();usingIDbTransactiontran=con.BeginTransaction();stringsql=@"
SELECT
key_name as Key
,ja as Ja
,en as En
FROM
localization_resource
WHERE
category = @category
ORDER BY
key
";varparam=new{category=resourceSource.Name};IEnumerable<LocalizationRecord>records=con.Query<LocalizationRecord>(sql,param,tran);returnDbLocalizedStringSource.FromEnumerable(records);}}}IStringLocalizerの実装クラスDbLocalizedStringSourceに委譲しています。
(ResourceManagerStringLocalizerを参考)
usingMicrosoft.Extensions.Localization;usingSystem;usingSystem.Collections.Generic;usingSystem.Globalization;namespaceDbStringLocalizerSample.Localizer{/// <summary>/// データベースを使用したIStringLocalizerの実装/// </summary>publicclassDbStringLocalizer:IStringLocalizer{privatereadonlyDbLocalizedStringSource_dbLocalizedStringSource;publicDbStringLocalizer(DbLocalizedStringSourcedbLocalizedStringSource){_dbLocalizedStringSource=dbLocalizedStringSource;}/// <inheritdoc/>publicLocalizedStringthis[stringname]{get{if(name==null){thrownewArgumentNullException(nameof(name));}varvalue=GetString(name);returnnewLocalizedString(name,value??name,resourceNotFound:value==null,searchedLocation:null);}}/// <inheritdoc/>publicLocalizedStringthis[stringname,paramsobject[]arguments]{get{if(name==null){thrownewArgumentNullException(nameof(name));}varformat=GetString(name);varvalue=string.Format(format??name,arguments);returnnewLocalizedString(name,value,resourceNotFound:format==null,searchedLocation:null);}}privatestringGetString(stringname,CultureInfoculture=null){if(name==null){thrownewArgumentNullException(nameof(name));}varkeyCulture=culture??CultureInfo.CurrentUICulture;return_dbLocalizedStringSource.GetString(name,keyCulture);}publicIEnumerable<LocalizedString>GetAllStrings(boolincludeParentCultures){//includeParentCulturesを使ってない...IEnumerable<string>allKey=_dbLocalizedStringSource.GetAllKey();varculture=CultureInfo.CurrentUICulture;foreach(varkeyinallKey){varvalue=GetString(key,culture);yieldreturnnewLocalizedString(key,value??key,resourceNotFound:value==null,searchedLocation:null);}}/// <summary>/// インターフェースのこのメソッドがObsoleteなので実装していません。/// </summary>/// <param name="culture"></param>/// <returns></returns>[Obsolete("This method is obsolete. Use `CurrentCulture` and `CurrentUICulture` instead.")]publicIStringLocalizerWithCulture(CultureInfoculture){thrownewNotImplementedException("Not Implemented");}}}IStringLocalizerFactoryの実装クラスDbStringLocalizerの生成とキャッシュをしています。
(ResourceManagerStringLocalizerFactoryを参考)
usingMicrosoft.Extensions.Localization;usingSystem;usingSystem.Collections.Concurrent;namespaceDbStringLocalizerSample.Localizer{/// <summary>/// DbStringLocalizerのファクトリ/// </summary>publicclassDbStringLocalizerFactory:IStringLocalizerFactory{privatereadonlyConcurrentDictionary<RuntimeTypeHandle,DbStringLocalizer>_localizerCache=newConcurrentDictionary<RuntimeTypeHandle,DbStringLocalizer>();privatereadonlyDbLocalizedStringSourceProvider_dbLocalizedStringSourceProvider;publicDbStringLocalizerFactory(DbLocalizedStringSourceProviderdbLocalizedStringSourceProvider){_dbLocalizedStringSourceProvider=dbLocalizedStringSourceProvider;}/// <inheritdoc/>publicIStringLocalizerCreate(stringbaseName,stringlocation){thrownewNotImplementedException("Not Implemented");}/// <inheritdoc/>publicIStringLocalizerCreate(TyperesourceSource){return_localizerCache.GetOrAdd(resourceSource.TypeHandle,_=>CreateDbStringLocalizer(resourceSource));}privateDbStringLocalizerCreateDbStringLocalizer(TyperesourceSource){DbLocalizedStringSourcesource=_dbLocalizedStringSourceProvider.GetLocalizedStrings(resourceSource);returnnewDbStringLocalizer(source);}}}Startupで使用するクラスを登録する。AddLocalizationより前にDbStringLocalizerFactoryをしておく
publicvoidConfigureServices(IServiceCollectionservices){//...省略...//AddLocalizationより前にDbStringLocalizerFactoryを登録するservices.AddTransient<DbLocalizedStringSourceProvider>();services.AddSingleton<IStringLocalizerFactory,DbStringLocalizerFactory>();services.AddLocalization();}使用方法
使用方法は.resxを使うときと同じです。
まずカテゴリ用に2つのクラスを用意します。
namespaceDbStringLocalizerSample.Dummy{publicclassItem{}}namespaceDbStringLocalizerSample.Dummy{publicclassMessage{}}IStringLocalizer<T>をインジェクションするだけです。
[ApiController][Route("api/sandbox01")]publicclassSandbox01Controller:ControllerBase{privatereadonlyIStringLocalizer<Item>_itemLocalizer;privatereadonlyIStringLocalizer<Message>_messageLocalizer;publicSandbox01Controller(IStringLocalizer<Item>itemLocalizer,IStringLocalizer<Message>messageLocalizer){_itemLocalizer=itemLocalizer;_messageLocalizer=messageLocalizer;}[HttpGet("message01")]publicIActionResultMessage01(){stringitem=_itemLocalizer["Item01"];stringmes=_messageLocalizer["M0001",item];returnContent(mes);}[HttpGet("message02")]publicIActionResultMessage02(){stringmes=_messageLocalizer["M0002"];returnContent(mes);}}最後に
今回はデータベースを用いましたが、上記のポイントを押さえていれば、任意の方法を使って国際化対応ができそうです。
ただこの実装方法ではまだ少しだけ課題が残っています。
IStringLocalizer.WithCultureメソッドが実装されていないIStringLocalizer.GetAllStrings(bool includeParentCultures)メソッドでincludeParentCulturesが未使用IStringLocalizer Create(string baseName, string location)を実装していないのでIViewLocalizerを使うことができない
これらを改善すればもう少し実用的なものになりそうです。