Skip to content

pl: inflect unit names according to the numeral, not with an English "s" - #744

Merged
NSoiffer merged 4 commits into
daisy:mainfrom
michaldziwisz:pl-unit-plurals
Sep 3, 2026
Merged

pl: inflect unit names according to the numeral, not with an English "s"#744
NSoiffer merged 4 commits into
daisy:mainfrom
michaldziwisz:pl-unit-plurals

Conversation

@michaldziwisz

@michaldziwisz michaldziwisz commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Polish unit names took an English plural ending. The unit rule appends ct: "s" when a unit has no entry in PluralForms, and the Polish PluralForms contained no SI base unit at all. Measured engine output before this change:

1 metr, 2 metrs, 5 metrs, 21 metrs, 2 grams, 2 sekundas, 2 ampers, 2 mols, 2 kilo-metrs

So a Polish NVDA user heard an English suffix on essentially every quantity, including after 5 and 21.

How other languages handle this, measured in SharedRules/general.yaml: Hungarian appends nothing (CT: ""), French and German legitimately use "s", and Russian - whose plurals work like Polish - does not rely on a suffix at all, filling in PluralForms instead (метрметра). Polish was in the wrong group.

Why one plural column is not enough

Polish inflects the noun according to the numeral:

numeral form example
1 singular 1 metr
2, 3, 4 nominative plural 2 metry
5 and above genitive plural 5 metrów

and the teens are the exception that breaks the naive rule: 12, 13, 14 metrów (genitive despite ending in 2, 3, 4), likewise 112-114, while 22 and 102 take the nominative again.

This PR therefore adds a second definition section, PluralGenitive, next to PluralForms (which is now the nominative plural). No Rust change is needed - build_values in src/definitions.rs accepts any definition name as a hashmap.

The unit rule (both the bare and the prefixed branch) and the currency rule now read the value of the numeral and pick the section accordingly. When the numeral cannot be determined - a variable coefficient, as in x ms - $Coefficient is 0 and the genitive is used, which is also the form Polish uses for unknown quantities ("x mili-sekund").

Where no inflected form is known, the singular is spoken rather than a guessed ending: for a screen reader user a wrong ending is worse than a missing one.

Three base forms were wrong as well

Plurals were being used as singulars, so the engine said "1 herce" and "1 konie mechaniczne":

Hz  herce             -> herc
hp  konie mechaniczne -> koń mechaniczny
℧   M h o             -> mho

Every other language has a singular here (en hertz, ru герц, fi hertsi, es hercio; hp: ru лошадиная сила, hu lóerő, nb hestekraft; ℧: de/fr mho).

Why this survived 609 passing tests

The existing Polish expectations in tests/Languages/pl/units.rs recorded the broken output, so the suite was green while the speech was wrong - "2 dzieńs", "2 tydzieńs", "kwetta-grams". Expectations harvested from real engine output are a good regression net but prove nothing about correctness unless they are read as a user would hear them. 21 expectations are updated here, each difference reviewed word by word.

Verification

cargo test --test languages Languages::pl        620 passed, 0 failed
reverting general.yaml alone, keeping the new expectations
                                              -> 6 of 6 new tests fail

The second run is the validity control: without it the new tests would not be shown to discriminate.

Both definition sections cover all 131 unit and currency names that the Polish SIUnits, UnitsWithoutPrefixes, EnglishUnits and CurrencySymbols can produce - checked programmatically against those sections, which is how the three broken base forms were found. The forms are written out because Polish inflection cannot be derived from the ending: metr/metry but litr/litry, om/omy, dzień/dni, tydzień/tygodnie, rok/lata. Multi-word units inflect the head noun and keep the dependent word's case ("5 lat świetlnych", "5 minut łuku"). All forms were checked against hunspell pl_PL; the only words it does not know are the binary-prefix neologisms (kibibajt, gibibit, ...) whose singular forms already existed in the file.

New tests in tests/Languages/pl/unit_plurals.rs cover one class of phenomenon each: the three numeral classes including the teens exception, SI base units, the prefixed branch of the rule, irregular nouns, the corrected base forms, and multi-word units.

