Skip to content

pl: speak the unit after "na" in the accusative, not the nominative - #745

Merged
NSoiffer merged 6 commits into
daisy:mainfrom
michaldziwisz:pl-preposition-case
Sep 3, 2026
Merged

pl: speak the unit after "na" in the accusative, not the nominative#745
NSoiffer merged 6 commits into
daisy:mainfrom
michaldziwisz:pl-preposition-case

Conversation

@michaldziwisz

@michaldziwisz michaldziwisz commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

The per-fraction rule speaks the denominator of a unit fraction after the preposition "na", which in Polish governs the accusative case. The unit was inserted in the nominative, so the engine said:

62 mile na godzina              should be: na godzinę
5 metrów na sekunda             should be: na sekundę
3 galony na mila                should be: na milę
3 metry na sekunda do kwadratu  should be: na sekundę do kwadratu

Scope is narrower than it looks

Only feminine nouns change form here. For masculine inanimate and neuter names the accusative already equals the nominative, so 3 gramy na litr and 1 dżul na mol were already correct and are untouched. That narrows the work to 32 of the 131 unit and currency names, which is why the new AccusativeCase section deliberately holds only those - a missing entry means "the form does not change".

Why this is in the unit rule, not in per-fraction

Russian has exactly this problem and solves it inside per-fraction by testing the unit symbol and writing the inflected word inline (милю), which works because only one word is affected there. Polish would need 32 such cases in the rule body. Instead the unit rule asks whether it is currently being spoken as a fraction denominator, so the choice is made once, in one place, for every name.

The denominator can also be raised to a power, as in m/s², and then the unit's parent is the power element rather than the fraction itself, so IsFractionDenominator tests both shapes.

That second shape is worth a note, because it is a measurement lesson rather than a coding one: after the first fix the existing expectation "3 metry na sekunda do kwadratu" was still passing green. The list of failing tests only shows the cases whose output actually changed, so a case the fix never reached stays silent. Searching every Polish expectation for a nominative after "na" is what surfaced it - not the test run.

An adjective agrees with its noun (na stopę kwadratową) while a dependent noun keeps its own case (na minutę łuku, na uncję płynu), so the forms are written out rather than derived from the ending.

Verification

cargo test --test languages Languages::pl        625 passed, 0 failed

reverting general.yaml alone, keeping the new data and expectations:
  per_fraction_feminine_denominator          FAILED
  per_fraction_denominator_with_power        FAILED
  per_fraction_multiword_denominator         FAILED
  per_fraction_masculine_denominator_unchanged   ok   <- negative control
  standalone_unit_stays_nominative               ok   <- negative control

The two controls that stay green under a reverted rule are the point: they assert that masculine and neuter denominators must not change, and that a unit standing on its own stays in the nominative, so the accusative cannot leak outside the fraction denominator.

Based on #744

This branch sits on top of #744, which in turn sits on #741 — merge order is #741, #744, then this one. The stack is deliberate: all three touch adjacent lines of Rules/Languages/pl/definitions.yaml and each adds a mod line at the same place in tests/Languages/pl.rs, so building them independently would hand you three trivial conflicts. Already rebased once when #741 grew; I will do it again if anything moves. Test count (625) includes the tests added by both PRs below.

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.
The `per-fraction` rule speaks the denominator of a unit fraction after
the preposition "na", which in Polish governs the accusative case. The
unit was inserted in the nominative, so the engine said:

    62 mile na godzina        (should be "na godzinę")
    5 metrów na sekunda       (should be "na sekundę")
    3 galony na mila          (should be "na milę")
    3 metry na sekunda do kwadratu

Only feminine nouns change form here. For masculine inanimate and neuter
names the accusative already equals the nominative, so "3 gramy na litr"
and "1 dżul na mol" were correct and stay untouched. That narrows the
work to 32 of the 131 unit and currency names, which is why
`AccusativeCase` deliberately holds only those - a missing entry means
"the form does not change".

Why this lives in the `unit` rule rather than in `per-fraction`: Russian
has the same problem and solves it inside `per-fraction` by testing the
unit symbol and writing the inflected word inline (`милю`), which works
because only one word is affected. Polish would need 32 such cases in
the rule. Instead the `unit` rule asks whether it is being spoken as a
fraction denominator, so the choice is made once, in one place, for every
name.

The denominator can also be raised to a power, as in m/s², and then the
unit's parent is the `power` element rather than the fraction itself, so
`IsFractionDenominator` tests both shapes. That second case was found
only after the first fix: the existing expectation "3 metry na sekunda do
kwadratu" was still passing green, because the list of failing tests
shows only the cases whose output actually changed. Searching every
Polish expectation for a nominative after "na" is what surfaced it.

An adjective agrees with its noun ("na stopę kwadratową") while a
dependent noun keeps its own case ("na minutę łuku", "na uncję płynu"),
so the forms are written out rather than derived.

Verification:

    cargo test --test languages Languages::pl   620 passed, 0 failed

New tests in tests/Languages/pl/preposition_case.rs include two negative
controls: masculine and neuter denominators must NOT change, and a unit
standing on its own must stay in the nominative, so the accusative cannot
leak outside the fraction denominator.
@NSoiffer
NSoiffer merged commit 9f4721a 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