Djangoの多言語対応:"Polish"が"ポーランド語"になる悲劇を防ぐ(Contextual Markers)

多言語(i18n)を正しくサポートすると、こんな経験をすることがあります。

  • ボタンに “Polish”(UIを整える)と書いておくと、翻訳結果が “ポーランド語” になる
  • 日付選択コンポーネントの “May” が、ある言語では 人名 のように翻訳される
  • メニューの “Book” が「本」だけに翻訳され、"予約する" には翻訳されない

問題の原因は簡単です。

コンピュータは「文脈(context)」を理解しません。 文字列が同じならすべて同じものとして扱います。

この記事では、Djangoの Contextual Markers を活用して、ソースコードはそのままに、 同一文字列に対して異なる翻訳を付与する方法を整理します。


なぜこうなるのか? gettextの基本動作



Djangoの翻訳システムは内部で GNU gettext を使用します。 gettextは非常に単純なルールで動作します。

  • 原文文字列 → msgid
  • 翻訳文字列 → msgstr
  • msgid が同じなら必ず同じ msgstr を使用
# django.po

msgid "Polish"
msgstr "ポーランド語"

このように .po ファイルに一度マッピングが作られると、 どこで "Polish" を使っても 常に同じ翻訳 が使われます。

したがって、次の二つのUIは区別できません。

  • "Polish" = UIを整える動詞
  • "Polish" = 言語/国名

これを避けるためにソースコードをこう変更するケースがあります。

<!-- 避けるべきコード -->
{% trans "Polish (verb)" %}
{% trans "Polish (language)" %}

こうすると翻訳はうまくいくものの、 ユーザーに表示される文字列自体が変になる か 他の場所で再利用しにくくなる問題が生じます。

私たちが望むのは:

  • ソース文字列はまだ "Polish" のまま
  • 「今どの意味で使われているか」を別途説明 すること

このとき使うのが Contextual Marker(文脈情報) です。


テンプレートで解決する: {% translate %} + context

Djangoテンプレートでは {% translate %}(または旧スタイル {% trans %})タグに context オプションを付けて、同一文字列を文脈別に区別できます。

1) 既存コード(衝突発生)

{% load i18n %}

<button>{% translate "Polish" %}</button>
<span>{% translate "Polish" %}</span>

.po ファイルでは両方とも msgid "Polish" になるので 一方の意味だけに翻訳 できます。

2) 改善されたコード(context 使用)

{% load i18n %}

<button>
  {% translate "Polish" context "verb: to refine UI" %}
</button>

<span>
  {% translate "Polish" context "language name" %}
</span>

ここで重要なのは:

  • context の後の文字列は ユーザーには表示されません
  • 翻訳システムと翻訳者のための メタ情報 です。
  • 意味が明確になるように短く説明するのがベスト。

  • "verb: to refine UI"

  • "language name"
  • "menu label"
  • "button text" など

3) {% blocktranslate %} でも使用可能

文が長い場合 {% blocktranslate %}(または {% blocktrans %})を使うときも context を付けられます。

{% load i18n %}

{% blocktranslate context "greeting message" with username=user.username %}
  Hello {{ username }}
{% endblocktranslate %}

こうすれば同じ "Hello %(username)s" スタイルの文でも 別の文脈で複数回使えます。


Pythonコードで解決する: pgettext



テンプレートではなく views, models, forms などの Python コード内では gettext の代わりに pgettext 系関数 を使用します。

代表的に次の3つがあります。

  • pgettext(context, message)
  • pgettext_lazy(context, message) – 遅延評価用(モデルフィールド、モジュールレベルなど)
  • npgettext(context, singular, plural, number) – 複数形 + 文脈同時処理

1) 基本例

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 を使う

モデルフィールドの verbose_namehelp_text、 あるいは choices に入るラベルでも同音異義語の問題が起きます。

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

同一語が複数形で意味が変わる場合は npgettext を使って文脈 + 複数形を同時に処理できます。

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 ファイルではどう見えるか?

上記のように context を使った後:

python manage.py makemessages -l ko

同じコマンドでメッセージを抽出すると、 .po ファイルには msgctxt フィールドが追加されます。

# 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 の書き方のヒント

文脈文字列はユーザーには表示されませんが、 翻訳者にとってはほぼ唯一のヒント です。以下の基準を推奨します。

1) 「役割」を説明するように書く

  • "button label" – ボタンに使われるテキスト
  • "menu item" – ナビゲーションメニュー
  • "tooltip" – マウスオーバーテキスト
  • "error message" – エラーメッセージ
  • "form field label" – フォームラベル

文字列の UI 役割 を説明すると、 ほぼすべての言語で自動翻訳を経ても意味が保たれます。

2) 「どの概念か」をもう一度書く

同一語が複数概念で使われる場合:

  • "File"
  • "file menu item"
  • "uploaded file object"

  • "Order"

  • "e-commerce order"
  • "sorting order"

このように ドメイン概念 を一緒に書くと、 多言語翻訳でも意味がより安定します。

3) 翻訳文字列を context に入れない

context は説明領域です。 ここに翻訳結果(別言語)を直接書くと:

  • 言語が増えるほど管理が非常に難しくなる
  • 自動翻訳システムが混乱する可能性がある

常に原文(通常は英語)を基準に、概念/役割を短く説明 するのがベストです。


いつ Contextual Markers を使うべきか?

次のいずれかに該当する場合、ほぼ必ず context/pgettext を検討してください。

  1. 短い文字列(1–2語)
  • ボタンテキスト
  • タブ名
  • メニュー項目
  1. 実際の UI で異なる意味で再利用される文字列
  • "Open", "Close", "Save", "Apply", "Reset" など共通アクション
  • "Book", "Order", "Post" のように名詞/動詞として使われる単語
  1. 翻訳者が画面を見ずに翻訳するケース
  • 翻訳プラットフォームを使用する時
  • 外部翻訳業者に .po ファイルだけを渡す時

まとめ

同音異義語翻訳に戸惑う開発者の姿

  • Django はデフォルトで同一 msgid に一つの翻訳しかマッピングしません。
  • そのため "Polish", "May", "Book" のように 同音異義語 が翻訳でよく絡みます。
  • このとき ソース文字列を無理に変えずに

  • テンプレートでは {% translate "…" context "…" %}

  • Python コードでは pgettext / pgettext_lazy / npgettext を使い、文脈(context) を追加

  • すると .po ファイルに msgctxt が追加され、

  • 同じ原文でも別々の翻訳が可能
  • 翻訳ツールでも別項目として管理できる

結果として:

  • コードの可読性は保ちつつ
  • 翻訳品質と保守性を大幅に向上

多言語サポートがますます必須になる時代に、 Contextual Markers は Django 開発者が一度は習得しておくべき i18n ツールです。


おすすめの関連記事