Conversation
Contributor
Author
|
Please see https://github.com/acinader/hledger/blob/i18n-poc/doc/TRANSLATING.md as an entry point for reviewing this draft pr. |
acinader
force-pushed
the
i18n-poc
branch
3 times, most recently
from
September 17, 2026 23:27
0697094 to
2f50272
Compare
Contributor
Author
This comment was marked as off-topic.
This comment was marked as off-topic.
acinader
added a commit
to acinader/hledger
that referenced
this pull request
Sep 20, 2026
"Monthly Balance Sheet" was an interval word plus a report name, joined by a space in the code. One adjective form had to fit every report, and the space could not be dropped. Now each compound report lists its own title per interval, so a translator renders the whole phrase as the language needs, with no grammar logic in Haskell. The German catalog grows from 14 to 44 title entries. English output is unchanged. this should bring hledgerorg@8ddb03f changes into hledgerorg#2736 should bring hledgerorg#2736 upto parity with hledgerorg#2735 AI usage: drafted with Claude Code, reviewed and edited by the author. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…y ago" "Biannual" means twice a year; the title of a report with a two-year interval now says "Biennial". And stats no longer says "1 days ago". The interval word is about to become a translation key, and every later change to an English string invalidates its translations, so it is fixed first; the stats wording is fixed while looking at it. AI usage: drafted with Claude Code, reviewed and edited by the author. Claude-Session: https://claude.ai/code/session_01G2VXnprjHmXZR8tgWPV3vz
A Translations value maps English text to a translation; tr, trc, trf
and trn look text up, with contexts, {name} placeholders and plural
forms. Catalogs are gettext PO files, so the usual translator tools can
edit them; a German catalog is built in (hledger-lib/locale/de.po,
embedded at build time), and a user can override or add a language with
$XDG_CONFIG_HOME/hledger/locale/LANG.po. The parser is ours, since no
Haskell gettext library is in the snapshot, and handles what Poedit,
Weblate and msgmerge write, including the Plural-Forms expression.
Lookups are pure functions of the Translations value: there is no
process-global locale, so hledger-web can serve each request in its own
language. With the default (English) value every lookup is the identity,
so existing output is unchanged byte for byte.
hledger-lib/locale/README.md explains the catalogs to translators, with
the German terminology choices.
ReportOpts gains translations_ (English by default), and
showPeriodAbbrevWith and showDateSpanAbbrevWith render month names from a
given TimeLocale; the --lang option and the report renderers that use
these follow in the next commit. Language tags are normalized and matched
with RFC 4647 truncation (de-CH falls back to de), and the "auto" setting
follows LANGUAGE, LC_ALL, LC_MESSAGES and LANG with gettext's rules, so
LC_ALL=C still means English.
AI usage: drafted with Claude Code, reviewed and edited by the author.
Note: I made no real effort to read or understand
hledger-lib/Hledger/Utils/I18n.hs (yet?). I anticipate that it
will be maintained by agent and covered with appropriate tests.
Claude-Session: https://claude.ai/code/session_01G2VXnprjHmXZR8tgWPV3vz
A new general option, --lang=LANG (also usable in config files), selects a translation catalog: a language tag like de, auto (from the environment), or en, the default. It translates the compound reports' titles, section titles and interval words, the balance and budget report titles and their valuation descriptions, the Total/Average column headings and the Net: row in text output, month names in period headings (reportPeriodName now takes the ReportOpts and uses the translated TimeLocale), and the sheet names of FODS output. Titles are built once and shared by every output format, so like --title they are translated in CSV, TSV and JSON output too; column headings in those formats stay English, and an explicit --title or --subreport-titles is used as given. The composed titles are now single templates with placeholders, so a translator can reorder them. Docs: a Languages section, the environment variables, and the general options list. AI usage: drafted with Claude Code, reviewed and edited by the author. Claude-Session: https://claude.ai/code/session_01G2VXnprjHmXZR8tgWPV3vz
The help dialog and the register screen's composed labels are left for a follow-up; they need a layout rework to fit longer text. AI usage: drafted with Claude Code, reviewed and edited by the author. Claude-Session: https://claude.ai/code/session_01G2VXnprjHmXZR8tgWPV3vz
hledger-web now loads every available translation catalog at startup and
chooses one per request: a _LANG query parameter (remembered in a
SameSite cookie, only when it names an available catalog), the _LANG
cookie, the browser's Accept-Language header (each preference tried with
its subtags dropped, so de-CH gets de), then the server's --lang. Pages
say which language they are in (<html lang>) and that they vary by it
(Vary), for caches in front of a shared server.
Templates use _{HMsg "..."} and _{HMsgc "context" "..."}, which look up
the request's catalog through Yesod's RenderMessage; handlers get the
same Translations from getViewData. The add form's validation messages
and yesod-form's own messages follow too, and the placeholders that
hledger.js writes on added rows come from the page.
Translations are viewer-controlled text and are rendered as text,
in content and in attributes; a test with a hostile catalog checks it.
AI usage: drafted with Claude Code, reviewed and edited by the author.
Claude-Session: https://claude.ai/code/session_01G2VXnprjHmXZR8tgWPV3vz
tools/i18n-extract.py collects the translatable strings (tr, trc, trf,
trn, i18n, i18nc, HMsg, HMsgc) into hledger-lib/locale/hledger.pot with
source references, translator comments and the python-brace-format flag
that makes Poedit and Weblate check placeholders. It can also write a
pseudo-locale catalog, which shows any output that is still English, and
check the catalogs for stale and untranslated entries.
just i18n-pot, i18n-check, i18n-merge (msgmerge), i18n-pseudo.
AI usage: drafted with Claude Code, reviewed and edited by the author.
Note: I anticipate that i18n-extract.py will be agent maintained and I
made no effort to understad it.
Claude-Session: https://claude.ai/code/session_01G2VXnprjHmXZR8tgWPV3vz
A step-by-step guide for contributing a language, written for people who are neither programmers nor professional translators: creating a catalog from the template with Poedit or a text editor, what placeholders, contexts and the developer notes mean, trying the file against an installed hledger without building anything, sending it in, and keeping it current. Ends with the rules and tooling for developers. Linked from the developer docs index, the contributor quick start, the locale directory and the manual. AI usage: drafted with Claude Code, reviewed and edited by the author. Claude-Session: https://claude.ai/code/session_01G2VXnprjHmXZR8tgWPV3vz
The journal and register handlers still set their titles with plain setTitle, so a French or German viewer got "journal - hledger-web" in the tab while the rest of the page was translated. The edit and upload pages already used setTitleI. Both titles are now catalog entries, with a translator note, and the German catalog has them. The browser test for German checks the tab titles too; since the English ones are lower case, the check cannot pass on an untranslated title. AI usage: drafted with Claude Code, reviewed and edited by the author.
So that the cost of catalogs can be kept an eye on as they grow, --debug now reports, once, how long parsing the built-in catalogs took, and for each language loaded: where its catalog came from (built-in and/or the path of the user's file), how many entries it has, roughly how much text it holds, and how long loading and merging took. The size is measured after the timer stops, and none of it is computed without --debug. translations: parsed 1 built-in catalogs, 1.8 ms translations: loaded de (built-in, ~/.config/hledger/locale/de.po): 179 entries, ~8 KB of text, 0.5 ms The built-in parse is timed in availableLanguages, because listing the built-in languages is what forces it. AI usage: drafted with Claude Code, reviewed and edited by the author.
quotedString collected every character of every quoted string into a String one at a time, then packed it; since nearly all of a catalog is quoted strings, that was most of the parse. It now takes runs of ordinary characters as slices of the input and handles only escapes a character at a time. A 920 KB catalog of 4000 entries parses in 36 ms instead of 137 ms (25 MB/s instead of 7), with a quarter of the transient allocation. The built-in German catalog goes from about 1.8 ms to about 0.9 ms. AI usage: drafted with Claude Code, reviewed and edited by the author.
"Monthly Balance Sheet" was an interval word plus a report name, joined by a space in the code. One adjective form had to fit every report, and the space could not be dropped. Now each compound report lists its own title per interval, so a translator renders the whole phrase as the language needs, with no grammar logic in Haskell. The German catalog grows from 14 to 44 title entries. English output is unchanged. this should bring hledgerorg@8ddb03f changes into hledgerorg#2736 should bring hledgerorg#2736 upto parity with hledgerorg#2735 AI usage: drafted with Claude Code, reviewed and edited by the author.
acinader
added a commit
to acinader/hledger
that referenced
this pull request
Sep 20, 2026
"Monthly Balance Sheet" was an interval word plus a report name, joined by a space in the code. One adjective form had to fit every report, and the space could not be dropped. Now each compound report lists its own title per interval, so a translator renders the whole phrase as the language needs, with no grammar logic in Haskell. The German catalog grows from 14 to 44 title entries. English output is unchanged. this should bring hledgerorg@8ddb03f changes into hledgerorg#2736 should bring hledgerorg#2736 upto parity with hledgerorg#2735 AI usage: drafted with Claude Code, reviewed and edited by the author. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
acinader
force-pushed
the
i18n-poc
branch
2 times, most recently
from
September 20, 2026 23:42
19b2d02 to
b0251c0
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.







Translations for hledger's own output text, following the list discussion
(https://groups.google.com/g/hledger/c/PmcY3J8WiDc), #1025 and #231. German is
the second language.
What it does
hledger bs --lang de:--lang=LANG(usable in config files) selects atranslation catalog. It translates the compound reports' titles, section
titles and interval words, the balance and budget report titles and their
valuation descriptions, the Total/Average column headings and the Net: row
in text output, month names in period headings, and FODS sheet names.
?_LANG=de(remembered in a cookie), the
_LANGcookie, the browser's Accept-Language,then the server's
--lang. Pages, the add form and its validation messages,the help dialog and the file management pages are covered.
hledger-lib/locale/, embedded atbuild time. A user can override or add a language by dropping
~/.config/hledger/locale/LANG.poin place, without rebuilding: atranslator can work against a release binary.
--lang, every lookupis the identity. Checked by diffing the previous build's output for a few
dozen report invocations across output formats, and by the existing test
suites, which pass unchanged.
Design, against the points raised on the list
Stackage snapshot, and haskell-gettext last shipped in 2019. The mechanism
is one module,
Hledger.Utils.I18n, with no new dependency: a small POparser (including the Plural-Forms rule), lookup functions
tr,trc(contexts),
trf({placeholders}),trn(plurals), and language taghandling. The library is agent-generated (Claude Session) and
should be agent-maintained. Goal is to make implementation compliant
with existing tools for localization.
180 strings. The commits are split so that the hledger-lib and CLI parts
(commits 2 and 3) stand on their own; the hledger-ui and hledger-web
commits can be deferred.
--langplus a field on ReportOpts. The field,translations_, holds theloaded catalog rather than a tag, because the renderers are pure and the
catalog is loaded once in IO; the tag is inside it.
--lang=auto, which follows LANGUAGE, LC_ALL,LC_MESSAGES and LANG with gettext's rules, is implemented but opt-in; I am
happy to drop it from this PR if you would rather defer it.
the manual.
--titleand--subreport-titleswin over translation, and number formatsare untouched (they come from the journal).
--titlethey are translated in csv/tsv/json title rows too; column headings in
those formats stay English. If you would rather csv stay entirely English,
the alternative is keeping titles structured until rendering, as
translate report texts to user language using haskell-gettext #2735
does; that is a bigger refactor and can follow.
One deliberate divergence: PO catalogs rather than Shakespeare's
.msgfiles.hledger-web does use Shakespeare's I18N interface (
RenderMessage,_{...}in templates), with these catalogs behind it. PO was chosen because complete
translations come from translators, and PO is what their tools (Poedit,
Weblate) speak: it gives contexts, translator notes, a fuzzy workflow, and
lets someone test a catalog against a release without a Haskell build. My POV being
that gettext is mature and we can reliably maintain the functionality we want
with agents maintaing the i18n core code.
Docs and tooling
options list; hledger-web's manual: a Language section.
doc/TRANSLATING.md: a step-by-step guide for translators who are neitherprogrammers nor professionals, with a worked example.
tools/i18n-extract.pyandjust i18n-pot,i18n-check,i18n-merge,i18n-pseudomaintain the catalogs.Tests
hledger functional suite (9 new cases in
hledger/test/i18n.test),hledger-lib unit tests for the parser, plural rules and tag handling,
hledger-web yesod tests for language selection, the cookie and a hostile
catalog rendered as text, and a Playwright spec for the German UI. All four
packages build warning-free.
Caveats
I have not personally walked through the steps indoc/TRANSLATING.mdasa translator would; the guide was checked mechanically (a throwaway French
catalog placed in the config directory, exercised through
--lang fr), notby a person following it cold.
No performance testing. With no
--langoption nothing is parsed or read,so the default path should be unaffected. Any
--langvalue, includingenandauto, lists the override directory and parses the built-incatalogs (a few hundred entries). I have not measured either path.
Follow-ups
hledger_site symlink and SUMMARY entry, which I will open separately.
code contributions that touch localizable strings.
Not in this PR
The hledger-ui help dialog and register labels (need a layout rework),
statslabel widths, html/fods column headings, and translating errormessages or manuals.
Terminology
The German catalog uses the terms Henning chose in #2735 (Einnahmen,
Ausgaben, Einnahmenüberschussrechnung, Vermögen, Gesamt, Überschuss,
Einheit), so the two agree; see
hledger-lib/locale/README.md. The oneplace I kept a different word is hledger-web's account column (Konto).
examples/i18n/de.journalnames its accounts aktiva and passiva, whichGerman speakers may want to reconcile.
AI usage: designed and implemented with Claude Code, reviewed, tested, and edited by the author.
🤖 Generated with Claude Code
https://claude.ai/code/session_01G2VXnprjHmXZR8tgWPV3vz