0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

プリザンターの多言語対応の実装を詳しくみてみる[第4回:言語解決メカニズム編]

0
Posted at

はじめに

前回(第 3 回)では、CodeDefiner による多言語コードの自動生成の仕組みを見てきました。第 4 回では、ユーザーの言語がどのように決定され、表示テキストの取得にどうつながるのかを詳しくみていきます。

回 テーマ
第 1 回 多言語アーキテクチャ概要編 — 全体設計と構成要素
第 2 回 Display 定義と DisplayAccessor 編 — JSON 定義ファイルの構造とアクセサクラス
第 3 回 CodeDefiner によるコード生成編 — テンプレートから C# / TypeScript を自動生成する仕組み
第 4 回(本記事) 言語解決メカニズム編 — ユーザー言語の決定ロジックとフォールバック
第 5 回 クライアントサイド多言語対応編 — TypeScript での言語切り替えと $p.display()

バージョン 1.5.1.0 を対象にしています

Context.Language プロパティ

プリザンターのリクエスト処理では、すべてのコンテキスト情報が Context クラスに保持されます。ユーザーの言語もその一部で、Language プロパティとして定義されています。

Implem.Pleasanter/Libraries/Requests/Context.cs
public string Language { get; set; } = Parameters.Service.DefaultLanguage;

デフォルト値は Parameters.Service.DefaultLanguage(Service.json で設定)です。この値はリクエスト処理の過程で、ユーザーの言語設定に基づいて上書きされます。

3 つの言語決定パターン

Context の初期化時、ユーザーの認証状態に応じて 3 つのパターンで言語が決定されます。

パターン 1: 認証済みユーザー(セッション認証 / API キー認証)

ログイン済みのユーザーや API キーで認証されたユーザーの場合、SetUser() メソッドでユーザーモデルから言語を取得します。

Implem.Pleasanter/Libraries/Requests/Context.cs
private void SetUser(UserModel userModel, bool noHttpContext = false)
{
    if (userModel.AccessStatus == Databases.AccessStatuses.Selected)
    {
        // ... 他のユーザープロパティの設定 ...
        Language = userModel.Language;   // DB の Users.Language カラム
        // ...
    }
}

Users テーブルの Language カラムに保存されたユーザーの言語設定がそのまま Context.Language に設定されます。

パターン 2: 未認証ユーザー

未認証のユーザー(ログイン画面など)の場合は SessionLanguage() メソッドで言語を決定します。

Implem.Pleasanter/Libraries/Requests/Context.cs
private void SetUserProperties(bool sessionStatus, bool setData)
{
    if (HasRoute)
    {
        if (setData) SetData();
        var jsonDeserializedRequestApi = RequestDataString
            .Deserialize<Api>();
        if (jsonDeserializedRequestApi?.ApiKey.IsNullOrEmpty() == false)
        {
            ApiKey = jsonDeserializedRequestApi.ApiKey;
            SetUser(userModel: GetUser(...));
        }
        else if (!LoginId.IsNullOrEmpty())
        {
            SetUser(userModel: GetUser(...));
        }
        else
        {
            if (sessionStatus)
                Language = SessionLanguage();   // 未認証時のフォールバック
        }
    }
}

SessionLanguage() の多段フォールバック

未認証ユーザーの言語決定は SessionLanguage() メソッドで行われます。このメソッドは 4 段階のフォールバックを持っています。

Implem.Pleasanter/Libraries/Requests/Context.cs
private string SessionLanguage()
{
    var types = Def.ColumnTable.Users_Language.ChoicesText
        .SplitReturn()
        .Select(o => o.Split_1st())
        .ToList();

    var language = string.Empty;

    if (HasRoute)
    {
        // 1. クエリ文字列のチェック
        language = QueryStrings.Data("Language");
        if (!language.IsNullOrEmpty())
        {
            SessionUtilities.Set(
                context: this,
                key: "Language",
                value: language);
        }
        else
        {
            // 2. Accept-Language ヘッダーの解析
            var lang = HttpAcceptLanguage()?.Split_1st('-');
            switch (lang)
            {
                case "en":
                case "zh":
                case "ja":
                case "de":
                case "ko":
                case "es":
                    language = lang;
                    break;
                case "vi":
                    language = "vn";   // vi → vn の変換
                    break;
                default:
                    // 3. システムデフォルト
                    language = Parameters.Service?.DefaultLanguage;
                    break;
            }
        }
        // 4. セッションに保存された言語
        language = SessionData.Get("Language") ?? language;
    }

    return types.Contains(language)
        ? language
        : Parameters.Service?.DefaultLanguage;
}

この処理の流れを整理すると以下のようになります。

フォールバックの優先順位

優先度 取得元 説明
1(最高) クエリ文字列 ?Language=ja URL パラメータで明示的に指定。セッションにも保存される
2 セッション保存値 過去のリクエストでセッションに保存された言語
3 Accept-Language ヘッダー ブラウザの言語設定から取得
4(最低) DefaultLanguage Service.json のシステムデフォルト

