.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) | 英語文字列 |
以下のデータを入れています。
-- カテゴリ:Item
insert into localization_resource values('Item', 'Item01', '田中', 'Tanaka');
-- カテゴリ:Message
insert into localization_resource values('Message', 'M0001', 'ようこそ{0}さん!', 'Hello. {0}!');
insert into localization_resource values('Message', 'M0002', 'おはようございます。', 'Good Morning.');
実装
SQLの検索結果の1レコードを表すクラスを用意
namespace DbStringLocalizerSample.Localizer
{
/// <summary>
/// データベースで管理されている国際化リソースのレコード
/// </summary>
public class LocalizationRecord
{
public string Key { get; set; }
public string Ja { get; set; }
public string En { get; set; }
}
}
データベースから取得したレコードを保持するクラス
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
namespace DbStringLocalizerSample.Localizer
{
/// <summary>
/// データベースから取得した国際化リソースのソースを保持するクラス
/// </summary>
public class DbLocalizedStringSource
{
private readonly IDictionary<string, LocalizationRecord> _records;
public DbLocalizedStringSource(IDictionary<string, LocalizationRecord> records)
{
_records = records;
}
public static DbLocalizedStringSource FromEnumerable(IEnumerable<LocalizationRecord> src)
{
IDictionary<string, LocalizationRecord> records = src.ToDictionary(x => x.Key);
return new DbLocalizedStringSource(records);
}
public IEnumerable<string> GetAllKey()
{
return _records.Keys;
}
public string GetString(string name, CultureInfo currentUICulture)
{
if (_records.TryGetValue(name, out LocalizationRecord record))
{
switch (currentUICulture.Name)
{
case "ja": return record.Ja;
case "en": return record.En;
}
}
return null;
}
}
}
データベースからレコードを取得しDbLocalizedStringSource
を返すクラス
using Dapper;
using Npgsql;
using System;
using System.Collections.Generic;
using System.Data;
namespace DbStringLocalizerSample.Localizer
{
/// <summary>
/// データベースから国際化リソースのソースを取得するクラス
/// </summary>
public class DbLocalizedStringSourceProvider
{
private const string connectionString = "Host=localhost;Database=test_db;Username=test_user;Password=test_user";
public DbLocalizedStringSource GetLocalizedStrings(Type resourceSource)
{
using IDbConnection con = new NpgsqlConnection(connectionString);
con.Open();
using IDbTransaction tran = con.BeginTransaction();
string sql = @"
SELECT
key_name as Key
,ja as Ja
,en as En
FROM
localization_resource
WHERE
category = @category
ORDER BY
key
";
var param = new
{
category = resourceSource.Name
};
IEnumerable<LocalizationRecord> records = con.Query<LocalizationRecord>(sql, param, tran);
return DbLocalizedStringSource.FromEnumerable(records);
}
}
}
IStringLocalizer
の実装クラス
DbLocalizedStringSource
に委譲しています。
(ResourceManagerStringLocalizer
を参考)
using Microsoft.Extensions.Localization;
using System;
using System.Collections.Generic;
using System.Globalization;
namespace DbStringLocalizerSample.Localizer
{
/// <summary>
/// データベースを使用したIStringLocalizerの実装
/// </summary>
public class DbStringLocalizer : IStringLocalizer
{
private readonly DbLocalizedStringSource _dbLocalizedStringSource;
public DbStringLocalizer(DbLocalizedStringSource dbLocalizedStringSource)
{
_dbLocalizedStringSource = dbLocalizedStringSource;
}
/// <inheritdoc/>
public LocalizedString this[string name]
{
get
{
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
var value = GetString(name);
return new LocalizedString(name, value ?? name, resourceNotFound: value == null, searchedLocation: null);
}
}
/// <inheritdoc/>
public LocalizedString this[string name, params object[] arguments]
{
get
{
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
var format = GetString(name);
var value = string.Format(format ?? name, arguments);
return new LocalizedString(name, value, resourceNotFound: format == null, searchedLocation: null);
}
}
private string GetString(string name, CultureInfo culture = null)
{
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
var keyCulture = culture ?? CultureInfo.CurrentUICulture;
return _dbLocalizedStringSource.GetString(name, keyCulture);
}
public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures)
{
//includeParentCulturesを使ってない...
IEnumerable<string> allKey = _dbLocalizedStringSource.GetAllKey();
var culture = CultureInfo.CurrentUICulture;
foreach (var key in allKey)
{
var value = GetString(key, culture);
yield return new LocalizedString(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.")]
public IStringLocalizer WithCulture(CultureInfo culture)
{
throw new NotImplementedException("Not Implemented");
}
}
}
IStringLocalizerFactory
の実装クラス
DbStringLocalizer
の生成とキャッシュをしています。
(ResourceManagerStringLocalizerFactory
を参考)
using Microsoft.Extensions.Localization;
using System;
using System.Collections.Concurrent;
namespace DbStringLocalizerSample.Localizer
{
/// <summary>
/// DbStringLocalizerのファクトリ
/// </summary>
public class DbStringLocalizerFactory : IStringLocalizerFactory
{
private readonly ConcurrentDictionary<RuntimeTypeHandle, DbStringLocalizer> _localizerCache =
new ConcurrentDictionary<RuntimeTypeHandle, DbStringLocalizer>();
private readonly DbLocalizedStringSourceProvider _dbLocalizedStringSourceProvider;
public DbStringLocalizerFactory(DbLocalizedStringSourceProvider dbLocalizedStringSourceProvider)
{
_dbLocalizedStringSourceProvider = dbLocalizedStringSourceProvider;
}
/// <inheritdoc/>
public IStringLocalizer Create(string baseName, string location)
{
throw new NotImplementedException("Not Implemented");
}
/// <inheritdoc/>
public IStringLocalizer Create(Type resourceSource)
{
return _localizerCache.GetOrAdd(resourceSource.TypeHandle, _ => CreateDbStringLocalizer(resourceSource));
}
private DbStringLocalizer CreateDbStringLocalizer(Type resourceSource)
{
DbLocalizedStringSource source = _dbLocalizedStringSourceProvider.GetLocalizedStrings(resourceSource);
return new DbStringLocalizer(source);
}
}
}
Startup
で使用するクラスを登録する。
AddLocalization
より前にDbStringLocalizerFactory
をしておく
public void ConfigureServices(IServiceCollection services)
{
//...省略...
//AddLocalizationより前にDbStringLocalizerFactoryを登録する
services.AddTransient<DbLocalizedStringSourceProvider>();
services.AddSingleton<IStringLocalizerFactory, DbStringLocalizerFactory>();
services.AddLocalization();
}
使用方法
使用方法は.resx
を使うときと同じです。
まずカテゴリ用に2つのクラスを用意します。
namespace DbStringLocalizerSample.Dummy
{
public class Item
{}
}
namespace DbStringLocalizerSample.Dummy
{
public class Message
{}
}
IStringLocalizer<T>
をインジェクションするだけです。
[ApiController]
[Route("api/sandbox01")]
public class Sandbox01Controller : ControllerBase
{
private readonly IStringLocalizer<Item> _itemLocalizer;
private readonly IStringLocalizer<Message> _messageLocalizer;
public Sandbox01Controller(IStringLocalizer<Item> itemLocalizer, IStringLocalizer<Message> messageLocalizer)
{
_itemLocalizer = itemLocalizer;
_messageLocalizer = messageLocalizer;
}
[HttpGet("message01")]
public IActionResult Message01()
{
string item = _itemLocalizer["Item01"];
string mes = _messageLocalizer["M0001", item];
return Content(mes);
}
[HttpGet("message02")]
public IActionResult Message02()
{
string mes = _messageLocalizer["M0002"];
return Content(mes);
}
}
最後に
今回はデータベースを用いましたが、上記のポイントを押さえていれば、任意の方法を使って国際化対応ができそうです。
ただこの実装方法ではまだ少しだけ課題が残っています。
-
IStringLocalizer.WithCulture
メソッドが実装されていない -
IStringLocalizer.GetAllStrings(bool includeParentCultures)
メソッドでincludeParentCultures
が未使用 -
IStringLocalizer Create(string baseName, string location)
を実装していないのでIViewLocalizer
を使うことができない
これらを改善すればもう少し実用的なものになりそうです。