Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .docker_files/main/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"depends": [
"admin_light_base",
"base_extended_security",
"base_translate_labels",
"lang_fr_activated",
"test_http_request"
],
Expand Down
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ USER odoo

COPY admin_light_base /mnt/extra-addons/admin_light_base
COPY base_extended_security /mnt/extra-addons/base_extended_security
COPY base_translate_labels /mnt/extra-addons/base_translate_labels
COPY lang_fr_activated /mnt/extra-addons/lang_fr_activated
COPY test_http_request /mnt/extra-addons/test_http_request

Expand Down
47 changes: 47 additions & 0 deletions base_translate_labels/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
===============================
Canada French Accounting Labels
===============================

Context
=======
Odoo's default French terms are based on European terminology. In Quebec and Canada, some important accounting terms are significantly different, which can lead to confusion for local users.

The issue with these translations is that they appear hundreds of times in Odoo PO files across different modules. Updating and maintaining each of these translations manually would require a major and constant effort.

Description
===========
This module sanitizes and automatically adapts the labels of the accounting application for the French Canada (fr_CA) language.

Here is an overview of the main terminology changes applied by this module:
* Avoir -> Note de crédit
* Balance âgée -> Âge des comptes
* Lettrage -> Conciliation bancaire

Translations to update can be as specific as the following: "Compte utilisé sur les lignes de taxes des avoirs. Laissez vide pour utiliser le compte de dépenses."

Usage
=====
To use this module and trigger the translation replacements:

1. Navigate to **Accounting / Configuration / CA Accounting Terms**.
2. Add, edit, or remove the specific French to Canadian French term mappings.
3. Click the **"Apply translation"** button located in the header of the list view to apply changes across all installed modules.

Screenshots
===========

Menu Items Adjustments
----------------------
The missing `s` is added to the menu item `Accounting / Vendors / Refund`.

.. image:: static/description/supplier_refund_menu_item_en.png
:alt: Supplier Refund Menu Item EN
:width: 600 px

.. image:: static/description/supplier_refund_menu_item_fr.png
:alt: Supplier Refund Menu Item FR
:width: 600 px

Contributors
============
* Numigi (tm) and all its contributors (https://bit.ly/numigiens)
16 changes: 16 additions & 0 deletions base_translate_labels/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Copyright 2026 Numigi and all its contributors (https://bit.ly/numigiens)
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).


from . import models


def _get_installed_modules(env):
# Fetch all modules that are currently installed in the database
return env["ir.module.module"].search([("state", "=", "installed")])


def post_init_hook(env):
# Force translation reload immediately after the module installation is complete
installed_modules = _get_installed_modules(env)
installed_modules._update_translations(["fr_FR"], True)
22 changes: 22 additions & 0 deletions base_translate_labels/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Copyright 2026-today Numigi
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).

{
"name": "Canada French Labels",
"version": "18.0.1.0.0",
"author": "Numigi",
"maintainer": "Numigi",
"website": "https://www.numigi.com",
"license": "LGPL-3",
"category": "Accounting",
"summary": "Sanitize the accounting terms for Canada French",
"depends": ["base", "lang_fr_activated"],
"data": [
"security/ir.model.access.csv",
# "data/base_translate.csv",
"views/base_translate.xml",
],
"installable": True,
"auto-install": True,
"post_init_hook": "post_init_hook",
}
3 changes: 3 additions & 0 deletions base_translate_labels/data/base_translate.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
id,lang_id,model_id,term,new_term
translate_fr_ca_1,,,poste,job

77 changes: 77 additions & 0 deletions base_translate_labels/i18n/fr.po
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * base_fr_ca_labels
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 18.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-21 12:17+0000\n"
"PO-Revision-Date: 2026-07-21 12:17+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"

#. module: base_fr_ca_labels
#: model_terms:ir.ui.view,arch_db:base_fr_ca_labels.view_account_journal_group_tree
msgid "Appliquer les traductions"
msgstr "Appliquer les traductions"

#. module: base_fr_ca_labels
#: model_terms:ir.ui.view,arch_db:base_fr_ca_labels.view_account_journal_group_tree
msgid "Apply translation"
msgstr "Appliquer la traduction"

#. module: base_fr_ca_labels
#: model:ir.actions.act_window,name:base_fr_ca_labels.action_translate_new_term
#: model:ir.ui.menu,name:base_fr_ca_labels.menu_translate_new_term
msgid "CA Accouting Terms"
msgstr "Termes comptables CA"

#. module: base_fr_ca_labels
#: model:ir.model.fields,field_description:base_fr_ca_labels.field_translate_new_term__create_uid
msgid "Created by"
msgstr "Créé par"

#. module: base_fr_ca_labels
#: model:ir.model.fields,field_description:base_fr_ca_labels.field_translate_new_term__create_date
msgid "Created on"
msgstr "Créé le"

#. module: base_fr_ca_labels
#: model:ir.model.fields,field_description:base_fr_ca_labels.field_translate_new_term__display_name
msgid "Display Name"
msgstr "Nom affiché"