優先度 2 のセッション保存値は、コード上では Accept-Language ヘッダーの後に評価されていますが、SessionData.Get("Language") ?? language によって、セッションに値があればそちらが優先されます。一度クエリ文字列で言語を設定すると、以降のリクエストではセッション経由でその言語が維持されます。

Accept-Language ヘッダーの解析

ブラウザの Accept-Language ヘッダーは ja-JP,ja;q=0.9,en-US;q=0.8 のような形式です。プリザンターは先頭の言語コードを抽出し、ハイフン前の部分(ja)を使用します。

var lang = HttpAcceptLanguage()?.Split_1st('-');
// "ja-JP,ja;q=0.9" → "ja"

ベトナム語については特別な変換が行われます。HTTP 標準の言語コードは vi ですが、プリザンター内部では vn として扱われるため、case "vi": language = "vn"; という変換が入っています。

バリデーション

最後に、取得した言語コードが Users_Language の ChoicesText に含まれているかチェックし、無効な値の場合は DefaultLanguage にフォールバックします。

return types.Contains(language)
    ? language
    : Parameters.Service?.DefaultLanguage;

Service.json のデフォルト言語設定

フォールバックの最終段として使われる DefaultLanguage は Service.json で設定されます。

App_Data/Parameters/Service.json
{
    "Name": "Implem.Pleasanter",
    "DefaultLanguage": "en",
    "TimeZoneDefault": "UTC"
}

DefaultLanguage はサーバーのシステムデフォルト言語です。ユーザーの初期言語(新規ユーザー作成時のデフォルト値)は Users_Language カラムの Default フィールド("ja")で別途設定されています。

ユーザーの言語設定の保存先

ユーザーが言語を変更すると、Users テーブルの Language カラムに保存されます。

App_Data/Definitions/Definition_Column/Users_Language.json
{
    "Id": "Users_Language",
    "ColumnName": "Language",
    "TypeName": "nvarchar",
    "MaxLength": "32",
    "Default": "ja",
    "Required": "1",
    "ChoicesText": "en,English\nzh,Chinese\nja,Japanese\nde,German\nko,Korean\nes,Spanish\nvn,Vietnamese"
}
プロパティ 値 説明
TypeName nvarchar Unicode 文字列型
MaxLength 32 最大 32 文字
Default ja 新規ユーザーの初期値は日本語
Required 1 必須フィールド

Displays.Get() による表示テキスト取得

Context.Language が決定されると、Displays.Get() メソッドで表示テキストを取得します。

Implem.Pleasanter/Libraries/Responses/Displays.cs
public static string Get(
    Context context, string id, params string[] data)
{
    return Get(
        id: id,
        language: context.Language,
        data: data);
}

public static string Get(
    string id, string language, params string[] data)
{
    var screen = id;
    var key = id + "_" + language;
    if (DisplayHash.ContainsKey(key))
        screen = DisplayHash[key];       // 言語固有キーを優先
    else if (DisplayHash.ContainsKey(id))
        screen = DisplayHash[id];        // なければデフォルト
    return data?.Any() == true
        ? screen.Params(data)
        : screen;
}

検索フローの図解

Displays.Get(context, "Confirm") を context.Language = "ja" で呼び出した場合の流れです。

パラメータ付きメッセージ

Displays.Get() は data パラメータを受け取り、{0} / {1} のようなプレースホルダを置換できます。

// Display 定義: "'{0}' has been saved."
// 日本語: "'{0}' を保存しました。"
var message = Displays.Get(
    context,
    "Updated",
    data: new[] { recordTitle });
// → "'顧客A' を保存しました。"

Display 拡張メソッド

List<DisplayElement> に対する拡張メソッドも用意されています。

Implem.Pleasanter/Libraries/Responses/Displays.cs
public static string Display(
    this List<DisplayElement> languages, Context context)
{
    return languages?
        .FirstOrDefault(o => o.Language == context.Language)?.Body
        ?? languages?
            .FirstOrDefault(o => o.Language.IsNullOrEmpty())?.Body
        ?? languages?.FirstOrDefault()?.Body;
}

この拡張メソッドは以下の 3 段階でフォールバックします。

段階 検索条件 例(context.Language = "ko")
1 Language == context.Language Language: "ko" の要素
2 Language が null/空 デフォルト(英語)の要素
3 先頭要素 Languages[0]

言語決定から表示までの全体フロー

最後に、HTTP リクエストから表示テキスト取得までの全体フローを図示します。

まとめ

本記事では、プリザンターの言語解決メカニズムを見てきました。

  • Context.Language のデフォルト値は Parameters.Service.DefaultLanguage
  • 認証済みユーザーは Users.Language カラムの値で上書きされる
  • 未認証ユーザーは SessionLanguage() の多段フォールバック(クエリ文字列 → セッション → Accept-Language → デフォルト)で決定される
  • ベトナム語は HTTP 標準の vi からプリザンター内部の vn に変換される
  • Displays.Get() は DisplayId_language → DisplayId の 2 段階検索でテキストを取得する

次回は、クライアントサイド(TypeScript)での多言語対応の仕組みを見ていきます。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?