Commit 459a9227 authored by Zachary Seguin's avatar Zachary Seguin

Start switching translations to babel

parent 48d0db6a
Pipeline #128 passed with stages
in 8 minutes and 36 seconds
......@@ -23,12 +23,13 @@ RUN apk update \
libxslt-dev \
&& pip install -r requirements.txt \
&& pip install gunicorn \
&& rm -rf /root/.cache/pip \
&& apk del .build-deps
# Copy code
COPY . /srv/pyalertscanada
# Generate assets
# Generate assets + translate
RUN apk update \
&& apk add --no-cache --virtual .build-deps \
build-base \
......@@ -41,7 +42,8 @@ RUN apk update \
&& ./ac.py assets build \
&& chmod -R +rX /srv/pyalertscanada/pyalertscanada/static/.webassets-cache \
&& gem uninstall sass \
&& apk del .build-deps
&& apk del .build-deps \
&& pybabel compile -d translations
# Set defaults
USER nobody:nobody
......
......@@ -105,7 +105,7 @@ def test():
print('Hello :)')
manager.add_command('assets', ManageAssets(assets_env))
manager.add_command('runserver', Server(host='0.0.0.0', use_debugger=True))
manager.add_command('runserver', Server(host='0.0.0.0', use_debugger=False, use_reloader=True))
manager.add_command('shell', Shell())
manager.add_command('database', database_manager)
manager.add_command('alerts', alerts_manager)
......
[python: **.py]
[jinja2: **/templates/**.html]
extensions=jinja2.ext.autoescape,jinja2.ext.with_,webassets.ext.jinja2.AssetsExtension
silent=false
\ No newline at end of file
......@@ -11,5 +11,6 @@ docker run \
--env AC_BROKER_URL=redis://redis:6379/0 \
--env AC_REDIS_URL=redis://redis:6379/1 \
--entrypoint "pip" \
--user root \
pyalertscanada:dev \
"$@"
#!/bin/bash
docker run \
-it \
--volumes-from pyalertscanada_web \
--network pyalertscanada_default \
--rm \
--workdir /srv/pyalertscanada \
--entrypoint /usr/local/bin/pybabel \
pyalertscanada:dev \
"$@"
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
......@@ -24,6 +25,16 @@ THE SOFTWARE.
# Import secretys
from secrets import *
import os
# Translation
SUPPORTED_LANGUAGES = {
'en': 'English',
'fr': u'Français'
}
BABEL_DEFAULT_LOCALE = 'en'
BABEL_DEFAULT_TIMEZONE = 'America/Toronto'
BABEL_TRANSLATION_DIRECTORIES = os.getcwd() + '/translations'
# Locales
ENGLISH_LOCALE = 'en_CA'
......
......@@ -30,7 +30,7 @@ services:
- AC_REDIS_URL=redis://redis:6379/1
volumes:
- ./:/srv/pyalertscanada
- python:/usr/local/bin/python.27/sites-packages
- python:/usr/local
depends_on:
- db
ports:
......@@ -49,7 +49,7 @@ services:
- AC_REDIS_URL=redis://redis:6379/1
volumes:
- ./:/srv/pyalertscanada
- python:/usr/local/bin/python.27/sites-packages
- python:/usr/local
depends_on:
- db
- redis
......@@ -67,7 +67,7 @@ services:
- AC_REDIS_URL=redis://redis:6379/1
volumes:
- ./:/srv/pyalertscanada
- python:/usr/local/bin/python.27/sites-packages
- python:/usr/local
depends_on:
- db
- redis
......@@ -85,7 +85,7 @@ services:
- AC_REDIS_URL=redis://redis:6379/1
volumes:
- ./:/srv/pyalertscanada
- python:/usr/local/bin/python.27/sites-packages
- python:/usr/local
depends_on:
- db
- redis
......@@ -103,7 +103,7 @@ services:
- AC_REDIS_URL=redis://redis:6379/1
volumes:
- ./:/srv/pyalertscanada
- python:/usr/local/bin/python.27/sites-packages
- python:/usr/local
depends_on:
- db
- redis
......
......@@ -26,6 +26,7 @@ from flask import Flask
from flask_assets import Environment
from webassets.loaders import PythonLoader as PythonAssetsLoader
from flask_sqlalchemy import SQLAlchemy
from flask_babel import Babel
from celery import Celery
from redis import StrictRedis
......@@ -33,6 +34,7 @@ app = Flask(__name__)
app.config.from_object('config')
db = SQLAlchemy(app)
babel = Babel(app)
if not app.config['REDIS_URL'] is None:
redis = StrictRedis.from_url(app.config['REDIS_URL'], socket_timeout=app.config.get('REDIS_TIMEOUT', 20))
......
......@@ -22,6 +22,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
"""
from .. import app, celery, db
from .. import app, celery, db, babel
#app.jinja_env.cache = {}
app.jinja_env.cache = None
......@@ -30,7 +30,7 @@ import pytz
from . import app
from ..models import Alert, AlertCode, AlertReference, Info, InfoCategory, InfoEventCode, InfoParameter, InfoResponseType, Area, AreaPolygon, AreaCircle, AreaGeocode, Mapping
from .helpers import get_timezone, get_language, languages, get_show_test_alerts, get_alert
from .helpers import get_timezone, get_show_test_alerts, get_alert
@app.context_processor
def date_utils():
......@@ -50,14 +50,20 @@ def type_utils():
@app.context_processor
def possible_setting_values():
return dict(languages=languages, timezones=pytz.country_timezones['CA'])
return dict(timezones=pytz.country_timezones['CA'])
def get_alert_language():
if flask.g.lang == 'en':
return { 'code': 'en-CA' }
elif flask.g.lang == 'fr':
return { 'code': 'fr-CA' }
@app.context_processor
def user_settings():
return dict(tz=get_timezone(), language=get_language(), show_test_alerts=get_show_test_alerts())
return dict(tz=get_timezone(), language=get_alert_language(), show_test_alerts=get_show_test_alerts())
def label_mapping(type, value):
mapping = Mapping.query.get((type, value, get_language()['code']))
mapping = Mapping.query.get((type, value, get_alert_language()['code']))
if not mapping:
return 'secondary'
......@@ -65,7 +71,7 @@ def label_mapping(type, value):
return mapping.label_class
def text_mapping(type, value):
mapping = Mapping.query.get((type, value, get_language()['code']))
mapping = Mapping.query.get((type, value, get_alert_language()['code']))
if not mapping:
return value
......@@ -73,7 +79,7 @@ def text_mapping(type, value):
return mapping.display_value
def get_event_codes():
return Mapping.query.filter((Mapping.type == 'event_code') & (Mapping.language == get_language()['code'])).order_by(Mapping.display_value).all()
return Mapping.query.filter((Mapping.type == 'event_code') & (Mapping.language == get_alert_language()['code'])).order_by(Mapping.display_value).all()
@app.context_processor
def mappings():
......
......@@ -29,17 +29,14 @@ import flask
from . import app
from datetime import datetime
import pytz
from .helpers import get_timezone, get_language
from .helpers import get_timezone
from ..models import Mapping
import locale
from flask_babel import format_datetime, format_date, format_time, gettext, ngettext
@app.template_filter()
def translate(str):
mapping = Mapping.query.get(('text', str, get_language()['code']))
if not mapping:
return str
return mapping.display_value
return str
@app.template_filter()
def supress(value):
......@@ -74,62 +71,49 @@ def timediff(diff):
hours, remainder = divmod(remainder, 3600)
minutes, seconds = divmod(remainder, 60)
str = '' if in_past else translate('IN') + ' '
str = ''
if not days == 0:
str = '{str}{days:.0f} {str_days}'.format(
str=str,
days=days,
str_days=translate('DAY').lower() if days == 1 else translate('DAYS').lower()
)
str += ngettext('1 day', '%(num)d days', num=days)
if not hours == 0:
str = '{str}{separator}{hours:.0f} {str_hours}'.format(
str += '{separator}{hours}'.format(
separator=', ' if not days == 0 else '',
str=str,
hours=hours,
str_hours=translate('HOUR').lower() if hours == 1 else translate('HOURS').lower()
hours=ngettext('1 hour', '%(num)d hours', num=hours)
)
if not minutes == 0:
str = '{str}{separator}{minutes:.0f} {str_minutes}'.format(
str += '{separator}{minutes}'.format(
separator=', ' if not (days == 0 and hours == 0) else '',
str=str,
minutes=minutes,
str_minutes=translate('MINUTE').lower() if minutes == 1 else translate('MINUTES').lower()
minutes=ngettext('1 minute', '%(num)d minutes', num=minutes)
)
str += ' ' + translate('AGO').lower() if in_past else ''
return str
def format_datetime(date, format):
if not date:
return date
locale.setlocale(locale.LC_ALL, get_language()['locale'])
return unicode(date.strftime(format).decode(locale.getlocale()[1])).replace('à', u'à')
# Convert to a sentence
if not in_past:
return gettext('In %(str)s', str=str)
else:
return gettext('%(str)s ago', str=str)
@app.template_filter()
def date(date):
return format_datetime(date, translate('DATE_FORMAT'))
return format_date(date, 'long')
@app.template_filter()
def short_date(date):
return format_datetime(date, translate('SHORT_DATE_FORMAT'))
return format_date(date, 'medium')
@app.template_filter()
def time(date):
return format_datetime(date, translate('TIME_FORMAT'))
return format_time(date, 'long')
@app.template_filter()
def short_time(date):
return format_datetime(date, translate('SHORT_TIME_FORMAT'))
return format_time(date, 'medium')
@app.template_filter()
def datetime(date):
return format_datetime(date, translate('DATETIME_FORMAT'))
return format_datetime(date, 'full')
@app.template_filter()
def short_datetime(date):
return format_datetime(date, translate('SHORT_DATETIME_FORMAT'))
return format_datetime(date, 'medium')
......@@ -34,32 +34,12 @@ from . import app
from ..models import Alert, Info
def get_timezone():
tz = None
if 'timezone' in flask.session:
tz = pytz.timezone(flask.session['timezone'])
tz = pytz.timezone(flask.session.get('timezone', app.config['BABEL_DEFAULT_TIMEZONE']))
return tz or get_localzone()
def set_timezone(timezone):
flask.session['timezone'] = timezone
languages = [{ 'code': 'en-CA', 'description': 'English', 'locale': app.config['ENGLISH_LOCALE'] }, { 'code': 'fr-CA', 'description': u'Français', 'locale': app.config['FRENCH_LOCALE' ] }]
def get_language():
language = None
if 'language' in flask.session:
for lang in languages:
if lang['code'] == flask.session['language']:
language = lang
break
return language or languages[0]
def set_language(language):
flask.session['language'] = language
def get_show_test_alerts():
return flask.session.get('show_test_alerts', False)
......
......@@ -23,62 +23,103 @@ THE SOFTWARE.
"""
import flask
from .. import app
from .. import app, babel
from ..models import Alert, Info, Resource, Area, AlertReference, InfoEventCode, InfoResponseType
from datetime import datetime
import pytz
from tzlocal import get_localzone
import base64
import dateutil.parser
from .filters import translate
from flask_babel import gettext
import filters
import helpers
ac_web = flask.Blueprint('alerts_canada', __name__, url_prefix='', static_folder='resources', template_folder='templates')
# Translation
@app.url_defaults
def set_url_language(endpoint, values):
if 'lang' in values or not flask.g.get('lang', None):
return
if app.url_map.is_endpoint_expecting(endpoint, 'lang'):
values['lang'] = flask.g.lang
@app.url_value_preprocessor
def get_url_langauge(endpoint, values):
if values is not None:
flask.g.lang = values.pop('lang', flask.session.get('lang', app.config['BABEL_DEFAULT_LOCALE']))
else:
flask.g.lang = flask.session.get('lang', app.config['BABEL_DEFAULT_LOCALE'])
@babel.localeselector
def get_locale():
if not flask.g.lang in app.config['SUPPORTED_LANGUAGES']:
return flask.session.get('lang', app.config['BABEL_DEFAULT_LOCALE'])
return flask.g.lang
@babel.timezoneselector
def get_timezone():
from .helpers import get_timezone
return get_timezone()
@app.before_request
def set_session_time():
def verify_locale():
if not flask.g.lang in app.config['SUPPORTED_LANGUAGES']:
flask.g.lang = flask.session.get('lang', app.config['BABEL_DEFAULT_LOCALE'])
flask.abort(404)
@app.before_request
def set_session():
from datetime import timedelta
flask.session.permanent = True
flask.session['lang'] = flask.g.lang
app.permanent_session_lifetime = timedelta(weeks=52)
@app.before_request
def show_default_settings_alert():
if not 'default_settings_shown' in flask.session:
flask.flash(u'{warning}. <a href="{update_url}">{change}.</a>'.format(
warning=translate('DEFAULT_SETTINGS_WARNING'),
change=translate('DEFAULT_SETTINGS_CHANGE'),
update_url=flask.url_for('alerts_canada.settings')), 'info')
flask.flash(gettext('You are using default settings. <a href="%(url)s">Update your settings</a>.', url=flask.url_for('alerts_canada.settings')), 'info')
flask.session['default_settings_shown'] = True
@ac_web.route('/')
def index():
return flask.redirect(
flask.url_for('alerts_canada.active_alerts',
lang=flask.session.get('lang', app.config['BABEL_DEFAULT_LOCALE'])
)
)
@ac_web.route('/<lang>')
def lang_index():
return flask.redirect(flask.url_for('alerts_canada.active_alerts'))
@ac_web.route('/<lang>/active')
def active_alerts():
return flask.render_template(
'alerts.html',
title=translate('ACTIVE_ALERTS'),
title=gettext('Active alerts'),
alerts=helpers.active_alerts()
)
@ac_web.route('/settings')
@ac_web.route('/<lang>/settings')
def settings():
return flask.render_template('settings.html', title=translate('SETTINGS'))
return flask.render_template('settings.html', title=gettext('Settings'))
@ac_web.route('/settings', methods=['POST'])
@ac_web.route('/<lang>/settings', methods=['POST'])
def do_settings():
from .context import languages
from .helpers import get_timezone, set_timezone, get_language, set_language, get_show_test_alerts, set_show_test_alerts
from .helpers import get_timezone, set_timezone, get_show_test_alerts, set_show_test_alerts
set_timezone(flask.request.form.get('timezone', get_timezone()))
set_language(flask.request.form.get('language', get_language()['code']))
set_show_test_alerts(flask.request.form.get('show_test_alerts', 'off') == 'on')
flask.flash(translate('SETTINGS_SAVED') + '.', 'success')
flask.flash(gettext('Your settings have been saved.'), 'success')
return flask.redirect(flask.url_for('alerts_canada.settings'))
@ac_web.route('/archive')
@ac_web.route('/<lang>/archive')
def alerts_archive():
from flask import request
......@@ -96,14 +137,14 @@ def alerts_archive():
if request.args.get('type', 'all') != 'all':
alerts = alerts.join(InfoEventCode).filter((InfoEventCode.name == 'profile:CAP-CP:Event:0.4') & (InfoEventCode.value == request.args['type']))
return flask.render_template('alerts.html', title=translate('ARCHIVE_RESULTS'), alerts=alerts.all(), show_expired=True, show_superseded=True)
return flask.render_template('alerts.html', title=gettext('Alerts archive results'), alerts=alerts.all(), show_expired=True, show_superseded=True)
except ValueError:
flask.flash(translate('SELECT_VALID_DATE') + '.', 'alert')
flask.flash(gettext('Please provide a valid date.'), 'alert')
return flask.redirect(flask.url_for('alerts_canada.alerts_archive'))
return flask.render_template('archive.html', title=translate('ALERTS_ARCHIVE'))
return flask.render_template('archive.html', title=gettext('Alerts archive'))
@ac_web.route('/alert/<alert_id>')
@ac_web.route('/<lang>/alert/<alert_id>')
def alert(alert_id):
if not alert_id.isdigit():
alert_id = -1
......@@ -127,13 +168,13 @@ def alert(alert_id):
).join(Alert).order_by(AlertReference.id.desc()).all()
return flask.render_template('alert.html',
title=translate('ALERT_DETAILS'),
title=gettext('Alert details'),
alert = alert,
superseded_by = superseded_by,
references = references
)
@ac_web.route('/resource/<alert_id>/<info_id>/<resource_id>/<uri>')
@ac_web.route('/<lang>/resource/<alert_id>/<info_id>/<resource_id>/<uri>')
def alert_resource_by_uri(alert_id, info_id, resource_id, uri):
resource = Resource.query.filter(
Resource.alert_id == alert_id,
......@@ -147,7 +188,7 @@ def alert_resource_by_uri(alert_id, info_id, resource_id, uri):
decoded = base64.b64decode(str(data), '-_')
return flask.Response(decoded, mimetype=resource.mime_type)
@ac_web.route('/resource/<alert_id>/<info_id>/<resource_id>')
@ac_web.route('/<lang>/resource/<alert_id>/<info_id>/<resource_id>')
def alert_resource(alert_id, info_id, resource_id):
resource = Resource.query.filter(
Resource.alert_id == alert_id,
......@@ -166,10 +207,10 @@ def alert_resource(alert_id, info_id, resource_id):
@app.errorhandler(404)
def not_found(error):
return flask.render_template('error/404.html', title='Page Not Found'), 404
return flask.render_template('error/404.html', title=gettext('Page not found')), 404
@app.errorhandler(500)
def internal_server_error(error):
return flask.render_template('error/500.html', title='Internal Server Error'), 500
return flask.render_template('error/500.html', title=gettext('Server error')), 500
app.register_blueprint(ac_web)
......@@ -6,8 +6,8 @@
<div class="row">
<div class="small-12 columns">
<div class="panel callout">
<h4 class="subsubheader">{{ 'SUPERSEDED_ALERT' | translate }}</h4>
<p>{{ 'SUPERSEDED_ALERT_DESCRIPTION' | translate }}</p>
<h4 class="subsubheader">{{ _('Superseded alert') | upper }}</h4>
<p>{{ _('This alert has been superseded and is out of date.') }}</p>
<ul class="square">
{% for a in superseded_by %}
<li>
......@@ -33,11 +33,11 @@
<div class="container">
<strong>{{ info.headline }}</strong>
<span class="float-right">
{% if alert.status == 'Test' %}<div class="label warning radius">{{ 'TEST' | translate }}</div>{% endif %}
{% if alert.msg_type == 'Cancel' %}<div class="label alert radius">{{ 'CANCEL' | translate }}</div>{% endif %}
{% if alert.superseded %}<span class="label info radius">{{ 'SUPERSEDED' | translate }}</span>{% endif %}
{% if info.effective and utcnow < info.effective %}<span class="label success radius">{{ 'FUTURE' | translate }}</span>{% endif %}
{% if info.expires and info.expires < utcnow %}<span class="label alert radius">{{ 'EXPIRED' | translate }}</span>{% endif %}
{% if alert.status == 'Test' %}<div class="label warning radius">{{ _('Test') }}</div>{% endif %}
{% if alert.msg_type == 'Cancel' %}<div class="label alert radius">{{ _('Cancel') }}</div>{% endif %}
{% if alert.superseded %}<span class="label info radius">{{ _('Superseded') }}</span>{% endif %}
{% if info.effective and utcnow < info.effective %}<span class="label success radius">{{ _('Future') }}</span>{% endif %}
{% if info.expires and info.expires < utcnow %}<span class="label alert radius">{{ _('Expired') }}</span>{% endif %}
</span>
</div>
</a>
......@@ -51,16 +51,16 @@
<div class="meta">
<div class="medium-6 columns">
<dl>
<dt>{{ 'ISSUED_BY' | translate }}</dt>
<dt>{{ _('Issued by') }}</dt>
<dd>{% if info.web %} <span><a href="{{ info.web }}" target="_blank"> {% endif %}{{ info.sender_name }}{% if info.web %}</a></span>{% endif %}</dd>
<dt>{{ 'EFFECTIVE' | translate }}</dt>
<dt>{{ _('Effective') }}</dt>
{% if info.effective %}
<dd>{{ (info.effective - utcnow) | timediff }}</dd>
{% endif %}
<dd>{{ info.effective | localize | datetime }}</dd>
<dt>{{ 'EXPIRES' | translate }}</dt>
<dt>{{ _('Expires') }}</dt>
{% if info.expires %}
<dd>{{ (info.expires - utcnow) | timediff }}</dd>
{% endif %}
......@@ -69,29 +69,29 @@
</div>
<div class="medium-6 columns">
<dl>
<dt>{{ 'URGENCY' | translate }} / {{ 'SEVERITY' | translate }} / {{ 'CERTAINTY' | translate }}</dt>
<dt>{{ _('Urgency') }} / {{ _('Severity') }} / {{ _('Certainty') }}</dt>
<dd>
<span class="label radius {{ label_mapping('urgency', info.urgency) }} uppercase">{{ text_mapping('urgency', info.urgency) }}</span>
<span class="label radius {{ label_mapping('severity', info.severity) }} uppercase">{{ text_mapping('severity', info.severity) }}</span>
<span class="label radius {{ label_mapping('certainty', info.certainty) }} uppercase">{{ text_mapping('certainty', info.certainty) }}</span>
</dd>
<dt>{{ 'RESPONSE_TYPES' | translate }}</dt>
<dt>{{ _('Response types') }}</dt>
<dd>
{% for rt in info.response_types %}
<span class="label radius {{ label_mapping('response_type', rt) }} uppercase">{{ text_mapping('response_type', rt) }}</span>
{% endfor %}
</dd>
<dt>{{ 'AFFECTED_AREAS' | translate}}</dt>
<dt>{{ _('Affected areas') }}</dt>
{% if 'layer:EC-MSC-SMC:1.0:Alert_Coverage' in info.parameters and len(info.areas) > 6 %}
<dd>
<span><a href="#" data-reveal-id="affected-areas-{{ info.id }}">{{ info.parameters['layer:EC-MSC-SMC:1.0:Alert_Coverage'] }}</a> ({{ info.areas | length }} {{ 'AFFECTED_AREAS' | translate | lower }})</span>
<span><a href="#" data-reveal-id="affected-areas-{{ info.id }}">{{ info.parameters['layer:EC-MSC-SMC:1.0:Alert_Coverage'] }}</a> ({{ info.areas | length }} {{ _('affected areas') }})</span>
</dd>
<div id="affected-areas-{{ info.id }}" class="reveal-modal" data-reveal aria-labelledby="title" role="dialog">
<h2 id="title" class="page-title">{{ 'AFFECTED_AREAS' | translate }}</h2>
<h2 id="title" class="page-title">{{ _('Affected areas') }}</h2>
<ul class="no-bullet">
{% for area in info.areas %}
......@@ -121,13 +121,13 @@
{% if info.instruction %}
<hr>
<h4 class="subsubheader">{{ 'RECOMMENDED_ACTIONS' | translate }}</h4>
<h4 class="subsubheader">{{ _('Recommended actions') }}</h4>
<p>{{ info.instruction }}</p>
{% endif %}
{% if info.resources %}
<hr>
<h4 class="subsubheader">{{ 'RESOURCES' | translate }}</h4>
<h4 class="subsubheader">{{ _('Resources') }}</h4>
{% for resource in info.resources %}
<ul class="no-bullet">
<li><a href="{{ url_for('alerts_canada.alert_resource', alert_id=alert.id, info_id=info.id, resource_id=resource.id) }}" target="_blank">
......@@ -149,7 +149,7 @@
<div class="row references">
<div class="small-12 columns">
<div class="light-panel">
<h4 class="subsubheader">{{ 'REFERENCES' | translate }}</h4>
<h4 class="subsubheader">{{ _('References') }}</h4>
<ul class="square">
{% for reference in references %}
......
......@@ -25,31 +25,31 @@
</div>
<dl>
<dt>{{ 'ISSUED_BY' | translate }}</dt>
<dt>{{ _('Issued by') }}</dt>
<dd>{{ info.sender_name}}</dd>
<dt>{{ 'EFFECTIVE' | translate }} &mdash; {{ 'EXPIRES' | translate }}</dt>
<dt>{{ _('Effective') }} &mdash; {{ _('Expires') }}</dt>
<dd>{{ info.effective | localize | short_datetime }} &mdash; {{ info.expires | localize | short_datetime }}</dd>
<dt>{{ 'AFFECTED_AREAS' | translate }}</dt>
<dd>{% if 'layer:EC-MSC-SMC:1.0:Alert_Coverage' in info.parameters and len(info.areas) > 6 %} {{ info.parameters['layer:EC-MSC-SMC:1.0:Alert_Coverage'] }} ({{ info.areas | length }} {{ 'AFFECTED_AREAS' | translate | lower }}) {% else %} {% for area in info.areas %}{% if not loop.first %}{% if loop.last %} {{ 'AND' | translate }} {% else %}, {% endif %}{% endif %}{{ area.description }}{% endfor %} {% endif %}</dd>