Skip to content

fix: make string plural-compatible for non-English langs + add translator hints - #90

Open
emberfiend wants to merge 1 commit into
mainfrom
fix/string-plurals
Open

fix: make string plural-compatible for non-English langs + add translator hints#90
emberfiend wants to merge 1 commit into
mainfrom
fix/string-plurals

Conversation

@emberfiend

Copy link
Copy Markdown

fix: make string plural-compatible for non-English langs + add translator hints

…ator hints

Signed-off-by: Andrew Backhouse <andrew.backhouse@nextcloud.com>
@emberfiend
emberfiend requested a review from moodyjmz July 30, 2026 15:49
@moodyjmz

moodyjmz commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Reviewed this — the direction is right and the translator-hints half works exactly as intended. There's one blocking bug, which is why test-unit is red, plus a translation cost worth stating in the description.

TL;DR

  1. Blocker, one-line fix: n isn't imported, so the render function throws and the whole activeCreator file-list section fails to render.
    -import { translate as t } from '@nextcloud/l10n'
    +import { translate as t, translatePlural as n } from '@nextcloud/l10n'
  2. Switching this string from singular to plural orphans its existing translations in 21 languages, and they'll fall back to English until Transifex round-trips. Defensible, but worth saying in the PR body.
  3. Since both English forms are identical, translators get no signal about which is which. Distinct forms with %n cost nothing and communicate intent.
  4. The <!-- TRANSLATORS --> comments do get extracted — I verified this, no change needed. Details below.
1. Why the render throws (blocker)

src/views/OfficeOverview.vue:9 imports only translate as t, and translatePlural isn't imported anywhere in the repo. There's no global fallback either:

  • src/main.ts is createApp(App).mount('#office') — no app.use(), no plugin
  • no globalProperties or mixin anywhere in src/
  • @nextcloud/vue 9 is consumed via per-component deep imports (@nextcloud/vue/components/NcButton), so no global mixin is installed
  • Vue 3.5 emits no with(this), so the server's window.n is unreachable from a template

Compiling the SFC with the repo's own @vue/compiler-sfc shows the asymmetry directly — t binds to the setup scope, n doesn't:

_toDisplayString($setup.t('office', 'Recent {category}', …))
_toDisplayString(_ctx.n('office', '{count} found in {category}', …))

_ctx.n resolves to undefined, hence TypeError: _ctx.n is not a function at line 255. Because the throw happens during render, the failure isn't confined to the sr-only div — the entire v-else-if="activeCreator" section goes with it. That's the 14 failures in test-unit.

Worth noting @nextcloud/l10n's README gives that exact import line and explains the t/n aliases are required so string extraction doesn't break — so the alias naming here was already correct, it just needs the import.

Also: this is precisely the class of bug #49 exists to catch. eslint can't see it (no vue/no-undef-properties, and it isn't in @nextcloud's config) and there's no vue-tsc script, so lint and build both pass. vue-tsc --noEmit would flag it. Another point in #49's favour.

2. The translation cost (undisclosed)

{count} found in {category} is already translated in 21 languages (42 files counting the .js pairs) — e.g. l10n/de.json has "{count} gefunden in {category}" as a plain string.

Changing a singular msgid into a plural msgid pair makes the old source string obsolete on Transifex, and the new key starts empty. translationtool.php:165-167 then drops the string from the shipped bundle entirely unless every plural form for that language is filled (continue 2), so it isn't a graceful per-language degradation — those 20 languages show raw English for this string until translators fill the new forms.

That's a reasonable trade for getting the grammar right, but it's the sort of thing that should be in the PR description so nobody's surprised by it post-merge.

3. Give the English forms distinct text

Identical singular and plural msgids are legal and the l10n README even demonstrates it (n('%n Mississippi', '%n Mississippi', 3)), so this isn't broken. But a translator opening the pair sees the same string twice and has no signal about which slot is which — which undercuts the point of the change for exactly the languages it's meant to help.

n('office', '%n file found in {category}', '%n files found in {category}',
  files.length, { category: activeCategoryName })

%n is the Nextcloud convention for the count (see README examples), and it lets you drop count from the vars object — currently files.length is passed twice, once as the count argument and once as vars.count. I ran this form through translationtool.phar and it extracts cleanly:

msgid "%n file found in {category}"
msgid_plural "%n files found in {category}"

Argument order in the current call is correct, for what it's worth: translatePlural(app, singular, plural, number, vars?).

4. The TRANSLATORS comments work — no change needed

I assumed HTML comments in a Vue <template> wouldn't survive extraction, and that was wrong. translationtool.php's getTranslatorHintWithVueSource() explicitly supports the <!-- TRANSLATORS: … --> form and rewrites it to a // comment before xgettext --add-comments=TRANSLATORS runs. Both of your comments are on the line immediately above their call, which is what the extractor requires.

Generated the POT against both HEAD^ and HEAD to confirm:

#. TRANSLATORS number of Documents/Spreadsheets/Presentations/Diagrams found (src/views/OfficeOverview.vue:255)
#. TRANSLATORS category is Documents/Spreadsheets/Presentations/Diagrams (src/views/OfficeOverview.vue:261)

Both land. Also checked that the comment nodes surviving into the dev-build DOM don't affect the role="status" announcement — comment nodes aren't exposed to assistive tech.

Happy to push the one-line import fix if that's easier than a round trip.

Tangentially: the Recent {category} hint on line 261 looks aimed at #45, and while digging into this I found that bug isn't a translation problem — I'll write it up on #45 itself rather than derail this thread.

(Review assisted by Claude Code; findings verified against source, a POT generated with the real translationtool.phar, and the failure reproduced under the repo's own vitest config.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants