Skip to content

多言語対応の実装 ​

第5版作成 最終更新 (日本時間)
確認バージョン1.5.1.01.5.8.1

プリザンターは 7 言語(英語・日本語・中国語・ドイツ語・韓国語・スペイン語・ベトナム語)をサポートしています。このページでは、表示文字列がどこで定義され、どうコード生成され、リクエストごとにどの言語が選ばれて表示されるのかを、ソースコードに沿ってまとめます。

結論: 表示文字列は App_Data/Displays/*.json(約 1,255 ファイル)で一元管理され、CodeDefiner がサーバー用の Displays.cs とクライアント用の display.ts を生成します。実行時は Context.Language で決まった言語で {DisplayId}_{Language} → {DisplayId} の順に検索されます。

アーキテクチャ概要 ​

多言語対応は 3 つの層で構成されています。

層構成要素役割
定義層(ビルド前)Display JSON(App_Data/Displays/*.json、約 1,255 ファイル)UI に表示する文字列を 7 言語分定義
カラム定義(App_Data/Definitions/Definition_Column/*.json)テーブルカラムのラベルを多言語定義
コード定義テンプレート(App_Data/Definitions/Definition_Code/*.json + *.txt)CodeDefiner が生成するコードのテンプレート
生成層(CodeDefiner)CodeDefiner定義層を読み、Libraries/Responses/Displays.cs(約 28,000 行)や display.ts を自動生成
実行層(ランタイム)Initializer、Context.Language、Displays.Get()、$p.display()起動時に DisplayHash を構築し、リクエストごとに言語を解決して表示テキストを返す

データの流れは次のとおりです。

図を読み込み中…

対応言語 ​

言語コード言語名言語名(現地語)
en英語English
ja日本語日本語
zh中国語中文
deドイツ語Deutsch
ko韓国語한국어
esスペイン語Español
vnベトナム語Tiếng Việt

この一覧は Users_Language カラムの ChoicesText で定義されています。

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"
}

WARNING

ベトナム語のコードは HTTP の Accept-Language ヘッダーでは vi ですが、プリザンター内部では vn として扱われます。変換は Context.SessionLanguage() で行われています。

定義層:Display JSON ​

配置と構造 ​

App_Data/Displays/ に、1 ファイル 1 表示文字列で約 1,255 ファイルがあります(1.5.1.0 時点。1.5.8.1 では 1,353 ファイル)。ファイル名は {DisplayId}.json で、DisplayId がそのまま識別子です。

text
Implem.Pleasanter/App_Data/Displays/
├── AccessControls.json
├── Add.json
├── Confirm.json
├── Delete.json
├── Normal.json
├── Ym.json
└── ... (約 1,255 ファイル)
フィールド型必須説明
IdstringYes表示文字列の一意な識別子
TypeintYesメッセージ種別(Displays.Types 列挙型に対応)
ClientScriptbool?Notrue の場合、クライアントサイドにも公開される
LanguagesDisplayElement[]Yes各言語のテキスト定義
json
{
    "Id": "Confirm",
    "Type": 310,
    "Languages": [
        { "Body": "Confirm" },
        { "Language": "zh", "Body": "确认" },
        { "Language": "ja", "Body": "確認" },
        { "Language": "de", "Body": "Bestätigen" },
        { "Language": "ko", "Body": "확인" },
        { "Language": "es", "Body": "Confirmar" },
        { "Language": "vn", "Body": "Xác nhận" }
    ]
}

Languages 配列のルール ​

  1. 先頭要素にはデフォルト(英語)を置く: Language プロパティを持たない要素がフォールバック値になります。
  2. 以降の要素には Language を指定する: "ja"、"zh" などの言語コードを設定します。
  3. すべての言語をそろえる必要はない: 未定義の言語ではデフォルト値が使われます。

DisplayElement は Body 以外に LabelText、Description、InputGuide といった拡張フィールドも持てます。

json
{
    "Id": "DisplayName",
    "Type": 110,
    "ClientScript": true,
    "Languages": [
        {
            "Body": "Name displayed",
            "Description": "The name displayed on the screen"
        },
        {
            "Language": "ja",
            "Body": "表示名",
            "Description": "画面上に表示される名前"
        }
    ]
}

Display Type(メッセージ種別) ​

値名称用途CSS クラス
110Normal通常のラベル・テキスト—
120Date日付関連テキスト—
130DateFormat日付フォーマット—
210Success成功メッセージalert-success
220Information情報メッセージalert-information
230Warning警告メッセージalert-warning
240Errorエラーメッセージalert-error
310Confirmation確認ダイアログalert-confirm
410Validationバリデーション—

カラム定義の多言語ラベル ​

テーブルカラムのラベルは Definition_Column/*.json で定義されます。LabelText が日本語(主キー)で、LabelText_en / LabelText_zh / LabelText_de / LabelText_ko / LabelText_es / LabelText_vn が各言語のラベルです。

json
{
    "Id": "Users_Language",
    "ModelName": "User",
    "TableName": "Users",
    "ColumnName": "Language",
    "LabelText": "言語",
    "LabelText_en": "Language",
    "LabelText_zh": "语言",
    "LabelText_de": "Sprache",
    "LabelText_ko": "언어",
    "LabelText_es": "Idioma",
    "LabelText_vn": "Ngôn ngữ"
}

DisplayAccessor と DisplayHash の構築 ​

DisplayAccessor クラス群 ​

Display JSON をメモリ上に保持するのは Implem.DisplayAccessor プロジェクトのクラス群です。

クラスファイル内容
DisplayElementImplem.DisplayAccessor/DisplayElement.cs1 言語バリアント(Language(null = デフォルト/英語)、Body、LabelText、Description、InputGuide)
DisplayImplem.DisplayAccessor/Display.cs1 表示文字列と全言語バリアント(Id、Type、ClientScript、Languages)
DisplaysImplem.DisplayAccessor/Displays.csTypes 列挙型と DisplayHash(Dictionary<string, Display>)
csharp
public static class Displays
{
    public enum Types : int
    {
        Normal = 110,
        Date = 120,
        DateFormat = 130,
        Success = 210,
        Information = 220,
        Warning = 230,
        Error = 240,
        Confirmation = 310,
        Validation = 410
    }

    public static Dictionary<string, Display> DisplayHash;

    public static string Get(string id)
    {
        return DisplayHash[id].Languages.FirstOrDefault().Body;
    }
}

DisplayAccessor 版の Get() は Languages の先頭要素(デフォルト/英語)を返すだけです。

起動時の読み込み ​

起動時に Initializer.SetDefinitions() が呼ばれ、App_Data/Displays/ の全 JSON を Display にデシリアライズし、Id をキーに DisplayHash へ追加します。

csharp
public static void SetDefinitions()
{
    Displays.DisplayHash = DisplayHash();  // JSON ファイルをロード
    // ... 他の定義の初期化 ...
    SetDisplayAccessor();                  // カラム定義を統合
}

private static Dictionary<string, Display> DisplayHash()
{
    var hash = new Dictionary<string, Display>();
    new DirectoryInfo(Directories.Displays())
        .GetFiles("*.json")
        .ForEach(file =>
        {
            var data = Files.Read(file.FullName)
                .Deserialize<Display>();
            hash.Add(data.Id, data);
        });
    return hash;
}

初期化後の DisplayHash(DisplayAccessor 側)の構造は次のようになります。

図を読み込み中…

続く SetDisplayAccessor() で、カラム定義(Base でないもの)の LabelText_* を Display に変換して DisplayHash に統合します。このとき .Where(o => !Displays.DisplayHash.ContainsKey(o.Id)) により、Display JSON に同じキーがあればカラム定義では上書きしません(JSON 定義が優先)。

2 つの Displays クラス ​

Displays という名前のクラスが 2 つあるので注意が必要です。

クラス名前空間役割キーの形式
DisplayAccessor.DisplaysImplem.DisplayAccessorデータモデル。DisplayHash を保持DisplayId → Display オブジェクト
DisplaysImplem.Pleasanter.Libraries.Responses言語対応の表示テキスト取得DisplayId_language → テキスト文字列

Implem.Pleasanter 側は、DisplayAccessor.Displays.DisplayHash をフラット化して独自の DisplayHash(Dictionary<string, string>)を作ります。

図を読み込み中…

DisplayAccessor 側Implem.Pleasanter 側のキー値
Confirm → Languages[0]Confirm"Confirm"
Confirm → Languages[1]Confirm_zh"确认"
Confirm → Languages[2]Confirm_ja"確認"

生成層:CodeDefiner によるコード生成 ​

CodeDefiner の rds アクションでは、DB 構成・定義アクセサコード生成に続いて CreateMvcCode() が呼ばれ、その中で Display 関連のコードも生成されます。CodeDefiner 全体の仕組みは CodeDefiner を参照してください。

図を読み込み中…

コード定義テンプレート ​

json
{
    "Id": "Displays_Parts",
    "Source": "Mvc",
    "OutputPath": "Libraries\\Responses\\Displays.cs",
    "DisplayLanguages": true
}
フィールド説明
Idテンプレートの識別子。{Id}_Body.txt のテンプレートファイルと対応
Source生成先のプロジェクト種別
OutputPath生成されるファイルのパス
DisplayLanguagestrue の場合、全言語バリアントのコードを生成
text
public static string #DisplayId#(
    Context context,
    params string[] data)
{
    return Get(
        context: context,
        id: "#DisplayId#",
        data: data);
}

Display ハンドラ(Parts/Display.cs) ​

中核は Implem.CodeDefiner/Functions/AspNetMvc/CSharp/Parts/Display.cs です。SetCodeCollection() が DisplayAccessor.Displays.DisplayHash の全 Display の全 Languages を走査し、CheckExclude() で絞り込んで、ReplaceCode() でプレースホルダーを置換します。

CheckExclude() の除外条件:

条件除外対象説明
DisplayLanguages が falseデフォルト以外の言語言語バリアントを生成しない場合
DisplayType 指定あり指定外の Type特定種別のみ生成する場合
ClientScript が trueClientScript != true の Displayクライアントサイド専用の Display のみ

ReplaceCode() のプレースホルダー:

プレースホルダー置換内容例
#DisplayId#Display.Id + 言語サフィックス(言語ありなら _ + 言語コード)Confirm、Confirm_ja
#DisplayContent#DisplayElement.Body確認
#DisplayCssClass#Type に対応する CSS クラス(Success / Information / Warning / Error / Confirmation のみ)alert-confirm
#DisplayContentEncoded#HTML エンコードされた BodyBest&auml;tigen

#DisplayId# に言語サフィックスが付くため、1 つの Display に対して最大 8 つ(デフォルト + 7 言語)のメソッドが生成されます。Confirm.json の場合の流れは次のとおりです。

図を読み込み中…

生成される Displays.cs ​

Libraries/Responses/Displays.cs は約 28,000 行の自動生成ファイルで、フラット化された DisplayHash、Get()、全 Display × 全言語のラッパーメソッドを含みます。

csharp
public static Dictionary<string, string> DisplayHash = GetDisplayHash();

private static Dictionary<string, string> GetDisplayHash()
{
    var data = new Dictionary<string, string>();
    DisplayAccessor.Displays.DisplayHash.ForEach(display =>
        display.Value.Languages.ForEach(element =>
            data.Add(
                display.Key + (!element.Language.IsNullOrEmpty()
                    ? "_" + element.Language
                    : string.Empty),
                element.Body)));
    return data;
}

// --- 自動生成されたラッパーメソッド(1,255 × 8 言語分) ---
public static string Confirm(
    Context context, params string[] data)
{
    return Get(
        context: context,
        id: "Confirm",
        data: data);
}

public static string Confirm_ja(
    Context context, params string[] data)
{
    return Get(
        context: context,
        id: "Confirm_ja",
        data: data);
}

// ... Confirm_zh, Confirm_de, Confirm_ko, Confirm_es, Confirm_vn

Displays.Confirm(context) のように呼ぶと、内部で Get(context, "Confirm") が呼ばれ、context.Language に応じた文字列が返ります。

実行層:言語の解決 ​

Context.Language ​

ユーザーの言語は Context クラスの Language プロパティに保持されます。初期値は Service.json の DefaultLanguage です。

csharp
public string Language { get; set; } = Parameters.Service.DefaultLanguage;

Context の初期化(SetUserProperties())で、認証状態に応じて次のように上書きされます。

図を読み込み中…

認証状態処理言語の取得元
API キー認証SetUser()UserModel.Language(DB の Users.Language カラム)
セッション認証(ログイン済み)SetUser()UserModel.Language(DB の Users.Language カラム)
未認証(ログイン画面など)SessionLanguage()多段フォールバック(下記)

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

csharp
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=jaURL パラメータで明示的に指定。セッションにも保存される
2セッション保存値過去のリクエストでセッションに保存された言語
3Accept-Language ヘッダーブラウザの言語設定。ja-JP,ja;q=0.9,en-US;q=0.8 なら先頭のハイフン前 ja を使用
4(最低)DefaultLanguageService.json のシステムデフォルト

WARNING

セッション保存値はコード上では Accept-Language の後に評価されますが、SessionData.Get("Language") ?? language によって、セッションに値があればそちらが優先されます。クエリ文字列で指定した値は、SessionUtilities.Set() がセッションテーブルと同時に context.SessionData も書き換えるため、そのリクエストの時点でセッション保存値になり、最優先で使われます。以降のリクエストでもセッション経由でその言語が維持されます。1.5.8.1 のソースで確認しました(Context.cs#L859-L901、SessionUtilities.cs#L92-L134)。

最後に、得られた言語コードが Users_Language の ChoicesText に含まれるかを検証し、含まれなければ DefaultLanguage にフォールバックします。

DefaultLanguage とユーザーの初期言語 ​

json
{
    "Name": "Implem.Pleasanter",
    "DefaultLanguage": "en",
    "TimeZoneDefault": "UTC"
}

INFO

DefaultLanguage はサーバーのシステムデフォルト言語です。新規ユーザー作成時の初期言語は Users_Language カラムの Default ですが、定義ファイルの値("ja")は起動時に Initializer.SetLanguage() で上書きされます。DefaultLanguage が ChoicesText の言語コードに含まれていればその値、含まれていなければ "en" になります(Initializer.cs#L1342-L1350)。1.5.8.1 の既定の Service.json は "en" なので、新規ユーザーの初期言語も英語です。

全体フロー ​

図を読み込み中…

実行層:表示テキストの取得(サーバーサイド) ​

csharp
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;
}

context.Language が ja なら、Confirm_ja → Confirm の順に探し、どちらも無ければ id(Confirm)そのものを返します。

図を読み込み中…

data を渡すと、{0} / {1} のようなプレースホルダーが置換されます。

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

List<DisplayElement> に対する拡張メソッド Display(context) もあり、次の 3 段階でフォールバックします。

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

クライアントサイド:display.ts と $p.display() ​

公開されるのは ClientScript: true の Display だけ ​

図を読み込み中…

クライアントサイドには、ClientScript: true が設定された Display だけが公開されます。CodeDefiner の CheckExclude() が次の条件で除外するためで、サーバーサイド専用のメッセージ(セキュリティ関連のエラーメッセージなど)がクライアントに露出するのを防いでいます。

csharp
if (codeDefinition.ClientScript && display.ClientScript != true)
    return true;  // 除外

自動生成される辞書 ​

typescript
const displays = {
    Confirm: 'Confirm',
    Confirm_zh: '确认',
    Confirm_ja: '確認',
    Confirm_de: 'Bestätigen',
    Confirm_ko: '확인',
    Confirm_es: 'Confirmar',
    Confirm_vn: 'Xác nhận',
    Ym: 'Month and year',
    Ym_ja: '年月',
    // ... 多数のエントリ
} as const;

キー形式はサーバーサイドと同じ {DisplayId} / {DisplayId}_{Language} で、値は HTML エンコードされたテキスト(#DisplayContentEncoded# で生成)です。型は type DisplayId = keyof typeof displays; のようにキーから導出されるため、存在しない DisplayId はコンパイル時にエラーになります。

display 関数 ​

typescript
function display(defaultId: DisplayId): string {
    const language = ($('#Language').val() ?? '').toString();
    const localId = (language
        ? `${defaultId}_${language}`
        : defaultId) as DisplayIdAll;
    return displays[localId] ?? displays[defaultId] ?? defaultId;
}
  1. HTML 内の hidden input #Language(サーバーのレンダリング時に Context.Language の値がセットされる)から言語コードを取得します。
  2. 言語コードがあれば {DisplayId}_{Language}、なければ {DisplayId} をキーにします。
  3. 言語固有キー → デフォルトキー → キー文字列そのもの、の順でフォールバックします。
html
<input type="hidden" id="Language" value="ja" />

この関数は $p.display = display; で公開され、スクリプトから次のように使えます。

javascript
var confirmText = $p.display('Confirm');
// context.Language が "ja" の場合 → "確認"
// context.Language が "en" の場合 → "Confirm"

ページ遷移なしで言語を切り替える場合は、#Language の値を変更してから $p.display() を呼ぶと新しい言語のテキストが返ります。

サーバーとクライアントの対比 ​

項目サーバーサイドクライアントサイド
言語C#TypeScript
辞書の形式Dictionary<string, string>const displays = ... as const
言語の取得元Context.Language$('#Language').val()
取得関数Displays.Get(context, id)$p.display(id)
フォールバックid_lang → id → id 文字列id_lang → id → id 文字列
対象 Display全件(約 1,255)ClientScript: true のみ
生成ファイルDisplays.cs(約 28,000 行)display.ts

フォールバックのロジックは一致しており、サーバーとクライアントで同じ表示テキストが得られます。

通貨の書式(CultureInfo) ​

数値項目の通貨表示などに使う CultureInfo は、言語コードから Context.CultureInfoCurrency() の switch で決まります(Context.cs#L1403)。一覧に無いコードは new CultureInfo(language) にそのまま渡されます。

言語コードCultureInfo
enen-US
zhzh-CN
jaja-JP
dede-DE
koko-KR
eses-ES
vnvi-VN

クライアント側の通貨の入力チェック(validator.js)と日時ピッカーの日本語化(jqueryui.js の case 'ja')も、言語コードを列挙した switch です(validator.js#L149、jqueryui.js#L93)。

項目の多言語ラベル(サイト設定) ​

サイト設定のエディタで項目ごとに設定する言語別の表示名・説明・入力ガイドは、Column.MultilingualLabelText に DisplayElement の JSON 配列(Language・LabelText・Description・InputGuide)として保存されます。表示時は ColumnUtilities.GetMultilingualLabelText() が Language == context.Language の要素を探し、見つからなければ空文字を返します(呼び出し側で通常の表示名が使われます)(ColumnUtilities.cs#L521-L528)。

この多言語ラベルは CSV でエクスポート・インポートできます(MultilingualLabelExportImport)。

csv
ColumnName,Attributes,ja,en,zh,de,ko,es,vn
Status,LabelText,状況,Status,状态,Status,상태,Estado,Trạng thái
Status,Description,現在の状況,Current status,,,,,
  • 1 列目は ColumnName、2 列目は Attributes(LabelText・Description・InputGuide)でなければエラーになります。3 列目以降の見出しのうち、SupportedLanguages(ja, en, zh, de, ko, es, vn)に含まれるものだけが読まれます(MultilingualLabelExportImport.cs#L25-L28)。
  • エクスポートされるのは多言語ラベルが設定済みの項目だけで、値が 1 つも無い属性の行は出ません。
  • インポートすると、CSV に出てくる項目の MultilingualLabelText は CSV の内容で丸ごと置き換わります(CSV に無い言語・属性は消えます)。項目名がサイトに無い行は警告になり、スキップされます。

実践的なポイント ​

  • 表示文字列を探す: App_Data/Displays/{DisplayId}.json を見ます。カラムのラベルは Definition_Column/*.json の LabelText* です。同じキーがあれば Display JSON が優先されます。

  • スクリプトで表示文字列を使う: $p.display(id) で取得できるのは ClientScript: true の Display だけです。

  • ログイン前の画面の言語を指定する: URL に ?Language=ja を付けると、その言語がセッションに保存され以降も維持されます。

  • 既定の言語: 未認証時の最終フォールバックは Service.json の DefaultLanguage、新規ユーザーの初期言語は Users_Language の Default です。

  • 言語を追加する: 言語コードはソースの複数箇所に固定で書かれているため、Display JSON とカラム定義に訳を足すだけでは済みません。1.5.8.1 で言語コードを列挙している箇所は次のとおりです。

    箇所内容
    App_Data/Displays/*.json(1,353 ファイル)Languages 配列に新しい言語の要素を足す
    Definition_Column/*.json・__ColumnSettings.jsonLabelText_en 〜 LabelText_vn の言語ごとのプロパティ(Def.cs の ColumnDefinition のフィールドになる)
    Definition_Column/Users_Language.jsonChoicesText の 7 言語(SessionLanguage() の検証にも使う)
    Definition_Code/Displays_Body.txtcolumnDefinition.Id + "_ja" と日本語のサフィックスを固定(L32)
    Initializer.SetDisplayAccessor()En = o.LabelText_en … の 7 言語の対応付け(Initializer.cs#L1100)
    Context.SessionLanguage()Accept-Language の switch(vi → vn)
    Context.CultureInfoCurrency()言語コード → CultureInfo の switch
    MultilingualLabelExportImport.SupportedLanguagesCSV で扱う 7 言語
    validator.js・jqueryui.js通貨の書式と日時ピッカーのロケール

    これらを直したうえで CodeDefiner を実行します。言語の一覧を設定ファイルにまとめてコードを変えずに言語を足せるようにする改修案は 多言語の言語定義を設定ファイルで管理する(改修案) にまとめています。

関連ページ ​

変更履歴

第5版拡張ライブラリの読み込みと開発・デバッグ、拡張ヘッドリンク、SMTP の OAuth 送信の解説と、多言語・外部公開カレンダー・スレッド型サイトなどの改修・設計メモを追加
第4版「内部実装を読む」を 1.5.8.1 のソースで検証して修正
第3版元記事への言及を整理し、必要なコードをページに収録。検索機能に一覧の検索と絞り込みを追加
第2版記事のファイル名に並び順の番号を付け、元記事リンクを frontmatter の sources に移行
第1版「内部実装を読む」に CodeDefiner・多言語対応・ライセンス判定・拡張計算式を追加