多言語対応の実装
プリザンターは 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 で定義されています。
{
"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 がそのまま識別子です。
Implem.Pleasanter/App_Data/Displays/
├── AccessControls.json
├── Add.json
├── Confirm.json
├── Delete.json
├── Normal.json
├── Ym.json
└── ... (約 1,255 ファイル)| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
Id | string | Yes | 表示文字列の一意な識別子 |
Type | int | Yes | メッセージ種別(Displays.Types 列挙型に対応) |
ClientScript | bool? | No | true の場合、クライアントサイドにも公開される |
Languages | DisplayElement[] | Yes | 各言語のテキスト定義 |
{
"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 配列のルール
- 先頭要素にはデフォルト(英語)を置く:
Languageプロパティを持たない要素がフォールバック値になります。 - 以降の要素には
Languageを指定する:"ja"、"zh"などの言語コードを設定します。 - すべての言語をそろえる必要はない: 未定義の言語ではデフォルト値が使われます。
DisplayElement は Body 以外に LabelText、Description、InputGuide といった拡張フィールドも持てます。
{
"Id": "DisplayName",
"Type": 110,
"ClientScript": true,
"Languages": [
{
"Body": "Name displayed",
"Description": "The name displayed on the screen"
},
{
"Language": "ja",
"Body": "表示名",
"Description": "画面上に表示される名前"
}
]
}Display Type(メッセージ種別)
| 値 | 名称 | 用途 | CSS クラス |
|---|---|---|---|
| 110 | Normal | 通常のラベル・テキスト | — |
| 120 | Date | 日付関連テキスト | — |
| 130 | DateFormat | 日付フォーマット | — |
| 210 | Success | 成功メッセージ | alert-success |
| 220 | Information | 情報メッセージ | alert-information |
| 230 | Warning | 警告メッセージ | alert-warning |
| 240 | Error | エラーメッセージ | alert-error |
| 310 | Confirmation | 確認ダイアログ | alert-confirm |
| 410 | Validation | バリデーション | — |
カラム定義の多言語ラベル
テーブルカラムのラベルは Definition_Column/*.json で定義されます。LabelText が日本語(主キー)で、LabelText_en / LabelText_zh / LabelText_de / LabelText_ko / LabelText_es / LabelText_vn が各言語のラベルです。
{
"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 プロジェクトのクラス群です。
| クラス | ファイル | 内容 |
|---|---|---|
DisplayElement | Implem.DisplayAccessor/DisplayElement.cs | 1 言語バリアント(Language(null = デフォルト/英語)、Body、LabelText、Description、InputGuide) |
Display | Implem.DisplayAccessor/Display.cs | 1 表示文字列と全言語バリアント(Id、Type、ClientScript、Languages) |
Displays | Implem.DisplayAccessor/Displays.cs | Types 列挙型と DisplayHash(Dictionary<string, Display>) |
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 へ追加します。
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.Displays | Implem.DisplayAccessor | データモデル。DisplayHash を保持 | DisplayId → Display オブジェクト |
Displays | Implem.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 を参照してください。
図を読み込み中…
コード定義テンプレート
{
"Id": "Displays_Parts",
"Source": "Mvc",
"OutputPath": "Libraries\\Responses\\Displays.cs",
"DisplayLanguages": true
}| フィールド | 説明 |
|---|---|
Id | テンプレートの識別子。{Id}_Body.txt のテンプレートファイルと対応 |
Source | 生成先のプロジェクト種別 |
OutputPath | 生成されるファイルのパス |
DisplayLanguages | true の場合、全言語バリアントのコードを生成 |
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 が true | ClientScript != 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 エンコードされた Body | Bestätigen |
#DisplayId# に言語サフィックスが付くため、1 つの Display に対して最大 8 つ(デフォルト + 7 言語)のメソッドが生成されます。Confirm.json の場合の流れは次のとおりです。
図を読み込み中…
生成される Displays.cs
Libraries/Responses/Displays.cs は約 28,000 行の自動生成ファイルで、フラット化された DisplayHash、Get()、全 Display × 全言語のラッパーメソッドを含みます。
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_vnDisplays.Confirm(context) のように呼ぶと、内部で Get(context, "Confirm") が呼ばれ、context.Language に応じた文字列が返ります。
実行層:言語の解決
Context.Language
ユーザーの言語は Context クラスの Language プロパティに保持されます。初期値は Service.json の DefaultLanguage です。
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() の多段フォールバック
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 ヘッダー | ブラウザの言語設定。ja-JP,ja;q=0.9,en-US;q=0.8 なら先頭のハイフン前 ja を使用 |
| 4(最低) | DefaultLanguage | Service.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 とユーザーの初期言語
{
"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" なので、新規ユーザーの初期言語も英語です。
全体フロー
図を読み込み中…
実行層:表示テキストの取得(サーバーサイド)
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} のようなプレースホルダーが置換されます。
// Display 定義: "'{0}' has been saved."
// 日本語: "'{0}' を保存しました。"
var message = Displays.Get(
context,
"Updated",
data: new[] { recordTitle });
// → "'顧客A' を保存しました。"List<DisplayElement> に対する拡張メソッド Display(context) もあり、次の 3 段階でフォールバックします。
| 段階 | 検索条件 | 例(context.Language = "ko") |
|---|---|---|
| 1 | Language == context.Language | Language: "ko" の要素 |
| 2 | Language が null/空 | デフォルト(英語)の要素 |
| 3 | 先頭要素 | Languages[0] |
クライアントサイド:display.ts と $p.display()
公開されるのは ClientScript: true の Display だけ
図を読み込み中…
クライアントサイドには、ClientScript: true が設定された Display だけが公開されます。CodeDefiner の CheckExclude() が次の条件で除外するためで、サーバーサイド専用のメッセージ(セキュリティ関連のエラーメッセージなど)がクライアントに露出するのを防いでいます。
if (codeDefinition.ClientScript && display.ClientScript != true)
return true; // 除外自動生成される辞書
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 関数
function display(defaultId: DisplayId): string {
const language = ($('#Language').val() ?? '').toString();
const localId = (language
? `${defaultId}_${language}`
: defaultId) as DisplayIdAll;
return displays[localId] ?? displays[defaultId] ?? defaultId;
}- HTML 内の hidden input
#Language(サーバーのレンダリング時にContext.Languageの値がセットされる)から言語コードを取得します。 - 言語コードがあれば
{DisplayId}_{Language}、なければ{DisplayId}をキーにします。 - 言語固有キー → デフォルトキー → キー文字列そのもの、の順でフォールバックします。
<input type="hidden" id="Language" value="ja" />この関数は $p.display = display; で公開され、スクリプトから次のように使えます。
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 |
|---|---|
en | en-US |
zh | zh-CN |
ja | ja-JP |
de | de-DE |
ko | ko-KR |
es | es-ES |
vn | vi-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)。
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のswitchMultilingualLabelExportImport.SupportedLanguagesCSV で扱う 7 言語 validator.js・jqueryui.js通貨の書式と日時ピッカーのロケール これらを直したうえで CodeDefiner を実行します。言語の一覧を設定ファイルにまとめてコードを変えずに言語を足せるようにする改修案は 多言語の言語定義を設定ファイルで管理する(改修案) にまとめています。