Skip to content

Instantly share code, notes, and snippets.

@skkzsh
Created April 15, 2026 03:30
Show Gist options
  • Select an option

  • Save skkzsh/f9cb249394693f4f913ab5ad9f4bea5c to your computer and use it in GitHub Desktop.

Select an option

Save skkzsh/f9cb249394693f4f913ab5ad9f4bea5c to your computer and use it in GitHub Desktop.
changedetection.io Translation Skill
name translate-po
description Translate untranslated gettext .po entries for changedetection.io. Use when: "translate", "翻訳", "fill in translations", "translate po files", "translate missing strings", "/translate-po".
argument-hint [locale] (e.g. ja, fr, de, it, ko — omit for all languages)
allowed-tools
Read
Edit
Grep
Glob
Bash

translate-po: Gettext .po Translation Skill

Translate untranslated entries in changedetection.io's .po files.

Arguments

  • locale (optional): Target locale code (e.g. ja, fr, de). If omitted, process all languages.

Available Locales

Code Language Word order notes
cs Czech (Čeština) SVO, but flexible
de German (Deutsch) V2 word order, verb-final in subclauses
es Spanish (Español) SVO, adjective-after-noun
fr French (Français) SVO, adjective-after-noun
it Italian (Italiano) SVO, adjective-after-noun
ja Japanese (日本語) SOV, particles, modifier-before-head
ko Korean (한국어) SOV, particles, modifier-before-head
pt_BR Portuguese Brazil (Português) SVO, adjective-after-noun
tr Turkish (Türkçe) SOV, agglutinative
uk Ukrainian (Українська) SVO, flexible
zh Chinese Simplified (中文简体) SVO, modifier-before-head
zh_Hant_TW Chinese Traditional (繁體中文) SVO, modifier-before-head

Skip: en_GB and en_US — these fall through to English source strings by design.

Workflow

Step 1: Read the .po file

Read @changedetectionio/translations/{locale}/LC_MESSAGES/messages.po.

The file is large (80-130KB). Read it in chunks to find untranslated entries.

Step 2: Identify untranslated entries

Find entries with empty msgstr "" (but NOT the file header which also has msgstr "").

An untranslated entry looks like:

#: changedetectionio/blueprint/backups/restore.py
msgid "Backup zip file"
msgstr ""

A translated entry looks like:

#: changedetectionio/blueprint/backups/restore.py
msgid "Include groups"
msgstr "グループを含める"

Step 3: Translate

For each untranslated entry, produce a translation following these rules:

Terminology

  • Use "monitor" or "watcher" terminology — NEVER "clock"
  • Use the most brief wording suitable
  • Match the tone and style of existing translated entries in the same file

Format strings — preserve exactly

  • %(name)s, %(count)d, %(mb)s etc. must appear in msgstr unchanged
  • Entries marked #, python-format have Python format strings — be extra careful

Multiline strings

Some msgid/msgstr span multiple lines:

msgid ""
"First line "
"second line"
msgstr ""
"訳文1行目 "
"訳文2行目"

Preserve the multiline structure when the original uses it.

Step 4: Fragment redistribution (CRITICAL for non-SVO languages)

Templates sometimes split UI strings into multiple adjacent msgid fragments. For example, the template might render: "Set to" + input_field + "to disable".

In English this reads naturally, but in languages with different word order (especially SOV languages like Japanese, Korean, Turkish), a 1:1 translation of each fragment produces broken or unnatural text.

When to redistribute:

  • The msgid is a short phrase fragment (not a complete sentence)
  • Adjacent fragments in the same source file form a complete phrase when combined
  • Direct translation of the fragment would be unnatural in the target language

How to redistribute:

  1. Identify the complete phrase by looking at adjacent entries from the same source file
  2. Check the template source to understand the exact HTML structure between fragments
  3. Translate the complete phrase naturally in the target language
  4. Classify which redistribution pattern applies (see below), then distribute accordingly
  5. Every fragment MUST have a non-empty msgstr — gettext treats msgstr "" as untranslated and falls back to the English source string, which breaks the sentence (e.g. "Set to" appearing in the middle of Japanese text). If a fragment has no natural equivalent in the target language (e.g. English "Use" before a noun phrase is redundant in Japanese), use msgstr " " (a single space) — HTML collapses whitespace so it renders as nothing, while gettext treats it as translated.
  6. Always add a translator comment explaining which pattern was applied and why

Redistribution patterns

Pattern A — Move predicate to later fragment (most common)

English SVO puts the verb early; SOV languages need it at the end. Move the verb/predicate into the later fragment.

English Target (ja)
"These settings are" added "to any existing..." 「これらの設定は」既存の〜に追加「されます。」
"Accepts the" [token] "placeholders..." " " [token] 「以下のプレースホルダーを受け付けます」

Pattern B — Swap fragment meanings

When the English and target word order are fully reversed, swap what goes into the front vs. back fragment.

