Skip to content

Document the l10n conventions (imports, %n, TRANSLATORS placement, composed strings) #96

Description

@moodyjmz

There's no l10n guidance anywhere in this repo — no CONTRIBUTING.md, nothing in AGENTS.md, and README.md mentions @nextcloud/l10n only in passing. The conventions are all discoverable, but only if you already know where to look, and we've now hit three separate l10n problems in the same file because of it.

TL;DR — add a short l10n section (in AGENTS.md, CONTRIBUTING.md, or docs/) covering the import aliases, %n for counts, TRANSLATORS comment placement, and the rule against interpolating nouns into phrases.

Evidence: three issues in OfficeOverview.vue alone
  1. fix: make string plural-compatible for non-English langs + add translator hints #90 — used n(…) without importing translatePlural as n, so the render function threw and 14 tests failed. @nextcloud/l10n's README gives the exact import line and explains why the t/n aliases specifically are required, but nothing in this repo points at it.
  2. fix: make string plural-compatible for non-English langs + add translator hints #90 again — identical English singular and plural forms, and {count} rather than the conventional %n, with the count passed twice (as the count argument and in vars).
  3. 'Neueste Textdokument' is not valid German #45 — user-visible broken German (Neueste Textdokument). To be clear, this one is not a translation-convention mistake: the root cause is that getTemplates() fetches the generic apps/files/api/v1/templates endpoint unfiltered, so the Text app's markdown creator (German label Textdokument) appears in the Office overview and falls through categoryName() to creator.label. It's listed here only because a composed string is what turned an unrelated data-scoping bug into a grammar error in front of a user — which is the argument for writing the composed-string guidance down.

None of these are unreasonable mistakes. They're "didn't know the convention" mistakes, which is what documentation is for.

Suggested content

Imports. Always import { translate as t, translatePlural as n } from '@nextcloud/l10n'. The aliases are not cosmetic — the extraction scripts look for t( and n(, so renaming them breaks string extraction. There is no global t/n in this app: main.ts installs no plugin and @nextcloud/vue is consumed via per-component deep imports, so nothing is provided on the component context.

Counts. Use %n for the number, not a {count} placeholder, and don't also pass it in vars:

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

Give singular and plural distinct English text. Identical forms are legal but leave translators with no signal about which slot is which, which defeats the purpose for languages with more than two plural forms.

Translator hints. <!-- TRANSLATORS: … --> works inside a Vue <template>, and // TRANSLATORS: … in script. It must sit on the line immediately above the call — the extractor only checks the current and preceding line.

Be careful interpolating nouns into phrases. Recent {category} composes correctly in German with the app's own category names (DocumentsDokumente gives Neueste Dokumente), but only as long as every possible value of {category} is a plural noun that fits the surrounding phrase. categoryName() falls back to a server-supplied creator.label when no mimetype matches, and that value isn't under this app's control or translated by this app — so an unrelated bug upstream surfaces as broken grammar. Either constrain what can be interpolated, or use complete per-category strings so each language translates a whole phrase. (#45 is what this looks like in practice, though its actual fix is scoping the creator list, not the string.)

Converting an existing string to plural has a cost. It orphans the existing translations, and translationtool.php drops a string from the shipped bundle unless every plural form for that language is filled — so affected languages fall back to English until Transifex round-trips. Worth calling out in the PR description when you do it.

Related

Docs alone won't catch mistakes, so #49 is the complementary half of this: guidance for the conventions a linter can't check, a typecheck gate for the ones it can.

Metadata

Metadata

Assignees

No one assigned

    Labels

    0. Needs triagePending approval or rejectiondocumentationImprovements or additions to documentation

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions