Support plural translation of messages

This commit is contained in:
Cédric Krier
2024-10-16 19:57:43 +02:00
parent 851d0b580a
commit ffcf5a5225
8 changed files with 213 additions and 14 deletions
+1
View File
@@ -1,3 +1,4 @@
* Support plural translation of messages
* Support plural translations in report
* Add plural source to translations
* Add plural to language
+12 -2
View File
@@ -16,5 +16,15 @@ Internationalization
.. function:: lazy_gettext(message_id, [language[, \**variables]])
Return a LazyString that will be translated with gettext later when actually
used.
Return a LazyString that will be translated with :meth:`gettext` later when
actually used.
.. function:: ngettext(message_id, n, [language[, \**variables]])
Like :meth:`gettext` but for plural message which depend on ``n`` the number
of count.
.. function:: lazy_ngettext(message_id, n, [language[, \**variables]])
Return a LazyString that will be translated with :meth:`ngettext` later when
actually used.
+24 -5
View File
@@ -4,9 +4,10 @@ from trytond.pool import Pool
from trytond.tools.string_ import LazyString
from trytond.transaction import Transaction
_none = object()
def gettext(message_id, *args, **variables):
"Returns the message translated into language"
def _ngettext(message_id, *args, **variables):
if not Transaction().database:
return message_id
pool = Pool()
@@ -14,10 +15,11 @@ def gettext(message_id, *args, **variables):
Message = pool.get('ir.message')
except KeyError:
return message_id
if not args:
if len(args) < 2:
n, = args
language = Transaction().language
else:
language, = args
n, language = args
try:
module, id_ = message_id.split('.')
except ValueError:
@@ -25,13 +27,30 @@ def gettext(message_id, *args, **variables):
raise
return message_id
try:
return Message.gettext(module, id_, language, **variables)
if n is _none:
return Message.gettext(module, id_, language, **variables)
else:
return Message.ngettext(module, id_, n, language, **variables)
except (KeyError, ValueError):
if pool.test:
raise
return message_id
def gettext(message_id, *args, **variables):
"Returns the message translated into language"
return _ngettext(message_id, _none, *args, **variables)
def lazy_gettext(message_id, *args, **variables):
"Like gettext but the string returned is lazy"
return LazyString(gettext, message_id, *args, **variables)
def ngettext(message_id, n, *args, **variables):
return _ngettext(message_id, n, *args, **variables)
def lazy_ngettext(message_id, n, *args, **variables):
"Like ngettext but the string returned is lazy"
return LazyString(ngettext, message_id, n, *args, **variables)
+121 -5
View File
@@ -1,17 +1,24 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from collections import defaultdict
from trytond.cache import Cache
from trytond.model import ModelSQL, ModelView, fields
from trytond.pool import Pool
from trytond.tools import grouped_slice
from trytond.transaction import Transaction
from .lang import get_parent_language as get_parent
from .translation import INTERNAL_LANG
class Message(ModelSQL, ModelView):
__name__ = "ir.message"
_message_cache = Cache('ir.message', context=False)
text = fields.Text("Text", required=True, translate=True)
text_plural = fields.Text("Text Plural")
@classmethod
def gettext(cls, *args, **variables):
@@ -22,22 +29,131 @@ class Message(ModelSQL, ModelView):
key = (module, message_id, language)
text = cls._message_cache.get(key)
if text is None:
message_id = ModelData.get_id(module, message_id)
with Transaction().set_context(language=language):
message = cls(message_id)
text = message.text
message = cls(ModelData.get_id(module, message_id))
translation = message._get_translation(language)
if translation:
text = translation.value or message.text
else:
text = message.text
cls._message_cache.set(key, text)
return text if not variables else text % variables
@classmethod
def ngettext(cls, *args, **variables):
pool = Pool()
ModelData = pool.get('ir.model.data')
Lang = pool.get('ir.lang')
module, message_id, n, language = args
lang = Lang.get(language)
index = lang.get_plural(n)
key = (module, message_id, index, language)
text = cls._message_cache.get(key)
if text is None:
message = cls(ModelData.get_id(module, message_id))
translation = message._get_translation(language)
if translation:
values = [
translation.value, translation.value_1,
translation.value_2, translation.value_3]
text = values[index] or message.text
else:
text = message.text
cls._message_cache.set(key, text)
return text if not variables else text % variables
def _get_translation(self, language):
pool = Pool()
Translation = pool.get('ir.translation')
context = Transaction().context
if context.get('fuzzy_translation', False):
fuzzy_clause = []
else:
fuzzy_clause = [('fuzzy', '=', False)]
while language:
translations = Translation.search([
('lang', '=', language),
('type', '=', 'model'),
('name', '=', f'{self.__name__},text'),
('res_id', '=', self.id),
] + fuzzy_clause,
limit=1)
if translations:
translation, = translations
return translation
language = get_parent(language)
@classmethod
def _set_translations(cls, messages):
pool = Pool()
Translation = pool.get('ir.translation')
id2translations = {}
other_translations = defaultdict(list)
for sub_messages in grouped_slice(messages):
res_ids = [m.id for m in sub_messages]
translations = Translation.search([
('lang', '=', INTERNAL_LANG),
('type', '=', 'model'),
('name', '=', f'{cls.__name__},text'),
('res_id', 'in', res_ids),
])
id2translations.update((t.res_id, t) for t in translations)
for translation in Translation.search([
('lang', '!=', INTERNAL_LANG),
('type', '=', 'model'),
('name', '=', f'{cls.__name__},text'),
('res_id', 'in', res_ids),
]):
other_translations[translation.res_id].append(translation)
to_save = []
for message in messages:
translation = id2translations.get(message.id)
if translation:
if ((translation.src == message.text)
and (translation.src_plural == message.text_plural)):
continue
else:
translation = Translation(
lang=INTERNAL_LANG,
type='model',
name=f'{cls.__name__},text',
res_id=message.id)
translation.src = message.text
translation.src_plural = message.text_plural
translation.value = message.text
translation.value_1 = message.text_plural
to_save.append(translation)
for other_translation in other_translations[message.id]:
other_translation.src = message.text
other_translation.src_plural = message.text_plural
other_translation.fuzzy = True
to_save.append(other_translation)
if to_save:
Translation.save(to_save)
@classmethod
def create(cls, vlist):
messages = super().create(vlist)
cls._set_translations(messages)
return messages
@classmethod
def write(cls, messages, values, *args):
super(Message, cls).write(messages, values, *args)
cls._set_translations(messages)
cls._message_cache.clear()
@classmethod
def delete(cls, messages):
pool = Pool()
Translation = pool.get('ir.translation')
ids = [m.id for m in messages]
super(Message, cls).delete(messages)
Translation.delete_ids(cls.__name__, 'model', ids)
cls._message_cache.clear()
@classmethod
+5 -1
View File
@@ -1,6 +1,10 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<form col="1">
<form col="2">
<label name="text" yalign="0"/>
<field name="text"/>
<label name="text_plural" yalign="0"/>
<field name="text_plural"/>
</form>
+1
View File
@@ -3,4 +3,5 @@
this repository contains the full copyright notices and license terms. -->
<tree>
<field name="text" expand="1"/>
<field name="text_plural" expand="1" optional="1"/>
</tree>
+4
View File
@@ -6,6 +6,10 @@ this repository contains the full copyright notices and license terms. -->
<record model="ir.message" id="msg_test">
<field name="text">Message</field>
</record>
<record model="ir.message" id="msg_test_plural">
<field name="text">%(count)i message</field>
<field name="text_plural">%(count)i messages</field>
</record>
<record model="ir.message" id="msg_modelsql_check">
<field name="text">Value must be greater than 42.</field>
</record>
+45 -1
View File
@@ -3,7 +3,7 @@
from unittest.mock import patch
from trytond.i18n import gettext, lazy_gettext
from trytond.i18n import gettext, lazy_gettext, lazy_ngettext, ngettext
from trytond.pool import Pool
from trytond.tests.test_tryton import (
TestCase, activate_module, with_transaction)
@@ -110,3 +110,47 @@ class I18nTestCase(TestCase):
self.assertIsInstance(lazy, LazyString)
self.assertEqual(str(lazy), "Message")
@with_transaction()
def test_ngettext(self):
"ngettext returns the translated text"
pool = Pool()
Lang = pool.get('ir.lang')
Translation = pool.get('ir.translation')
ModelData = pool.get('ir.model.data')
lang, = Lang.search([('code', '=', 'nl')], limit=1)
Translation.create([{
'name': 'ir.message,text',
'lang': lang.code,
'type': 'model',
'res_id': ModelData.get_id('tests', 'msg_test_plural'),
'src': '%(count)i message',
'value': 'Translated %(count)i message',
'value_1': 'Translated %(count)i messages',
'module': 'tests',
'fuzzy': False,
}])
for n, result in [
(0, "Translated 0 messages"),
(1, "Translated 1 message"),
(2, "Translated 2 messages"),
]:
with self.subTest(n=n):
self.assertEqual(
ngettext('tests.msg_test_plural', n, lang.code, count=n),
result)
@with_transaction()
def test_lazy_ngettext(self):
"lazy_ngettext returns a LazyString"
for n, result in [
(1, "1 message"),
(2, "2 messages"),
]:
with self.subTest(n=n):
lazy = lazy_ngettext('tests.msg_test_plural', n, count=n)
self.assertIsInstance(lazy, LazyString)
self.assertEqual(str(lazy), result)