English Target (ja)
"Base URL used for the" [token] "token in notification links." 「通知リンクの」[token]「トークンに使用するベースURL。」
"This will wait" [n] "seconds before extracting the text." 「テキスト抽出前に」[n]「秒間待機します。」
"So it's always better to select" [X] "when you're interested in new content." 「新しいコンテンツに興味がある場合は」[X]「を選択することをおすすめします。」

Pattern C — Single-space suppression (often combined with Pattern A)

When a fragment is semantically unnecessary in the target language, set msgstr " " to suppress it. Frequently used together with Pattern A: suppress the front fragment, move predicate to the back.

msgid Pattern Reason
"Set to" A+C Predicate moved to "to disable", front suppressed
"There are" A+C Absorbed into "system-wide notification URLs enabled"
"Use" C alone Following noun phrase is self-sufficient

Comment format:

Typical explanation line patterns:

  • One-way move: # 述語の訳を後半にまとめた (predicate moved to later fragment)
  • Swap: # 〇〇と〇〇の訳を入れ替えた (e.g. # 条件節と述語の訳を入れ替えた)
# 訳注: "Original complete phrase in English"
# → 「Complete natural translation」
# 述語の訳を後半にまとめた  
#: changedetectionio/path/to/source.py
msgid "fragment part"
msgstr "redistributed translation"

Use the comment language that matches the locale:

  • Japanese: # 訳注:
  • Korean: # 역주:
  • Other languages: # TN: (Translator's Note)

Risk warning for generic short fragments

After redistribution, evaluate the msgid's reuse risk:

  • High risk (add risk warning): msgid is a short/generic English word or phrase (≤3 words, common parts of speech like prepositions, bare verbs, common nouns) AND the msgstr drastically differs from a 1:1 translation (e.g. "file" → complete predicate sentence, "in""-", "Set to"" ").
  • Low risk (skip warning): msgid is long/specific (≥4 words or contains a full clause, proper nouns, distinctive punctuation) and unlikely to be copy-pasted into a new template.

For high-risk entries, append a warning line to the # 訳注: block:

  • Japanese: # リスク: 将来このフラグメントが別テンプレートで再利用されると、本来の英語の意味とは異なる訳が流用されて壊れる。新規追加時は見直すこと。
  • Other languages: # RISK: If this fragment is reused in another template later, the redistributed translation will diverge from the original English meaning and break. Review before adding new usages.

This alerts future maintainers (human or agent) that the redistribute is context-specific and cannot be safely shared across templates.

Cross-reference for non-adjacent paired fragments

.po files sort entries by source file order. When a redistributed fragment's paired fragments live in the same source file, they appear adjacent in the .po file and a single # 訳注: block covers them all.

But if a fragment is referenced across multiple templates (e.g. "Use" in both _common_fields.html and text-options.html), its paired fragments may scatter to non-adjacent positions in the .po file. In that case, a maintainer editing one of the paired fragments won't see the main # 訳注: and may break the redistribution.

When to add a cross-reference comment:

For each non-adjacent paired fragment of a redistributed msgid — especially fragments that carry the moved predicate or other non-1:1 translation — add a short reference back to the main 訳注:

  • Japanese: # 訳注: msgid "<primary>" (別箇所) と共に再配分された訳。msgid "<primary>" の訳注を参照のこと。
  • Other languages: # TN: Redistributed together with msgid "<primary>" (elsewhere). See the 訳注 / TN on msgid "<primary>".

Replace <primary> with the anchor msgid that carries the full 訳注 block.

Adjacency check: After writing all translations, scan the paired-fragment msgids of each redistribute block. If any paired msgid is not directly adjacent (separated by unrelated entries) to the main 訳注 block, add a cross-reference there.

Languages most likely to need redistribution:

  • SOV languages (ja, ko, tr): Almost always need redistribution for fragment pairs
  • Romance languages (fr, es, it, pt_BR): Sometimes, especially with adjective placement
  • German (de): Sometimes, due to verb-final in subclauses
  • Others (cs, uk, zh, zh_Hant_TW): Occasionally

Reference: Check @changedetectionio/translations/ja/LC_MESSAGES/messages.po for existing examples of redistribution with # 訳注: comments.

Step 5: Write translations

Use the Edit tool to write translations into the .po file. Edit each untranslated entry by replacing its empty msgstr "" with the translation.

For entries needing redistribution, also add the translator comment lines before the entry. When the redistribution targets a short/generic fragment (see "Risk warning for generic short fragments" above), also append the risk warning line.

Step 6: Compile

Run:

python setup.py compile_catalog

This compiles all .po files to .mo binary format.

Quality checklist

Before finishing, verify:

  • All %(...)s / %(...)d format placeholders preserved exactly
  • No English left in msgstr (except for proper nouns, technical terms, URLs)
  • Fragment redistributions have translator comments
  • Short/generic redistribute fragments have a reuse-risk warning in their 訳注 block
  • Non-adjacent paired fragments of a redistribute block have a cross-reference 訳注 pointing to the primary msgid
  • python setup.py compile_catalog succeeds without errors
  • Tone matches existing translations in the file
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment