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

* 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/)