Internationalisation (gettext)

XCSoar uses GNU gettext for UI strings. Catalogues live in po/ (33 languages plus the xcsoar.pot template). Macros are defined in src/Language/Language.hpp.

Marking strings in source

  • _("Text") — translate immediately at the call site.

  • N_("Text") — mark for extraction only (static arrays / initialisers); translate later with gettext(). AddChoices() does this for StaticEnumChoice labels.

  • C_("Context", "Text") / NC_("Context", "Text") — same as _() / N_(), with a gettext msgctxt so identical English strings can be disambiguated by role.

Do not mark log messages for translation (LogFormat(), LogDebug(), LogInfo(), LogError(), and similar). Keep logs in English for consistency and debugging.

Do not mark ICAO abbreviations and codes for translation. They stay as published in Doc 8400 (IAS, TAS, CAS, EAS, QNH, QFE, QNE, IFR, VFR, VMC, IMC, and the rest of that list). Write the abbreviation as a string literal. A sentence around it is ordinary language and still uses _() / N_(). Inside that sentence, leave the abbreviation unchanged. Do not add a msgid that is only the abbreviation. Leave abbreviations that are already in the catalogues alone.

data.FmtComment("{} {}", speed, "TAS");
AddBoolean("IAS",
           _("Show the speed-to-fly as indicated airspeed."),
           on, listener);

Do not translate brand names. Write them as published: WeGlide, SoaringScoring, SkySight, XC Therm, NetCDF. A label that is only the name is a string literal. A sentence around it still uses _() / N_(), and the name stays unchanged inside that sentence.

Reuse existing English strings whenever the same wording fits. Search po/xcsoar.pot and existing _() / N_() call sites before adding a new string. Slightly different spellings or punctuation create extra catalogue entries and more work for every language. Prefer matching an existing msgid (or an existing msgctxt + msgid pair) over inventing a near-duplicate. Use msgctxt only when the same English text genuinely needs different translations by role.

When to use msgctxt

Use context for short, ambiguous UI labels that share English wording but mean different things (button vs setting vs status). Prefer existing context names:

  • Button, Menu, Setting, Status

  • Weather control, Weather layer, Abbreviation, InfoBox

Examples:

C_("Button", "Setup")
NC_("Setting", "Auto")
C_("Status", "Live")
C_("Weather control", "Time")

Avoid inventing new context names when an existing one fits.

Updating catalogues

After adding or changing marked strings:

make update-po TARGET=UNIX

This regenerates po/xcsoar.pot and merges into every po/*.po.

What make update-po does

Implemented in build/gettext.mk:

  1. xgettext / msgcat with --no-wrap and --no-location (keywords _, N_, C_, NC_) → po/xcsoar.pot

  2. For each language file:

    • msgmerge --no-wrap --no-location --previous

    • msgattrib --clear-fuzzy --empty --no-obsolete --clear-previous

    • tools/update_po_low_churn.py patches only changed entries

#: file:line locations are omitted on purpose. They churn on every source edit and are not required at runtime.

Low-churn policy

Routine catalogue updates must not rewrap or rewrite untouched entries.

tools/update_po_low_churn.py:

  • Rewrites only entries whose translation or key changed

  • Keeps original msgid / msgstr quote wrapping where possible

  • Strips leftover #: lines

  • Upgrades 1:1 bare → msgctxt additions in place when possible

  • Falls back safely if entry block spans overlap

Never bulk-save a whole .po with polib (or any tool that rewraps the file). That destroys low-churn history and produces huge diffs. Edit msgstr values with targeted replacements, or go through make update-po / update_po_low_churn.py.

Filling translations

  1. Run make update-po.

  2. Fill only the new empty msgstr entries (all 33 languages when possible). Do not rewrite, rewrap, or reorder existing translations.

  3. Validate:

    msgfmt --check -o /dev/null po/<lang>.po
    
  4. Spot-check placeholders (%s, %u, …) match the msgid.

  5. Avoid msgstr text that looks like printf tokens unless the msgid has the same placeholders (e.g. Hungarian 50%-nál can be misread as %-n; prefer a spelled-out “percent” form).

Keep brand names (WeGlide, SoaringScoring, SkySight, XC Therm, NetCDF, …) and ICAO abbreviations (IAS, TAS, QNH, …) unchanged in every language.

Commit organisation

  • Small fixes: one po: commit (or po/<lang>: for a single file), usually at the end of a feature branch.

  • Large refreshes:

    1. po: Regenerate translation catalog (xcsoar.pot, and any source fix required for extraction)

    2. One po/<lang>: Refresh translations commit per language

Each language file should be touched once on a refresh branch when practical. Fold follow-up msgstr fixes into that language’s commit.

.po syntax notes

  • Never put custom comments between #, fuzzy and msgid, or between msgid and msgstr.

  • Place comments before #, fuzzy or at the start of an entry block.

  • Invalid syntax breaks msgmerge during make update-po.