Improve localization on the History page using string replacement (#587)

This commit is contained in:
Andrew Dickinson 2021-07-12 16:48:19 -04:00 committed by GitHub
parent 2f42afbc69
commit 72230448a7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 147 additions and 46 deletions

View file

@ -71,7 +71,7 @@ build-translations: ## Build the translations
.PHONY: update-translations
update-translations: ## Extract new translations from source code
$(VENV)/bin/pybabel extract --strip-comments --omit-header --no-location --mapping-file ihatemoney/babel.cfg -o ihatemoney/messages.pot ihatemoney
$(VENV)/bin/pybabel extract --add-comments "I18N:" --strip-comments --omit-header --no-location --mapping-file ihatemoney/babel.cfg -o ihatemoney/messages.pot ihatemoney
$(VENV)/bin/pybabel update -i ihatemoney/messages.pot -d ihatemoney/translations/
.PHONY: create-database-revision

View file

@ -1,4 +1,3 @@
from flask_babel import gettext as _
from sqlalchemy_continuum import Operation, parent_class
from ihatemoney.models import BillVersion, Person, PersonVersion, ProjectVersion
@ -69,11 +68,10 @@ def get_history(project, human_readable_names=True):
history = []
for version_list in [person_query.all(), project_query.all(), bill_query.all()]:
for version in version_list:
object_type = {
"Person": _("Participant"),
"Bill": _("Bill"),
"Project": _("Project"),
}[parent_class(type(version)).__name__]
object_type = parent_class(type(version)).__name__
# The history.html template can only handle objects of these types
assert object_type in ["Person", "Bill", "Project"]
# Use the old name if applicable
if version.previous:

View file

@ -8,6 +8,7 @@ from flask_babel import Babel, format_currency
from flask_mail import Mail
from flask_migrate import Migrate, stamp, upgrade
from jinja2 import pass_context
from markupsafe import Markup
from werkzeug.middleware.proxy_fix import ProxyFix
from ihatemoney import default_settings
@ -17,7 +18,9 @@ from ihatemoney.models import db
from ihatemoney.utils import (
IhmJSONEncoder,
PrefixedWSGI,
em_surround,
locale_from_iso,
localize_list,
minimal_round,
static_include,
)
@ -150,6 +153,8 @@ def create_app(
app.jinja_env.globals["static_include"] = static_include
app.jinja_env.globals["locale_from_iso"] = locale_from_iso
app.jinja_env.filters["minimal_round"] = minimal_round
app.jinja_env.filters["em_surround"] = lambda text: Markup(em_surround(text))
app.jinja_env.filters["localize_list"] = localize_list
# Translations and time zone (used to display dates). The timezone is
# taken from the BABEL_DEFAULT_TIMEZONE settings, and falls back to

View file

@ -29,13 +29,14 @@
{% endif %}
{% endmacro %}
{% macro describe_object(event) %}{{ event.object_type }} <em class="font-italic">{{ event.object_desc }}</em>{% endmacro %}
{% macro simple_property_change(event, localized_property_name, from=True) %}
{{ describe_object(event) }}:
{{ localized_property_name }} {{ _("changed") }}
{% if from %}{{ _("from") }} <em class="font-italic">{{ event.val_before }}</em>{% endif %}
{{ _("to") }} <em class="font-italic">{{ event.val_after }}</em>
{% macro bill_property_change(event, localized_property_name, before=event.val_before|em_surround, after=event.val_after|em_surround) %}
{% set name=event.object_desc|em_surround %}
{% set property_name=localized_property_name %}
{% if before %}
{% trans %}Bill {{ name }}: {{ property_name }} changed from {{ before }} to {{ after }}{% endtrans %}
{% else %}
{% trans %}Bill {{ name }}: {{ property_name }} changed to {{ after }}{% endtrans %}
{% endif %}
{% endmacro %}
{% macro clear_history_modals() %}
@ -83,17 +84,13 @@
{% endmacro %}
{% macro owers_changed(event, add) %}
{{ describe_object(event) }}: {% if add %}{{ _("Added") }}{% else %}{{ _("Removed") }}{% endif %}
{% if event.val_after|length > 1 %}
{% for name in event.val_after %}
<em class="font-italic">{{ name }}</em>{% if event.val_after|length > 2 and loop.index != event.val_after|length %},{% endif %}
{% if loop.index == event.val_after|length - 1 %} {{ _("and") }} {% endif %}
{% endfor %}
{% set name=event.object_desc|em_surround %}
{% set owers_list_str=event.val_after|localize_list|safe %}
{% if add %}
{% trans %}Bill {{ name }}: added {{ owers_list_str }} to owers list{% endtrans %}
{% else %}
<em class="font-italic">{{ event.val_after[0] }}</em>
{% trans %}Bill {{ name }}: removed {{ owers_list_str }} from owers list{% endtrans %}
{% endif %}
{% if add %}{{ _("to") }}{% else %}{{ _("from") }}{% endif %}
{{ _("owers list") }}
{% endmacro %}
{% block sidebar %}
@ -174,53 +171,86 @@
></i>
</div>
<div class="history_text">
{# Common value setting #}
{% set name=event.object_desc|em_surround %}
{% if event.operation_type == OperationType.INSERT %}
{{ event.object_type }} <em class="font-italic">{{ event.object_desc }}</em> {{ _("added") }}
{% if event.object_type == "Project" %}
{% trans %}Project {{ name }} added{% endtrans %}
{% elif event.object_type == "Bill" %}
{% trans %}Bill {{ name }} added{% endtrans %}
{% elif event.object_type == "Person" %}
{% trans %}Participant {{ name }} added{% endtrans %}
{% endif %}
{% elif event.operation_type == OperationType.UPDATE %}
{% if event.object_type == _("Project") %}
{% if event.object_type == "Project" %}
{% if event.prop_changed == "password" %}
{{ _("Project private code changed") }}
{% elif event.prop_changed == "logging_preference" %}
{{ change_to_logging_preference(event) }}
{% elif event.prop_changed == "name" %}
{{ _("Project renamed to") }} <em class="font-italic">{{ event.val_after }}</em>
{% set new_project_name=event.val_after|em_surround %}
{% trans %}Project renamed to {{ new_project_name }}{% endtrans %}
{% elif event.prop_changed == "contact_email" %}
{{ _("Project contact email changed to") }} <em class="font-italic">{{ event.val_after }}</em>
{% set new_email=event.val_after|em_surround %}
{% trans %}Project contact email changed to {{ new_email }}{% endtrans %}
{% else %}
{{ _("Project settings modified") }}
{% endif %}
{% elif event.prop_changed == "activated" %}
{{ event.object_type }} <em class="font-italic">{{ event.object_desc }}</em>
{% if event.val_after == False %}{{ _("deactivated") }}{% else %}{{ _("reactivated") }}{% endif %}
{% elif event.prop_changed == "name" or event.prop_changed == "what" %}
{{ describe_object(event) }} {{ _("renamed to") }} <em class="font-italic">{{ event.val_after }}</em>
{% if event.val_after == False %}
{% trans %}Participant {{ name }} deactivated{% endtrans %}
{% else %}
{% trans %}Participant {{ name }} reactivated{% endtrans %}
{% endif %}
{% elif event.prop_changed == "name" %}
{% set new_name=event.val_after|em_surround %}
{% trans %}Participant {{ name }} renamed to {{ new_name }}{% endtrans %}
{% elif event.prop_changed == "what" %}
{% set new_description=event.val_after|em_surround %}
{% trans %}Bill {{ name }} renamed to {{ new_description }}{% endtrans %}
{% elif event.prop_changed == "weight" %}
{{ simple_property_change(event, _("Weight")) }}
{% set old_weight=event.val_before|em_surround %}
{% set new_weight=event.val_after|em_surround %}
{% trans %}Participant {{ name }}: weight changed from {{ old_weight }} to {{ new_weight }}{% endtrans %}
{% elif event.prop_changed == "external_link" %}
{{ describe_object(event) }}: {{ _("External link changed to") }}
<a href="{{ event.val_after }}" class="font-italic">{{ event.val_after }}</a>
{{ bill_property_change(event, _("External link"), None, "<a href='{link}' >{link}</a>".format(link=event.val_after | escape) | safe | em_surround) }}
{% elif event.prop_changed == "owers_added" %}
{{ owers_changed(event, True)}}
{% elif event.prop_changed == "owers_removed" %}
{{ owers_changed(event, False)}}
{% elif event.prop_changed == "payer" %}
{{ simple_property_change(event, _("Payer"))}}
{{ bill_property_change(event, _("Payer"))}}
{% elif event.prop_changed == "amount" %}
{{ simple_property_change(event, _("Amount")) }}
{{ bill_property_change(event, _("Amount")) }}
{% elif event.prop_changed == "date" %}
{{ simple_property_change(event, _("Date")) }}
{{ bill_property_change(event, _("Date")) }}
{% elif event.prop_changed == "original_currency" %}
{{ simple_property_change(event, _("Currency")) }}
{{ bill_property_change(event, _("Currency")) }}
{% elif event.prop_changed == "converted_amount" %}
{{ simple_property_change(event, _("Amount in %(currency)s", currency=g.project.default_currency)) }}
{{ bill_property_change(event, _("Amount in %(currency)s", currency=g.project.default_currency)) }}
{% else %}
{{ describe_object(event) }} {{ _("modified") }}
{% if event.object_type == "Bill" %}
{% trans %}Bill {{ name }} modified{% endtrans %}
{% elif event.object_type == "Person" %}
{% trans %}Participant {{ name }} modified{% endtrans %}
{% endif %}
{% endif %}
{% elif event.operation_type == OperationType.DELETE %}
{{ event.object_type }} <em class="font-italic">{{ event.object_desc }}</em> {{ _("removed") }}
{% if event.object_type == "Bill" %}
{% trans %}Bill {{ name }} removed{% endtrans %}
{% elif event.object_type == "Person" %}
{% trans %}Participant {{ name }} removed{% endtrans %}
{% endif %}
{% else %}
{# Should be unreachable #}
{{ describe_object(event) }} {{ _("changed in a unknown way") }}
{% if event.object_type == "Project" %}
{% trans %}Project {{ name }} changed in an unknown way{% endtrans %}
{% elif event.object_type == "Bill" %}
{% trans %}Bill {{ name }} changed in an unknown way{% endtrans %}
{% elif event.object_type == "Person" %}
{% trans %}Participant {{ name }} changed in an unknown way{% endtrans %}
{% endif %}
{% endif %}
</div>
</td>

View file

@ -410,7 +410,7 @@ class HistoryTestCase(IhatemoneyTestCase):
self.assertEqual(resp.status_code, 200)
self.assertRegex(
resp.data.decode("utf-8"),
r"Participant %s:\s* Weight changed\s* from %s\s* to %s"
r"Participant %s:\s* weight changed\s* from %s\s* to %s"
% (
em_surround("zorglub", regex_escape=True),
em_surround("1.0", regex_escape=True),
@ -426,7 +426,7 @@ class HistoryTestCase(IhatemoneyTestCase):
resp.data.decode("utf-8").index(
f"Participant {em_surround('zorglub')} renamed"
),
resp.data.decode("utf-8").index("Weight changed"),
resp.data.decode("utf-8").index("weight changed"),
)
# delete user using POST method

View file

@ -12,7 +12,7 @@ import socket
from babel import Locale
from babel.numbers import get_currency_name, get_currency_symbol
from flask import current_app, redirect, render_template
from flask import current_app, escape, redirect, render_template
from flask_babel import get_locale, lazy_gettext as _
import jinja2
from werkzeug.routing import HTTPException, RoutingException
@ -300,6 +300,74 @@ class FormEnum(Enum):
return str(self.value)
def em_surround(string, regex_escape=False):
# Needed since we're going to assume this is safe later in order to render
# the <em> tag we're adding
string = escape(string)
if regex_escape:
return fr'<em class="font-italic">{string}<\/em>'
else:
return f'<em class="font-italic">{string}</em>'
def localize_list(items, surround_with_em=True):
"""
Localize a list, optionally surrounding each item in <em> tags.
Uses the appropriate joining character, oxford comma behavior, and handles
1, and 2 object lists, all according to localizable behavior.
Examples (using en locale):
>>> localize_list([1,2,3,4,5], False)
1, 2, 3, 4, and 5
>>> localize_list([1,2], False)
1 and 2
Based on the LUA example from:
https://stackoverflow.com/a/58033018
:param list: The list of objects to localize by a call to str()
:param surround_with_em: Optionally surround each object with <em> tags
:return: A locally formatted list of objects
"""
if len(items) == 0:
return ""
item_wrapper = em_surround if surround_with_em else lambda x: x
wrapped_items = list(map(item_wrapper, items))
if len(wrapped_items) == 1:
return str(wrapped_items[0])
elif len(wrapped_items) == 2:
# I18N: List with two items only
return _("{dual_object_0} and {dual_object_1}").format(
dual_object_0=wrapped_items[0], dual_object_1=wrapped_items[1]
)
else:
# I18N: Last two items of a list with more than 3 items
output_str = _("{previous_object}, and {end_object}").format(
previous_object="{previous_object}", end_object=wrapped_items.pop()
)
# I18N: Two items in a middle of a list with more than 5 objects
middle = _("{previous_object}, {next_object}")
while len(wrapped_items) > 2:
temp = middle.format(
previous_object="{previous_object}",
next_object=wrapped_items.pop(),
)
output_str = output_str.format(previous_object=temp)
output_str = output_str.format(previous_object=wrapped_items.pop())
# I18N: First two items of a list with more than 3 items
output_str = _("{start_object}, {next_object}").format(
start_object="{start_object}", next_object=output_str
)
return output_str.format(start_object=wrapped_items.pop())
def render_localized_currency(code, detailed=True):
if code == "XXX":
return _("No Currency")