details:   https://code.tryton.org/tryton/commit/03757ba2dd9d
branch:    default
user:      Cédric Krier <[email protected]>
date:      Fri Sep 26 08:45:49 2025 +0200
description:
        Return the closest language instance for the code

        When the language code is provided by an external source, a best effort
        strategy must be used to always return a language.

        Closes #14258
diffstat:

 trytond/doc/modules/ir/reference.rst |   2 +-
 trytond/trytond/ir/lang.py           |  32 +++++++++++++++++++++++++++-----
 trytond/trytond/tests/test_ir.py     |  17 +++++++++++++++++
 3 files changed, 45 insertions(+), 6 deletions(-)

diffs (90 lines):

diff -r 8a062bb4f22e -r 03757ba2dd9d trytond/doc/modules/ir/reference.rst
--- a/trytond/doc/modules/ir/reference.rst      Wed Oct 01 10:17:22 2025 +0200
+++ b/trytond/doc/modules/ir/reference.rst      Fri Sep 26 08:45:49 2025 +0200
@@ -54,7 +54,7 @@
 
 .. method:: Language.get([code])
 
-   Returns the language instance for the ``code`` or the
+   Returns the closest language instance for the ``code`` or the
    :attr:`~trytond.transaction.Transaction.language`.
 
 .. method:: Language.format(percent, value[, grouping[, monetary[, 
\*\*additional]]])
diff -r 8a062bb4f22e -r 03757ba2dd9d trytond/trytond/ir/lang.py
--- a/trytond/trytond/ir/lang.py        Wed Oct 01 10:17:22 2025 +0200
+++ b/trytond/trytond/ir/lang.py        Fri Sep 26 08:45:49 2025 +0200
@@ -423,20 +423,42 @@
         cls._lang_cache.clear()
         Translation._get_language_cache.clear()
         _parents.clear()
-        if mode in {'write', 'delete'}:
+        if mode in {'create', 'write', 'delete'}:
             cls._code_cache.clear()
 
     @classmethod
     @inactive_records
     def get(cls, code=None):
         "Return language instance for the code or the transaction language"
+        pool = Pool()
+        Configuration = pool.get('ir.configuration')
+
+        def languages(code):
+            for sep in ['@', '_']:
+                if sep in code:
+                    code, _ = code.rsplit(sep, 1)
+                    if code:
+                        yield code
+                        yield from languages(code)
         if code is None:
             code = Transaction().language
+        code = code.replace('-', '_')
         lang_id = cls._code_cache.get(code)
-        if not lang_id:
-            lang, = cls.search([
-                    ('code', '=', code),
-                    ])
+        if lang_id is None:
+            try:
+                lang, = cls.search([
+                        ('code', '=', code),
+                        ])
+            except ValueError:
+                for parent_code in languages(code):
+                    if lang := cls.get(parent_code):
+                        break
+                else:
+                    default_code = Configuration.get_language()
+                    if default_code != code:
+                        lang = cls.get(default_code)
+                    else:
+                        raise
             cls._code_cache.set(code, lang.id)
         else:
             lang = cls(lang_id)
diff -r 8a062bb4f22e -r 03757ba2dd9d trytond/trytond/tests/test_ir.py
--- a/trytond/trytond/tests/test_ir.py  Wed Oct 01 10:17:22 2025 +0200
+++ b/trytond/trytond/tests/test_ir.py  Fri Sep 26 08:45:49 2025 +0200
@@ -347,6 +347,23 @@
         Model.global_search('User', 10)
 
     @with_transaction()
+    def test_lang_get_subtags(self):
+        "Test Lang.get with subtags"
+        pool = Pool()
+        Lang = pool.get('ir.lang')
+
+        self.assertEqual(Lang.get('fr_CA').code, 'fr')
+        self.assertEqual(Lang.get('fr-BE').code, 'fr')
+
+    @with_transaction()
+    def test_lang_get_unknown(self):
+        "Test Lang.get with unknown language"
+        pool = Pool()
+        Lang = pool.get('ir.lang')
+
+        self.assertEqual(Lang.get('foo').code, 'en')
+
+    @with_transaction()
     def test_lang_currency(self):
         "Test Lang.currency"
         pool = Pool()

Reply via email to