Not fixed here

Compound units read the second noun in the nominative instead of the accusative: "62 mile na godzina" should be "na godzinę". That predates this branch (git show confirms the old expectation was "62 milas na godzina") and lives in the per-fraction rule, so it is a different class of defect and will be a separate PR.

Based on #741

Rebased onto #741 (pl-verified-keys) rather than main: both branches add a mod line at the same place in tests/Languages/pl.rs, so stacking keeps that trivial conflict out of your way. Please merge #741 first. Test count here (620) includes the 5 number-set tests that #741 adds.

Mechanical change: 2435 text keys raised from t/ot/ct to T/OT/CT in the
two unicode files. No text is touched.

The Polish rules were written before the lowercase/uppercase convention
was in use, so entries that have been translated all along still carried
the "needs review" key. The audit tool therefore reported them as
untranslated, which made the real gaps impossible to see:

  audit-translations pl, untranslated text:  3162 -> 727

The remaining 727 are in the rule files, where the entries need reading
rather than a mechanical pass, so they are deliberately left alone.

Every raised entry was checked to differ from the English source, i.e.
it really is translated. Entries whose text legitimately equals English
(Roman numerals, ligatures, proper names, whitespace) were handled
separately in the previous PR; entries still holding English text are
NOT raised, since that would assert a translation that does not exist.

Verification:

  cargo test --test languages Languages::pl   609 passed, 0 failed
  diff: 2436 insertions, 2436 deletions, zero changes outside key case

The last point is checked by normalising the key case in the diff and
confirming no unpaired lines remain (without normalising, 4710 remain,
so the check does discriminate).
The previous commit raised the key on entries written on a single line
(`- "±": [t: "..."]`), but not on entries whose body is a block:

    - "°":
       - test:
           if: "parent::*[name(.)='msup' ...]"
           then: [t: "stopni"]
           else: [t: "stopień"]

Those are exactly the entries carrying inflection or a TTS condition, so
312 of the most deliberate translations in the file were left looking
unreviewed. This raises them: 64 in unicode.yaml, 247 in unicode-full.yaml
and one single-line entry (U+23E6) missed earlier.

One entry is deliberately left with a lower-case key: `t: ""` for U+00A0,
the else-branch of the `@data-empty-in-2D` test. That is silence rather
than text, and an upper-case key means "a translator checked this
wording" - there is no wording to check. English keeps a lower-case key
there too.

No content changes; only key case. `cargo test --test languages
Languages::pl` gives 609 passed, 0 failed, which is the same result as
before this commit, as expected for a change the engine treats as
equivalent (src/speech.rs matches "t"|"T" in one match arm).
Two loose ends in the Polish localization.

**Rule-file keys.** 248 entries in the rule files still used a lower-case
key while carrying translated Polish text. Content is untouched - every
one of the 248 changed lines differs from its predecessor only in the
case of the key name, verified line by line after normalizing the key.

17 entries are deliberately left lower-case: five are the letters C, N,
Q, R, Z in `dimension-number-sets` (the letter itself is the wording, as
in English); `ct: "s"`/`ct: "-"` in the unit rule are the plural suffix
and prefix separator; `navigate.yaml:176` is a Polish euphony variant
("ze" before "st-"); the remaining seven are XPath expressions rather
than speech.

**Tests for the number-set rules.** These rules had no Polish test at
all, in a file where two silent defects were already found. New
`tests/Languages/pl/number_sets.rs` covers the bare symbols, the
dimension form, both sign variants, and ClearSpeak agreeing with
SimpleSpeak.

The `number_set_with_variable_exponent` case is the negative control:
`dimension-number-sets` fires on `count(*)=2`, so a non-numeric exponent
must fall through to the full phrase ("liczby całkowite do potęgi n")
rather than being read as a dimension. Without it, a rule that swallowed
every superscript would still pass.

