# Djangoの多言語対応:"Polish"が"ポーランド語"になる悲劇を防ぐ(Contextual Markers) 多言語(i18n)を正しくサポートすると、こんな経験をすることがあります。 * ボタンに **“Polish”**(UIを整える)と書いておくと、翻訳結果が **“ポーランド語”** になる * 日付選択コンポーネントの **“May”** が、ある言語では **人名** のように翻訳される * メニューの **“Book”** が「本」だけに翻訳され、"予約する" には翻訳されない 問題の原因は簡単です。 > コンピュータは「文脈(context)」を理解しません。 > 文字列が同じならすべて同じものとして扱います。 この記事では、Djangoの **Contextual Markers** を活用して、ソースコードはそのままに、 同一文字列に対して異なる翻訳を付与する方法を整理します。 --- ## なぜこうなるのか? gettextの基本動作 {#sec-aff43a618865} Djangoの翻訳システムは内部で **GNU gettext** を使用します。 gettextは非常に単純なルールで動作します。 * 原文文字列 → `msgid` * 翻訳文字列 → `msgstr` * **`msgid` が同じなら必ず同じ `msgstr`** を使用 ```po # django.po msgid "Polish" msgstr "ポーランド語" ``` このように `.po` ファイルに一度マッピングが作られると、 どこで "Polish" を使っても **常に同じ翻訳** が使われます。 したがって、次の二つのUIは区別できません。 * "Polish" = UIを整える動詞 * "Polish" = 言語/国名 これを避けるためにソースコードをこう変更するケースがあります。 ```html {% trans "Polish (verb)" %} {% trans "Polish (language)" %} ``` こうすると翻訳はうまくいくものの、 **ユーザーに表示される文字列自体が変になる** か 他の場所で再利用しにくくなる問題が生じます。 私たちが望むのは: * ソース文字列はまだ `"Polish"` のまま * **「今どの意味で使われているか」を別途説明** すること このとき使うのが **Contextual Marker(文脈情報)** です。 --- ## テンプレートで解決する: `{% translate %}` + `context` {#sec-e28151952ba2} Djangoテンプレートでは `{% translate %}`(または旧スタイル `{% trans %}`)タグに `context` オプションを付けて、同一文字列を文脈別に区別できます。 ### 1) 既存コード(衝突発生) {#sec-40afc992a96b} ```html {% load i18n %} {% translate "Polish" %} ``` `.po` ファイルでは両方とも `msgid "Polish"` になるので **一方の意味だけに翻訳** できます。 ### 2) 改善されたコード(context 使用) {#sec-4fa05f04b9d3} ```html {% load i18n %} {% translate "Polish" context "language name" %} ``` ここで重要なのは: * `context` の後の文字列は **ユーザーには表示されません**。 * 翻訳システムと翻訳者のための **メタ情報** です。 * 意味が明確になるように短く説明するのがベスト。 * `"verb: to refine UI"` * `"language name"` * `"menu label"` * `"button text"` など ### 3) `{% blocktranslate %}` でも使用可能 {#sec-c2a9f27dea01} 文が長い場合 `{% blocktranslate %}`(または `{% blocktrans %}`)を使うときも `context` を付けられます。 ```html {% load i18n %} {% blocktranslate context "greeting message" with username=user.username %} Hello {{ username }} {% endblocktranslate %} ``` こうすれば同じ `"Hello %(username)s"` スタイルの文でも 別の文脈で複数回使えます。 --- ## Pythonコードで解決する: `pgettext` {#sec-3a9a008d3f99} テンプレートではなく **views, models, forms** などの Python コード内では `gettext` の代わりに **`pgettext` 系関数** を使用します。 代表的に次の3つがあります。 * `pgettext(context, message)` * `pgettext_lazy(context, message)` – 遅延評価用(モデルフィールド、モジュールレベルなど) * `npgettext(context, singular, plural, number)` – 複数形 + 文脈同時処理 ### 1) 基本例 {#sec-27f89a09cea2} ```python from django.utils.translation import pgettext def my_view(request): # 1. 月(month)名 "May" month = pgettext("month name", "May") # 2. 人名 "May" person = pgettext("person name", "May") # 3. 助動詞 "may" (~かもしれない) verb = pgettext("auxiliary verb", "may") ``` こうすれば三つのケースすべてで `msgid` は `"May"`/`"may"` ですが、 異なる `context` によってそれぞれ別の翻訳が可能です。 ### 2) モデルで `pgettext_lazy` を使う {#sec-11832a96275e} モデルフィールドの `verbose_name` や `help_text`、 あるいは `choices` に入るラベルでも同音異義語の問題が起きます。 ```python from django.db import models from django.utils.translation import pgettext_lazy class Order(models.Model): # "Order" = 注文 type = models.CharField( verbose_name=pgettext_lazy("order model field", "Order type"), max_length=20, ) STATUS_CHOICES = [ # "Open" = 状態 ("open", pgettext_lazy("order status", "Open")), # "Open" = 動作(開く) ("opened", pgettext_lazy("log action", "Open")), ] status = models.CharField( max_length=20, choices=STATUS_CHOICES, ) ``` 上記のように: * 同じ `"Open"` でも * **「状態(status)」** と **「行動(action)」** を明確に分けられます。 ### 3) 複数形まで一緒に処理: `npgettext` {#sec-ed84f3cf8d15} 同一語が複数形で意味が変わる場合は `npgettext` を使って文脈 + 複数形を同時に処理できます。 ```python from django.utils.translation import npgettext def get_notification(count): # "Message" を通知用に使うと仮定 return npgettext( "user notification", # context "You have %(count)d message", # singular "You have %(count)d messages", # plural count ) % {"count": count} ``` --- ## `.po` ファイルではどう見えるか? {#sec-fb610023c81a} 上記のように `context` を使った後: ```bash python manage.py makemessages -l ko ``` 同じコマンドでメッセージを抽出すると、 `.po` ファイルには **`msgctxt`** フィールドが追加されます。 ```po # django.po # 1) "Polish" = UIを整える動詞 msgctxt "verb: to refine UI" msgid "Polish" msgstr "整える" # 2) "Polish" = 言語/国名 msgctxt "language name" msgid "Polish" msgstr "ポーランド語" ``` `msgid` は同じですが: * `msgctxt` が異なるため * 翻訳ツール(Poedit, Weblate, Crowdin など)でも **別々の項目** として扱われます。 * 翻訳者は実際の UI でどのように使われるかを簡単に理解できます。 重要なのは: * **ソースコードの文字列はまったく変わらない** (`"Polish"` そのまま) * `.po` ファイルの構造だけが賢くなる --- ## 良い Context の書き方のヒント {#sec-df37b35ab408} 文脈文字列はユーザーには表示されませんが、 **翻訳者にとってはほぼ唯一のヒント** です。以下の基準を推奨します。 ### 1) 「役割」を説明するように書く {#sec-9dfe29490c86} * `"button label"` – ボタンに使われるテキスト * `"menu item"` – ナビゲーションメニュー * `"tooltip"` – マウスオーバーテキスト * `"error message"` – エラーメッセージ * `"form field label"` – フォームラベル 文字列の **UI 役割** を説明すると、 ほぼすべての言語で自動翻訳を経ても意味が保たれます。 ### 2) 「どの概念か」をもう一度書く {#sec-135dce4b280b} 同一語が複数概念で使われる場合: * `"File"` * `"file menu item"` * `"uploaded file object"` * `"Order"` * `"e-commerce order"` * `"sorting order"` このように **ドメイン概念** を一緒に書くと、 多言語翻訳でも意味がより安定します。 ### 3) 翻訳文字列を context に入れない {#sec-9197b1a38f27} `context` は説明領域です。 ここに翻訳結果(別言語)を直接書くと: * 言語が増えるほど管理が非常に難しくなる * 自動翻訳システムが混乱する可能性がある **常に原文(通常は英語)を基準に、概念/役割を短く説明** するのがベストです。 --- ## いつ Contextual Markers を使うべきか? {#sec-58e84d2a2b4c} 次のいずれかに該当する場合、ほぼ必ず `context`/`pgettext` を検討してください。 1. **短い文字列(1–2語)** * ボタンテキスト * タブ名 * メニュー項目 2. **実際の UI で異なる意味で再利用される文字列** * `"Open"`, `"Close"`, `"Save"`, `"Apply"`, `"Reset"` など共通アクション * `"Book"`, `"Order"`, `"Post"` のように名詞/動詞として使われる単語 3. **翻訳者が画面を見ずに翻訳するケース** * 翻訳プラットフォームを使用する時 * 外部翻訳業者に `.po` ファイルだけを渡す時 --- ## まとめ {#sec-8cc5759d0812} ![同音異義語翻訳に戸惑う開発者の姿](/media/editor_temp/6/14112b82-d049-4b32-9275-02429e9305c0.png) * Django はデフォルトで同一 `msgid` に一つの翻訳しかマッピングしません。 * そのため `"Polish"`, `"May"`, `"Book"` のように **同音異義語** が翻訳でよく絡みます。 * このとき **ソース文字列を無理に変えずに**: * テンプレートでは `{% translate "…" context "…" %}` * Python コードでは `pgettext` / `pgettext_lazy` / `npgettext` を使い、**文脈(context)** を追加 * すると `.po` ファイルに `msgctxt` が追加され、 * 同じ原文でも別々の翻訳が可能 * 翻訳ツールでも別項目として管理できる 結果として: * コードの可読性は保ちつつ * 翻訳品質と保守性を大幅に向上 多言語サポートがますます必須になる時代に、 Contextual Markers は Django 開発者が一度は習得しておくべき i18n ツールです。 --- **おすすめの関連記事** - [Djangoでgettext vs gettext_lazyの混乱を解消する:評価時点で理解する](/ko/whitedec/2026/1/5/django-gettext-vs-gettext-lazy/) - [gettext_lazyをJSONキーに使うと起こる問題と解決策](/ko/whitedec/2025/4/26/gettext-lazy-json-key-problem-solution/)