#. module: base_fr_ca_labels
#: model:ir.model.fields,field_description:base_fr_ca_labels.field_translate_new_term__id
msgid "ID"
msgstr "ID"

#. module: base_fr_ca_labels
#: model:ir.model.fields,field_description:base_fr_ca_labels.field_translate_new_term__write_uid
msgid "Last Updated by"
msgstr "Dernière mise à jour par"

#. module: base_fr_ca_labels
#: model:ir.model.fields,field_description:base_fr_ca_labels.field_translate_new_term__write_date
msgid "Last Updated on"
msgstr "Dernière modification le"

#. module: base_fr_ca_labels
#: model:ir.model.fields,field_description:base_fr_ca_labels.field_translate_new_term__new_term
msgid "Term CA"
msgstr "Terme CA"

#. module: base_fr_ca_labels
#: model:ir.model.fields,field_description:base_fr_ca_labels.field_translate_new_term__term
msgid "Term Fr"
msgstr "Terme Fr"

#. module: base_fr_ca_labels
#: model:ir.model,name:base_fr_ca_labels.model_translate_new_term
msgid "Translation Terms (FR to CA)"
msgstr "Termes de traduction (FR vers CA)"
7 changes: 7 additions & 0 deletions base_translate_labels/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Copyright 2026 Numigi and all its contributors (https://bit.ly/numigiens)
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).

from . import base
from . import base_translate
from . import translate

59 changes: 59 additions & 0 deletions base_translate_labels/models/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Copyright 2026 Numigi
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).

from odoo import models


class BaseTranslationCA(models.AbstractModel):
_inherit = "base"

def action_translate_to_ca(self):
mapping_dict = self._get_ca_translation_mapping()
translatable_fields = self._get_translatable_fields()
self._apply_ca_translations_to_records(translatable_fields, mapping_dict)

def _get_ca_translation_mapping(self):
term_model = self.env["base.translate"]
return term_model.get_mapping_for_model(self._name)

def _get_translatable_fields(self):
return [
field_name
for field_name, field_def in self._fields.items()
if field_def.translate
]

def _apply_ca_translations_to_records(self, fields_list, mapping_dict):
for record in self:
record._translate_record_fields(fields_list, mapping_dict)

def _translate_record_fields(self, fields_list, mapping_dict):
for field_name in fields_list:
self._translate_single_field(field_name, mapping_dict)

def _translate_single_field(self, field_name, mapping_dict):
fr_value = self.with_context(lang="fr_FR")[field_name]
if isinstance(fr_value, str):
ca_value = self._replace_terms_in_text(fr_value, mapping_dict)
self._write_ca_value(field_name, ca_value)

def _replace_terms_in_text(self, text_value, mapping_dict):
sorted_keys = sorted(
mapping_dict.keys(),
key=lambda k: len(str(k)),
reverse=True,
)
for old_term in sorted_keys:
text_value = self._replace_single_term(text_value, old_term, mapping_dict)
return text_value

def _replace_single_term(self, text_value, old_term, mapping_dict):
if old_term and str(old_term) in text_value:
text_value = text_value.replace(
str(old_term),
str(mapping_dict[old_term]),
)
return text_value

def _write_ca_value(self, field_name, ca_value):
self.with_context(lang="fr_CA").write({field_name: ca_value})
68 changes: 68 additions & 0 deletions base_translate_labels/models/base_translate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Copyright 2026 Numigi
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).

from odoo import fields, models, api


class BaseTranslate(models.Model):
_name = "base.translate"
_description = "Database Translation Terms (FR to CA)"

lang_id = fields.Many2one(
comodel_name="res.lang",
default=lambda self: self._get_default_lang(),
string="Language",
required=True,
)
model_id = fields.Many2one(
comodel_name="ir.model",
string="Model",
ondelete="cascade",
help="Optional: Leave empty to apply globally (menus, forms, reports).",
)
term = fields.Char(string="Label", required=True)
new_term = fields.Char(string="New Label", required=True)

def _get_default_lang(self):
return self.env["res.lang"].search([("code", "=", "fr_FR")], limit=1)

def action_apply_translations(self):
lang_codes = self._get_active_lang_codes()
installed_modules = self._get_installed_modules()
installed_modules._update_translations(lang_codes, True)
self.env.registry.clear_cache()
return self._build_success_notification()

def _get_active_lang_codes(self):
codes = list(set(self.search([]).mapped("lang_id.code")))
if not codes:
codes = ["fr_FR"]
return codes

def _get_installed_modules(self):
return self.env["ir.module.module"].search([("state", "=", "installed")])

def _build_success_notification(self):
return {
"type": "ir.actions.client",
"tag": "display_notification",
"params": {
"title": "Success",
"message": "Translations applied on all the system",
"type": "success",
"sticky": False,
},
}

@api.model
def get_mapping_for_model(self, model_name):
domain = [
"|",
("model_id", "=", False),
("model_id.model", "=", model_name),
]
records = self.search(domain)
return self._format_mapping_dictionary(records)

def _format_mapping_dictionary(self, records):
return {record.term: record.new_term for record in records}
Loading