Polish keeps the letter with a dimension ("R 3"), matching en, de, fr,
nb, sv, fi and es; Hungarian is the only language that translates in that
position too. Measured with the SAPI voice NVDA actually uses: "R 3" is
byte-identical to a phonetic "er 3" and "N 4" to "en 4", so the letters
are pronounced as their Polish names.

    cargo test --test languages Languages::pl   614 passed, 0 failed
The `unit` rule appended `ct: "s"` whenever a unit had no entry in
`PluralForms`, and the Polish `PluralForms` contained no SI base unit at
all. Measured output of the engine before this change:

    1 metr, 2 metrs, 5 metrs, 21 metrs, 2 grams, 2 sekundas,
    2 ampers, 2 mols, 2 kilo-metrs

So a Polish NVDA user heard an English plural ending on essentially every
quantity. Hungarian avoids this with `CT: ""` (it appends nothing);
French and German legitimately use "s"; Russian, whose plurals work like
Polish, does not rely on a suffix at all and fills in `PluralForms`
instead. Polish was in the wrong group.

Polish does not have one plural form. The noun follows the numeral:

    1 metr | 2, 3, 4 metry (nominative plural) | 5 metrów (genitive plural)

and the teens are the exception that breaks the naive rule: 12, 13, 14
metrów, likewise 112-114, while 22 and 102 take the nominative again.
One extra plural column is therefore not enough, so this adds a second
definition section, `PluralGenitive`, next to `PluralForms` (now the
nominative plural). No Rust change is needed: `build_values` accepts any
definition name as a hashmap.

The `unit` rule (both the bare and the prefixed branch) and the
`currency` rule now read the value of the numeral and pick the section
accordingly. When the numeral cannot be determined - a variable
coefficient, as in "x ms" - `$Coefficient` is 0 and the genitive is
used, which is also what Polish uses for unknown quantities ("x
mili-sekund").

Where no inflected form is known the singular is spoken rather than a
guessed ending: a wrong ending is worse for a screen reader user than a
missing one.

Both sections cover all 131 unit and currency names that the Polish
`SIUnits`, `UnitsWithoutPrefixes`, `EnglishUnits` and `CurrencySymbols`
can produce. The forms are written out because Polish inflection cannot
be derived from the ending: metr/metry but litr/litry, om/omy,
dzień/dni, tydzień/tygodnie, rok/lata. Multi-word units inflect the head
noun and keep the dependent word's case ("5 lat świetlnych", "5 minut
łuku"). They were checked against hunspell pl_PL; the only words it does
not know are the binary-prefix neologisms (kibibajt, gibibit, ...) whose
singular forms already existed in the file.

Three base forms were themselves wrong - plurals used as singulars, so
the engine said "1 herce" and "1 konie mechaniczne":

    Hz  herce             -> herc
    hp  konie mechaniczne -> koń mechaniczny
    ℧   M h o             -> mho

Every other language has a singular here (en hertz, ru герц, fi hertsi,
es hercio; hp: ru "лошадиная сила", hu "lóerő", nb "hestekraft"; ℧:
de/fr "mho").

Why this survived: the existing Polish expectations in
tests/Languages/pl/units.rs recorded the broken output, so 609 tests
passed while the speech was wrong ("2 dzieńs", "2 tydzieńs",
"kwetta-grams"). Expectations harvested from real engine output are a
good regression net but prove nothing about correctness unless they are
read as a user would hear them. 21 test expectations are updated here,
each difference reviewed word by word.

Verification:

    cargo test --test languages Languages::pl   615 passed, 0 failed
    reverting general.yaml alone            ->  6 of 6 new tests fail

The new tests in tests/Languages/pl/unit_plurals.rs cover one class of
phenomenon each: the three numeral classes including the teens
exception, SI base units, the prefixed branch of the rule, irregular
nouns, the corrected base forms, and multi-word units.
@NSoiffer

NSoiffer commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

The wrong plural ending is why checking the speech output in the tests is important. I'm glad you were able to resolve this without requiring Rust code.

@NSoiffer
NSoiffer merged commit df565e2 into daisy:main Sep 3, 2026
10 checks passed
@github-project-automation github-project-automation Bot moved this from Triage to Done in MathCAT Project Board Sep 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants