Support plural translation of messages
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
* Support plural translation of messages
|
||||
* Support plural translations in report
|
||||
* Add plural source to translations
|
||||
* Add plural to language
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user