Fix Language and Plural-Forms headers written by waGettextParser - #390
Open
SergeR wants to merge 1 commit into
Open
Fix Language and Plural-Forms headers written by waGettextParser#390SergeR wants to merge 1 commit into
SergeR wants to merge 1 commit into
Conversation
The .po generator behind `php wa.php locale <slug>` had two independent bugs,
both invisible on ru_RU and hitting every other locale.
1. The `Language:` header was never written. getMeta()'s template carries
`X-Poedit-Language` but not the standard header gettext has used since 0.18,
and getOldMeta() only rewrites keys already found in the file, so no amount
of regeneration adds it. msgfmt --check complains about this on every locale,
ru_RU included.
2. getPluralByLocale() knew a single language: ru_RU got the three-form Slavic
rule, everything else got `nplurals=2; plural=(n != 1)`. getPluralsText(),
meanwhile, always wrote exactly three `msgstr[]`. So for two-form locales the
header and the body disagreed and msgfmt refused the catalog outright
("number of plural forms in 'msgstr' doesn't match ..."), while uk_UA and
be_BY compiled but got Russian's count with English's rule - the third form
unreachable, the first two wrongly bounded.
Plural rules now live in wa-system/locale/data/{locale}.php under a new
`plural_forms` key, next to the rest of what the framework knows about a locale.
The parser holds no language table: it reads the rule through waLocale::getInfo()
and falls back to a single default for locales without a data file. The number
of forms written to the body is taken from the very header that goes into the
file, so the two cannot drift apart again.
Locale data files are added for uk_UA, be_BY, pl_PL, kk_KZ, nl_NL and ka_GE -
without them the fix cannot reach the locales that need it most. Note that
Polish shares neither its rule with the other Slavic languages (its first form
is used for 1 only) nor French with the other two-form ones (zero is singular
in French).
An existing `Plural-Forms` header is replaced only when it declares fewer forms
than the locale has, which is exactly the catalogs broken by the bug above; a
header declaring enough forms is left alone, so both a rule corrected by hand
and a locale with no data file keep working. ru_RU catalogs are unaffected: the
generated header is byte-identical to the previous one.
Also in wa-system/locale:
* waLocale::format() read `frac_digits` without ifset() while the two keys next
to it on the same line were already guarded. Locale data files in the wild
have unknown shape, and es_ES/fr_FR in this repository did not carry the key
either, which is now fixed as well.
* getMeta() wrote `X-Poedit-Basepath: utf-8`, having interpolated the charset
where the basepath was meant.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
Author
|
Tests for this PR, kept out of the diff: https://gist.github.com/SergeR/fe435c48cc806fc195027e991c07f9e6 Same arrangement as #380: a throwaway
What it exercises:
Not proposing PHPUnit as a dependency — this is just so the behaviour can be checked locally. Happy to reshape it if the project adopts a format of its own. |
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.
The problem
php wa.php locale <slug>produces.pofiles with two defects. Both are invisible onru_RU, which is why they went unnoticed, and both hit every other locale.Language:is never writtengetMeta()builds the header of a new catalog. It writesX-Poedit-Language, a Poedit field, but notLanguage:— the standard header gettext has used since 0.18. The template looks like it was copied from Poedit output older than that.Existing catalogs are no better:
getOldMeta()rewrites the keys it finds in the file, and a key that was never there does not appear by itself, however many times the catalog is regenerated.This affects all locales,
ru_RUincluded.msgfmt --checkcomplains about it.The header and the body disagree on how many plural forms there are
getPluralByLocale()was a singleif:ru_RUgot the three-form Slavic rule, everything else gotnplurals=2; plural=(n != 1).getPluralsText()meanwhile wrote plural forms like this:Always exactly three, whatever the header said. Two symptoms follow:
en_US,de_DE,fr_FR,nl_NL,kk_KZ— the rule is right for them, but the header declares two forms while the body holds three.msgfmtrejects the file: number of plural forms in 'msgstr' doesn't match the number specified in 'Plural-Forms'. The catalog does not compile at all.uk_UA,be_BY,pl_PL— three forms in the body is what they need, so the file compiles, and the damage moves to runtime. The header claims two forms with(n != 1), so the third form is unreachable and the first two are bounded as "1 / everything else" instead of "1 / 2–4 / 5+". The user sees the Russian equivalent of "5 файла".ru_RUwas the only locale where the header and the body agreed.Reproduction on the current
dev, from the root of an installation:The header promises two forms, the generator writes three.
The fix
Plural rules move into the locale data files. A new
plural_formskey inwa-system/locale/data/{locale}.php, alongside everything else the framework knows about a locale:The parser keeps no language table of its own. It reads the rule through
waLocale::getInfo()and falls back to a single default (nplurals=2; plural=(n != 1)) for a locale with no data file — a default, not a second copy of the data.The number of forms in the body comes from the header that actually goes into the file.
getPluralsText()receives it from the same prepared metadatagetOldMeta()renders, so the two cannot drift apart again — which is the failure mode that produced this bug in the first place.Language:is added togetMeta()'s template, andgetOldMeta()now fills it in for existing catalogs. That part needs no data file and works for any locale.Six locale data files are added —
uk_UA,be_BY,pl_PL,kk_KZ,nl_NL,ka_GE. Without them the fix cannot reach the locales that need it most:uk_UAandbe_BYwould still fall back to the two-form default.Two rules here are easy to get wrong and are worth a look:
n == 1only, so21takes the third form (21 plików), where Russian takes the first (21 файл).(n > 1), not(n != 1): zero is singular in French (0 fichier).Existing headers are respected. A catalog's
Plural-Formsis replaced only when it declares fewer forms than the locale has — precisely the catalogs the old generator broke. A header declaring enough forms is left alone, so a rule a translator corrected by hand survives regeneration, and so does a locale the framework ships no data file for. Repairing a brokenuk_UAcatalog keeps its third translation rather than truncating it.ru_RUis untouched by design. The generatedPlural-Formsline is byte-identical to the one every existing Russian catalog already carries; there is a test asserting exactly that.Two smaller things in the same files
waLocale::format()read$locale_info['frac_digits']directly while the two keys next to it on the same line already went throughifset(). Locale data files on installations in the wild have unknown shape — andes_ES.php/fr_FR.phpin this repository did not carry the key either, so that is fixed too, along with the other number-formatting keys those files were missing.getMeta()emittedX-Poedit-Basepath: utf-8: the charset was interpolated where$meta['basepath']was meant. The key was defined and unused.Happy to drop either of these into a separate PR if you would rather keep this one narrow.
Compatibility
ru_RUcatalogs: no change, byte for byte.Plural-Forms: no change unless it declares too few forms for the language.Language:starts being written, plural handling falls back to the previous two-form default, and any hand-written rule is preserved.waContactLocaleFieldwhenoptions['all']is set.waLocale::getAll()filters bywa-config/locale.phpby default, so interface language selection andwaRequest::getLocale()are unaffected.Tests
The repository has no PHPUnit setup, so I have not added one here. The suite lives in a gist instead, same as for #380 — link in a comment below. It covers both files end to end, parametrised over every locale in
wa-system/locale/data/, and fails 30 of its 122 tests against the unpatched parser.🤖 Generated with Claude Code