diff --git a/trytond/CHANGELOG b/trytond/CHANGELOG index 6548efa6b4..8e99107f82 100644 --- a/trytond/CHANGELOG +++ b/trytond/CHANGELOG @@ -1,3 +1,4 @@ +* Support plural translation of messages * Support plural translations in report * Add plural source to translations * Add plural to language diff --git a/trytond/doc/ref/i18n.rst b/trytond/doc/ref/i18n.rst index e5f6a7c723..8bb8b289c8 100644 --- a/trytond/doc/ref/i18n.rst +++ b/trytond/doc/ref/i18n.rst @@ -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. diff --git a/trytond/trytond/i18n.py b/trytond/trytond/i18n.py index e7c1ac53c0..3d6b1b9640 100644 --- a/trytond/trytond/i18n.py +++ b/trytond/trytond/i18n.py @@ -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) diff --git a/trytond/trytond/ir/message.py b/trytond/trytond/ir/message.py index 8eb500b3fd..c0091246b4 100644 --- a/trytond/trytond/ir/message.py +++ b/trytond/trytond/ir/message.py @@ -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 diff --git a/trytond/trytond/ir/view/message_form.xml b/trytond/trytond/ir/view/message_form.xml index ac0cfab25c..266a753494 100644 --- a/trytond/trytond/ir/view/message_form.xml +++ b/trytond/trytond/ir/view/message_form.xml @@ -1,6 +1,10 @@ -
diff --git a/trytond/trytond/ir/view/message_list.xml b/trytond/trytond/ir/view/message_list.xml index 0ecc4303b5..475ae32cf2 100644 --- a/trytond/trytond/ir/view/message_list.xml +++ b/trytond/trytond/ir/view/message_list.xml @@ -3,4 +3,5 @@ this repository contains the full copyright notices and license terms. -->