1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

.NET Coreの国際化リソースにデータベースを使う

Posted at

.NET Coreの国際化リソースにデータベースを使う

前回に関連した内容です。

.NET Coreの国際化対応はデフォルトでは.resxを使いますが、それ以外の方法も使える仕組みになっています。

今回はデータベースのテーブルに保存した内容を用いて国際化対応をします。

その前にデフォルト実装の確認

デフォルトではLocalizationServiceCollectionExtensionsAddLocalizationメソッドを実行することで.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レコードを表すクラスを用意

LocalizationRecord.cs
namespace DbStringLocalizerSample.Localizer
{
    /// <summary>
    /// データベースで管理されている国際化リソースのレコード
    /// </summary>
    public class LocalizationRecord
    {
        public string Key { get; set; }

        public string Ja { get; set; }

        public string En { get; set; }
    }
}

データベースから取得したレコードを保持するクラス

DbLocalizedStringSource.cs
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 を返すクラス

DbLocalizedStringSourceProvider.cs
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 を参考)

DbStringLocalizer.cs
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を参考)

DbStringLocalizerFactory.cs
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をしておく

Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    //...省略...
    
    //AddLocalizationより前にDbStringLocalizerFactoryを登録する
    services.AddTransient<DbLocalizedStringSourceProvider>();
    services.AddSingleton<IStringLocalizerFactory, DbStringLocalizerFactory>();
    services.AddLocalization();
}

使用方法

使用方法は.resx を使うときと同じです。

まずカテゴリ用に2つのクラスを用意します。

Item.cs
namespace DbStringLocalizerSample.Dummy
{
    public class Item
    {}
}
Message.cs
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を使うことができない

これらを改善すればもう少し実用的なものになりそうです。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?