Repository navigation
Translations
Nextcloud apps get their translations from Transifex through the Nextcloud translation bot, which pushes them into the repository. Joining that sync is a checklist, the translation request at nextcloud/docker-ci, and most of what it asks the repository to carry follows from what ncmake reads anyway: the app id from appinfo/info.xml, the build outputs from .gitignore. Three targets in the translations module cover the repository side and the source strings. Transifex itself is not touched: no API call and no token, the sync stays with the bot.
Tip
TL;DR: make dev-init once per machine, then make l10n-init PR=1 writes l10n/.gitkeep, .l10nignore and .tx/config, covers them in REUSE.toml and opens the pull request. Once it is merged, make l10n-check validates the setup the way the Nextcloud check of the request does, on every branch the bot syncs. Then invite the bot and open the request, as l10n-init prints. Before that, make l10n-lint finds the source strings the translation team would reject, and l10n-lint FIX=1 rewrites the ones that have a fix.
- What l10n-init writes
- The steps outside the repository
- What l10n-check checks
- What l10n-lint finds
- The rule file
- Maintenance lines
- FAQ
make l10n-init writes each of the three files the bot needs that is missing, and leaves every file that already exists as it is:
| File | Content |
|---|---|
l10n/.gitkeep |
empty; the translation tool finds the app by its l10n/ directory |
.l10nignore |
the trees the string extraction must skip: js/ and vendor/ where .gitignore marks them as build output or they exist, vendor-bin/ where it exists |
.tx/config |
the template of the translation request, with the app id as the resource and in the names of the .po and .pot files |
The app id in .tx/config is the one from appinfo/info.xml, not the name of the checkout, because that is the name the translation tool gives the .pot file it writes. A resource or file name that differs from it makes the sync fail.
Where the app has a REUSE.toml, the new files join the annotation that already covers one of the sync files, .tx/config or .tx/backport for example. Without such an annotation they get one of their own, under the copyright and license that REUSE.toml states in its header comment. When the header states neither, l10n-init says so and leaves the annotation to you, so make reuse names what is still missing.
Without flags it only writes the files and prints the git add line for them. COMMIT=1 commits them with -s on the branch ncmake/l10n/init without pushing, and PR=1 also pushes and opens the pull request through gh, as the other writing targets do (Workflows explains the flow).
Two steps of the checklist are settings on GitHub, and l10n-init prints them after the files:
-
Invite the bot. It pushes the translations with its own account, so for an app outside the nextcloud organisation
nextcloud-botneeds write access as a collaborator.l10n-initprints thegh apicommand that sends the invitation. -
Let it past the branch rules. Once it has accepted: the bot pushes its commits directly, without a pull request. Add it to the bypass list of the ruleset that protects
main, mode Exempt. Classic branch protection has no such list, so a branch under it cannot take the bot's pushes; see the Nextcloud documentation of the setup.
Then open the translation request and work through its checklist; make l10n-check before that shows whether the repository side holds.
Before the first sync, Nextcloud runs translations/validateSyncSetup.sh of nextcloud/docker-ci against the repository. That script clones over ssh and cannot run on your machine as it is, so make l10n-check runs its steps instead, on what origin has after a git fetch:
-
The branches are
main,masterand those.tx/backportonmainlists, or without that file the default list of the script, which follows the server'sstable*branches and is read from the script at run time. Each counts as far as origin has it. -
Per branch it checks that
.tx/configandl10n/.gitkeepexist and notes a missing.l10nignore. Then it runs the translation tool'sprepare-source-validationandvalidateTranslationFiles.shover a copy of the branch, in the image the bot itself runs in,ghcr.io/nextcloud/continuous-integration-translations. The validation is the Nextcloud one, not a copy that drifts.
The copy is a full checkout of the branch under build/cache/l10n-check/, because the translation tool writes its source files and translationfiles/ into the app it reads. It is taken through a separate git index, so a file that .gitattributes marks export-ignore is checked too.
On top of the script it reports what makes a sync fail or extract the wrong strings without any message:
- an
.l10nignorethat does not excludejs/,vendor/orvendor-bin/under the same rulel10n-initapplies. A line with a carriage return, trailing blanks or a leading slash is named on its own: the tool compares each line unchanged with paths relative to the app root, so such a line never matches - a
.tx/configwhose resource,file_filterorsource_filedoes not name the app id - a
.tx/backportentry that is not astable*branch, or that origin does not have
Findings are marked [!] for what is worth fixing and [i] for what is worth knowing. A [!] or a failed validation makes the target exit non-zero, once every branch is checked. It writes nothing into the checkout and needs podman or docker; RUNTIME=bare stops with an error, since the validation runs in the image.
Before the first sync, the Nextcloud translation team reviews the English source strings and locks the resource on Transifex until the ones it rejects are fixed. Much of what it rejects is mechanical: a typographic apostrophe, three dots instead of the ellipsis, a contraction, a number without the plural form. make l10n-lint finds those before the review does.
- The strings are the ones the sync sends. The translation tool writes its template in the image the bot runs in, for a copy of the working tree with the tracked and untracked files as git lists them, so a string counts before it is committed.
-
Each finding names the string, the rules it breaks with their reason, the fix where a rule has one, and the file and line. The template points the strings of a
.vuefile and ofinfo.xmlat placeholder files, sol10n-lintlocates every string in the sources itself: as the literal argument of at()orn()call, or as the text of an element ofappinfo/info.xml. A string it cannot locate is reported all the same, with that note. -
FIX=1rewrites every string that has a fix, in the sources, and keeps the line breaks and escapes of the literal.l10n/*.jsonstays as it is: it comes from Transifex, and a changed source string is a new string there anyway. What has no automatic fix, a leading space or a missing plural form, stays a finding.
[!] marks a must rule, which the review rejects, and makes the target exit 1. [i] marks a should rule, a style call. Without FIX=1 it writes nothing into the checkout; the copy stays under build/cache/l10n-lint/. It needs podman or docker.
| Option | Effect |
|---|---|
FIX=1 |
rewrite every string that has a fix, in the sources |
L10N_RULES=<file> |
a rule file of one's own instead of the shipped one |
JSON=<file> |
write the result as JSON to <file> as well |
ARGS=--self-test |
only test the rule file against its examples |
The rules are not code. lib/l10n-rules.json holds every rule as one entry, and the analyser lib/l10n-lint.mjs reads it and carries no rule of its own. The file comes with the analyser through the per-machine cache, so a rule merged in ncmake reaches every app with the next refresh.
Today it holds R1 to R9, the Dos and Don'ts and Correct plurals of the developer manual:
| Rule | Finds | Fix |
|---|---|---|
| R1 |
’ or ´ as an apostrophe inside a word |
' |
| R2 | three dots |
… with a non-breaking space (U+00A0) before it |
| R3 | a plain space before …
|
the non-breaking space |
| R4 |
Don't, Won't
|
Do not, Will not
|
| R5 | Can not |
Cannot |
| R6 |
id, ids, but not the placeholder {id}
|
ID, IDs
|
| R7 | a leading or trailing space, mostly a concatenated string | none, one string with placeholders |
| R8 | a varying number in t(): %d, %n, {count}, {seconds} and the like |
none, n() with %n
|
| R9 |
n() without %n in either form |
none, %n or t()
|
{
"id": "R5",
"weight": "must",
"source": "guidelines",
"note": "Cannot is one word",
"match": "\\b([Cc])an not\\b",
"replace": "$1annot",
"examples": [
{ "bad": "Can not save", "good": "Cannot save" },
{ "bad": "It can not be saved", "good": "It cannot be saved" }
],
"ok": ["Cannot save"]
}| Key | Meaning | |
|---|---|---|
id |
required |
R and a number; a number is never reused |
weight |
required |
must: the review rejects it, [!], exit 1; should: a style call, [i]
|
source |
required | a key of sources at the top of the file, today guidelines and plurals; a new source is one more line there |
note |
required | the reason, printed with every finding |
match |
one of three | a regular expression (JavaScript, with the u flag) that finds the mistake |
replace |
with match
|
the replacement, $1 for a group; without it the rule only reports |
patterns |
one of three | a list of { "match", "replace" } for one rule with several spellings, as in R4 |
require |
one of three | a regular expression the string has to contain; it is found when no form of the string does, as %n in R9 |
calls |
optional |
["t"] or ["n"]: only strings of t(), or only plurals of n(); without it both |
examples |
required | at least one { "bad", "good" }; good is required when the rule has a replacement |
ok |
optional | strings the rule has to leave alone, for the edge cases |
In JSON every backslash of a regular expression is doubled, \b is written "\\b", and a non-breaking space is "\u00a0". The rules run in the order of the file, each on what the fixes before it left: R2 turns Loading... into Loading … with the non-breaking space, so R3 never sees a plain space there.
The file is checked whole before a rule judges an app. An unknown key, a duplicate id or a pattern that does not compile stops the run with the place in the file. Then every rule is held against its own examples:
- every
badstring is found, - with a replacement, the fix of
badgives exactlygood, - neither
goodnor anyokstring is found.
One failing example stops the run and is named. The Smoke workflow of ncmake runs the same self-test on every pull request.
More rules come from the review practice of the translation team. They are collected in ncmake#257, and comments with proposals or corrections are welcome there, one line per rule is enough. A rule can also go straight in as a pull request against lib/l10n-rules.json: the entry at the end with the next free number and its examples, checked in an ncmake checkout with Node 18 or newer:
node lib/l10n-lint.mjs --self-testThe bot syncs a release line named after the app version, stable0.7 for example, only when .tx/backport on main lists it, and that list then replaces the server's branch names. make stable-branch offers the pull request that adds the new branch (Releasing has the details). Once it is merged, make l10n-check checks the new line with the others, and the ruleset bypass for the bot has to cover the new branch as well.
Does ncmake run the sync? No. The bot syncs on Nextcloud's side; ncmake prepares the repository and checks it.
Can I edit the files after l10n-init? Yes. They are ordinary committed files, and l10n-init never overwrites one. l10n-check holds an edited file against the same rules.
My app commits its built js/. Does it still go into .l10nignore? Yes. The extraction reads every file it is not told to skip, the bundle included. That is why js/ counts whether it is git-ignored or committed.
Can I use rules of my own? Yes: make l10n-lint L10N_RULES=<file> reads that file instead of the shipped one, under the same checks. A rule that would help every app is better proposed in ncmake#257.
Why does l10n-check pull an image? The validation runs the translation tool and gettext in the versions the bot uses. The image is pulled and removed like every other ncmake image, see How ncmake understands your app.
© 2026 [ernolf] Raphael Gradenwitz · MIT-licensed · Report an issue
For app developers
- Getting started
- How ncmake understands your app
- Building and packaging
- Bundle analysis
- Build tools
- Checkout audit
- Releasing
- Integrity signature
- Translations
- Per-app tuning
- Target reference
- AI assistant
CI workflows
App Store
For app users
Git
Background
Reference