PMIS Translation Overrides
Maintain translations for multiple languages for Odoo 18 / OCB in one repository.
Slovenian (sl) and Italian (it) are configured initially. Edit complete
PO catalogs; the tooling reconciles upstream updates, generates sparse overrides,
and distributes them to the original modules' i18n_extra directories. No wrapper
addons or changes to an upstream Python/XML source are needed.
Structure
i18n-extra.json Source checkout, addon paths, languages, module selection
translations/
upstream/<module>/ Shared template.pot and per-language PO snapshots
working/<module>/<lang>.po Complete catalogs to edit (sl.po, it.po, ...)
state.json Upstream revision and content fingerprints
report.json Conflicts and retired overrides
generated/<module>/<lang>.po Generated sparse overrides (committed)
generated/manifest.json Generated file hashes and ownership
generated/build.json Portable build input fingerprints
tools/i18n_extra.py Discovery, synchronization, build, deployment
tools/i18n-extra Shell command launcher
tools/completion/ Bash tab completion
tests/ Synthetic catalog and Git-repository tests
LICENSE contains this repository's existing license. Upstream catalog snapshots
originate from the selected OCB checkout; retain its applicable licensing terms.
Requirements and configuration
Use Python 3.12 or newer, polib from requirements.txt, Git, and GNU gettext's
msgfmt for building. Deployment needs Python, polib, and Git, but does not
run gettext. The existing PMIS Odoo interpreter already supplies polib; no Odoo
import, server, database connection, or additional Python package is needed here.
The commands below run from this repository and use the local suite interpreter:
I18N_PYTHON=/srv/pmiverse/_pmisuite18/venv/bin/python
"$I18N_PYTHON" tools/i18n_extra.py discover
The checked-in configuration selects base, web, and sale as a starting set.
Change modules to ["*"] to discover every addon in the configured addon paths,
or supply a list of module technical names. Module selection is configuration,
not a separate setup step per module. Installed modules are not queried from a
database in this version.
source is relative to this repository; addon_paths are relative to that Git
checkout. Both odoo/addons and addons are included. Duplicate module names are
rejected instead of silently choosing one.
Configure languages as a list in i18n-extra.json:
{
"languages": ["sl", "it"]
}
All commands process every configured language for every selected module. To add
another language, append its PO language code and run sync; existing corrections
are retained. If an upstream PO does not exist, the new working catalog starts with
untranslated template entries. Language codes must be distinct; their list order
does not affect the result. The original single-language state is upgraded by
sync without moving existing PO files.
Removing a language from configuration preserves its working catalogs and snapshots.
After sync, build retires its generated files and deploy previews removal of
its owned deployed files. Re-adding the language resumes from its preserved baseline.
Database retirement still requires the reconciliation described below.
The defaults sl and it match upstream Slovenian and Italian catalogs. If deployment
uses a more specific locale with its own upstream PO, configure that locale, for
example sl_SI: the tool merges that language's base and locale translations and
writes the override under the specific locale. Spanish locales also use Odoo's
es_419 fallback where applicable. Odoo can otherwise load a specific-locale
catalog after a base-language override.
Bash shortcut and tab completion
The i18n-extra launcher and Bash completion follow the _pmiconfig shortcut
pattern. From this repository, enable them for the current shell:
export I18N_PYTHON=/srv/pmiverse/_pmisuite18/venv/bin/python
export PATH="$PWD/tools:$PATH"
source /usr/share/bash-completion/bash_completion
source tools/completion/i18n-extra.bash
This requires the bash-completion package. If it is already loaded, only the
repository completion file needs sourcing. To enable this in future terminals,
place the corresponding commands in your Bash profile using absolute repository
paths instead of $PWD and relative paths. No shell profile or system files are
modified by this repository's tools.
i18n-extra sy<TAB> → sync
i18n-extra deploy --a<TAB> → --apply
i18n-extra --root <TAB> → directories
Completion offers the four subcommands, their supported flags, and directory paths
for --root (including --root=...). Root options precede the subcommand. Completing
an argument does not run Python, inspect catalogs, or perform deployment operations.
The launcher uses the exported I18N_PYTHON executable, or python3 from PATH
when unset; Python 3.12 or newer and polib are still required. It works from any
directory, including when symlinked into a directory on PATH. Completion supports
the shortcut and direct tools/i18n-extra invocation. Existing explicit Python
commands remain available.
The launcher and every subcommand accept --help or -h:
i18n-extra --help
i18n-extra discover --help
i18n-extra sync --help
i18n-extra build --help
i18n-extra deploy --help
Help includes command descriptions, supported options, and examples. It does not load project configuration or perform synchronization, generation, or deployment.
Initial setup and editing
"$I18N_PYTHON" tools/i18n_extra.py sync
This creates full editable catalogs in translations/working. They include
existing upstream translations and untranslated template entries. Edit those
files with a PO editor, for example translations/working/sale/sl.po or
translations/working/sale/it.po. Keep source text, Odoo extraction comments, and references
intact; the tool gets deployment references from the upstream snapshots.
sync reads only checked-in, unmodified source catalogs. It never pulls Git,
extracts translations, or writes to OCB. Missing templates fail with the affected
path before any catalog writes; provide a current upstream template or explicitly
narrow module selection. A checked-in POT is the source of truth for this version:
the tool cannot prove that it reflects all source-code changes. Regenerating stale
templates requires a separately authorized Odoo extraction workflow.
Upstream snapshots are normalized to remove translator identities and unrelated header churn. Source byte hashes still record the exact input files used. Commit the baseline, working catalogs, state, and report together after review; the tools never stage, commit, or push changes.
After an upstream update
Run these steps after your existing deployment process updates the OCB checkout:
"$I18N_PYTHON" tools/i18n_extra.py sync
"$I18N_PYTHON" tools/i18n_extra.py build
"$I18N_PYTHON" tools/i18n_extra.py deploy
Synchronization compares the previous upstream snapshot, your working catalog, and the current upstream catalog:
| Change | Result |
|---|---|
| No local correction | Adopt the upstream translation |
| Local correction, unchanged upstream text | Preserve the correction and refresh references |
| Upstream now matches the correction | Stop generating that override |
| Both translations changed differently | Preserve the correction, mark fuzzy, report both values |
| Source message disappears or changes | Preserve the old correction as obsolete; use the new upstream entry |
| Retired source reappears | Restore the correction as fuzzy for explicit review |
| Module deselected | Preserve its working files; retire its deployed overrides |
| Language deselected | Preserve its working files; retire only that language's deployed overrides |
There are no automatic fuzzy matches or machine translations. Resolve competing
translations in the working PO and clear the fuzzy flag after review. Conflicts
remain flagged across repeated synchronization until resolved. Obsolete corrections
remain available for reference and are excluded from generated output. New
untranslated entries do not block a build. The report groups results by module and
language. Any unresolved conflict blocks the complete build/deployment before output
writes; corrections in other languages remain intact.
build refuses unresolved fuzzy entries, changed source keys, empty overrides,
missing Odoo references, invalid percent placeholders, and gettext format errors.
Gettext plural/context overrides are explicitly rejected because this tool targets
Odoo 18's translation loader. It checks only your overrides, not the linguistic
quality or format correctness of unchanged upstream translations. HTML/XML content
and actual rendered behavior still need application acceptance.
Generated output contains only translated entries that differ from upstream. Unchanged outputs are not rewritten. Changes to configured languages or upstream catalog hashes require synchronization before build or deployment; unrelated upstream commits do not.
Commit translations/generated/ together with the working catalogs, snapshots,
state, and report after a successful build. Include both manifest.json and
build.json, even when there are no overrides. They record artifact hashes and
portable input fingerprints; do not edit generated files by hand.
After updating this repository and the corresponding OCB checkout on a remote instance, the only translation file preparation command needed there is:
i18n-extra deploy --apply
No remote sync or build is needed. The configured source path must resolve to
the target checkout, whose selected upstream catalogs must match the synchronized
baseline. If they differ, synchronize and build on the authoring instance and
publish the updated files first. Database activation remains a separate step.
Deployment ownership
deploy is a read-only preview. To write the reviewed deployment:
"$I18N_PYTHON" tools/i18n_extra.py deploy --apply
Deployment verifies build input fingerprints, the manifest, and generated file
hashes, then copies the exact built bytes without regenerating or running gettext.
Missing or changed artifacts and stale working catalogs block deployment; run
build and publish the outputs first. It writes the configured source checkout only;
deployments elsewhere should run this repository's tooling alongside that checkout
with the corresponding configuration.
The tool refuses tracked upstream destinations, existing unowned files, symlinks,
and modifications to files from its previous deployment. Existing upstream
i18n_extra files are not merged or taken over. A local receipt under .cache/
records hashes of files the tool owns. Keep that receipt: without it, existing files
are deliberately treated as unowned. Retired files are removed only when their
bytes still match the receipt. Unselected working catalogs are preserved.
When overrides are present, deploy --apply adds **/i18n_extra/ once to the
source repository's local Git info/exclude file. Existing rules and comments
are preserved; upstream .gitignore and global Git settings are never changed.
Git resolves the metadata location, so linked worktrees use their shared exclude
file. The JSON git_exclude field previews append or unchanged with the path
and rule; preview mode writes nothing. It is null when no overrides remain.
The rule stays in place after retirement and covers all untracked i18n_extra
directories in that repository, not only files owned by this tool. Higher-priority
.gitignore rules can override local excludes. Ignoring a file does not protect
it from future upstream ownership; tracked and unowned destination checks still
apply. Symlinked excludes and detected concurrent edits are refused. An exclusion
write failure prevents PO deployment; if a later PO write fails, the rule may
already be present and can safely remain for the next run.
Run one synchronization/deployment process per project and avoid concurrent PO edits
during synchronization. File writes are atomic individually, not a transaction over
the entire checkout.
Maintainer: apply translation updates from the command line
No Odoo web login is required. Use the server's existing maintenance account, suite interpreter, configuration, and database list. Start with the normal backup and maintenance window. This workflow treats addon PO files and this repository's overrides as authoritative, so it can replace translations edited in a database.
1. Update repositories and deploy translations
On the authoring instance, synchronize, review corrections, and build:
"$I18N_PYTHON" tools/i18n_extra.py sync
"$I18N_PYTHON" tools/i18n_extra.py build
Commit and publish the synchronized catalogs and complete translations/generated/
directory together. After the usual repository updates on the target instance,
run from this repository before upgrading the databases:
"$I18N_PYTHON" tools/i18n_extra.py deploy --apply
Resolve reported conflicts before building. Keep the previous deployed PO files in the release backup when retiring corrections.
2. Include translations in the usual database upgrade
Add --i18n-overwrite to the existing -u all command. For example, set these
values for the target instance and repeat the upgrade for each maintained database:
I18N_ODOO_BIN=/srv/pmiverse/_pmisuite18/repos/OCB/odoo-bin
I18N_CONFIG=/path/to/odoo.conf
I18N_DATABASE=example_database
"$I18N_PYTHON" "$I18N_ODOO_BIN" \
-c "$I18N_CONFIG" -d "$I18N_DATABASE" \
-u all --i18n-overwrite --stop-after-init --no-http
Odoo upgrades the installed modules and loads their normal and i18n_extra
translations for installed languages. Plain -u all does not overwrite existing
translations by default. No separate per-module PO import is needed.
For a database that also needs Slovenian and Italian activated, add
--load-language=sl_SI,it_IT to that command. These are database language codes;
the corresponding PO filenames are sl.po and it.po. Do not activate additional
languages on databases that do not need them.
3. Restart the serving workers
Check each upgrade's exit status and log, then restart the Odoo service through the usual server maintenance procedure. The upgrade updates database terms; restarting the serving workers refreshes Python/JavaScript translation caches. Users may need to refresh their browsers.
Reconciliation: only when an old correction is not restored
The upgrade also restores removed overrides when upstream provides a nonempty
translation and the target record allows overwrite. It cannot clear an old value
using an empty upstream translation, and ordinary overwrite preserves noupdate
records.
For those exceptions, the translation maintainer should provide a reviewed,
targeted restoration PO for the affected database and language. It must contain
only the affected current references, with msgstr set to the desired upstream
text, or to msgid to display the source text. Empty msgstr values do nothing.
The same method can apply a reviewed correction to a protected record.
Database contents can be obtained without a web login. For example, export the
current Slovenian translations for sale to prepare or verify that restoration:
"$I18N_PYTHON" "$I18N_ODOO_BIN" \
-c "$I18N_CONFIG" -d "$I18N_DATABASE" \
--i18n-export=/path/to/sale-current-sl.po --modules=sale --language=sl_SI
Compare affected entries with the previous deployed corrections before replacing them; independently changed values and renamed references need a translation maintainer's decision. Keep database exports outside Git.
After the upgrade, apply the reviewed file to the intended database from the shell:
export I18N_RESTORE_PO=/path/to/reviewed-restoration-sl.po
export I18N_RESTORE_LANG=sl_SI
"$I18N_PYTHON" "$I18N_ODOO_BIN" shell \
-c "$I18N_CONFIG" -d "$I18N_DATABASE" --no-http <<'PY'
import os
from odoo.tools.translate import TranslationImporter
language = os.environ["I18N_RESTORE_LANG"]
if not env["res.lang"].search_count([("code", "=", language), ("active", "=", True)]):
raise ValueError(f"Language must be active: {language}")
importer = TranslationImporter(env.cr)
with open(os.environ["I18N_RESTORE_PO"], "rb") as translation_file:
importer.load(translation_file, "po", language)
importer.save(overwrite=True, force_overwrite=True)
env.cr.commit()
PY
Use it_IT for an Italian restoration. This deliberately bypasses noupdate for
the supplied entries, so use only the reviewed sparse file, never a full export or
upstream catalog. Missing references are not repaired by forcing the import.
Re-export the affected terms to verify the result, then complete the worker restart
from step 3. Keep restoration files outside working and deployed catalogs.
Current limitation: database upgrades activate translations from the command line, but this repository does not yet generate retirement/restoration plans. Exceptional reconciliation still needs the targeted file described above. These instructions follow the local Odoo 18 implementation; no database operations were executed as part of this documentation change.
Validation
"$I18N_PYTHON" -m unittest discover -s tests -v
bash tests/test_completion.sh
Tests use synthetic temporary Git repositories and catalogs. They cover three-way updates, persistent conflicts, obsolete entries, placeholder validation, locale fallback, language isolation/addition/retirement, single-language state migration, deployment preview/application/retirement, and preservation of unowned, tracked, modified, and symlinked destinations. They do not use an Odoo database.
Review
TODO
- Generate targeted restoration plans for retired overrides during database activation.