# HG changeset patch # User Paul Boddie # Date 1446681482 -3600 # Node ID 819a0a8a3ae5c363daef4ea06c31ac2c7d0682d4 # Parent 4bc73eb1e076b8bf91a1cc0c0b35991ba579ee79 Added initial message localisation support. diff -r 4bc73eb1e076 -r 819a0a8a3ae5 imiptools/client.py --- a/imiptools/client.py Wed Nov 04 23:35:16 2015 +0100 +++ b/imiptools/client.py Thu Nov 05 00:58:02 2015 +0100 @@ -26,6 +26,7 @@ uri_dict, uri_item, uri_items, uri_parts, uri_values from imiptools.dates import check_permitted_values, format_datetime, get_default_timezone, \ get_duration, get_timestamp +from imiptools.i18n import get_translator from imiptools.period import can_schedule, remove_event_periods, \ remove_additional_periods, remove_affected_period, \ update_freebusy @@ -59,6 +60,11 @@ self.preferences_dir = preferences_dir self.preferences = None + # Localise the messenger. + + if self.messenger: + self.messenger.gettext = self.get_translator() + # Store-related methods. def acquire_lock(self): @@ -74,6 +80,12 @@ self.preferences = Preferences(self.user, self.preferences_dir) return self.preferences + def get_locale(self): + return self.get_preferences().get("LANG", "en", True) + + def get_translator(self): + return get_translator([self.get_locale()]) + def get_user_attributes(self): prefs = self.get_preferences() return prefs and prefs.get_all(["CN"]) or {} @@ -203,7 +215,7 @@ """ obj = obj or self.obj - calendar_uri = get_uri(self.messenger.sender) + calendar_uri = self.messenger and get_uri(self.messenger.sender) for attendee, attendee_attr in uri_items(obj.get_items("ATTENDEE")): if attendee != self.user: if attendee_attr.get("SENT-BY") == calendar_uri: @@ -224,7 +236,7 @@ # Search for the sender of the message or the calendar system address. - senders = self.senders or [self.messenger.sender] + senders = self.senders or self.messenger and [self.messenger.sender] or [] for attendee, attendee_attr in uri_items(self.obj.get_items("ATTENDEE")): if get_address(attendee) in senders or \ @@ -618,6 +630,9 @@ recipient since the generic calendar user will be the actual sender. """ + if not self.messenger: + return + if not bcc_sender: message = self.messenger.make_outgoing_message(parts, recipients) self.messenger.sendmail(recipients, message.as_string()) @@ -629,6 +644,9 @@ "Send a message composed of the given 'parts' to the given user." + if not self.messenger: + return + sender = get_address(self.user) message = self.messenger.make_outgoing_message(parts, [sender]) self.messenger.sendmail([sender], message.as_string()) diff -r 4bc73eb1e076 -r 819a0a8a3ae5 imiptools/config.py --- a/imiptools/config.py Wed Nov 04 23:35:16 2015 +0100 +++ b/imiptools/config.py Thu Nov 05 00:58:02 2015 +0100 @@ -36,6 +36,11 @@ DEFAULT_DIR_PERMISSIONS = 02770 +# Internationalisation and translations support. + +LOCALE_DIR = "/usr/share/locale" +TRANS_DOMAIN = "imip-agent" + # The availability of a management interface for calendar information. diff -r 4bc73eb1e076 -r 819a0a8a3ae5 imiptools/handlers/__init__.py --- a/imiptools/handlers/__init__.py Wed Nov 04 23:35:16 2015 +0100 +++ b/imiptools/handlers/__init__.py Thu Nov 05 00:58:02 2015 +0100 @@ -69,11 +69,13 @@ "Wrap any valid message for passing to the recipient." + _ = self.get_translator() + texts = [] texts.append(text) if link and self.have_manager(): - texts.append("If your mail program cannot handle this " - "message, you may view the details here:\n\n%s" % + texts.append(_("If your mail program cannot handle this " + "message, you may view the details here:\n\n%s") % get_object_url(self.uid, self.recurrenceid)) return self.add_result(None, None, MIMEText("\n".join(texts))) diff -r 4bc73eb1e076 -r 819a0a8a3ae5 imiptools/handlers/person.py --- a/imiptools/handlers/person.py Wed Nov 04 23:35:16 2015 +0100 +++ b/imiptools/handlers/person.py Thu Nov 05 00:58:02 2015 +0100 @@ -242,59 +242,75 @@ "Queue a suggested additional recurrence for any active event." + _ = self.get_translator() + if self.allow_add() and self._process(self._add, queue=True): - return self.wrap("An addition to an event has been received.") + return self.wrap(_("An addition to an event has been received.")) def cancel(self): "Queue a cancellation of any active event." + _ = self.get_translator() + if self._cancel(): - return self.wrap("An event cancellation has been received.", link=False) + return self.wrap(_("An event cancellation has been received."), link=False) def counter(self): "Record a counter-proposal to a proposed event." + _ = self.get_translator() + if self._process(self._counter, from_organiser=False): - return self.wrap("A counter proposal to an event invitation has been received.", link=True) + return self.wrap(_("A counter proposal to an event invitation has been received."), link=True) def declinecounter(self): "Record a rejection of a counter-proposal." + _ = self.get_translator() + if self._process(self._declinecounter): - return self.wrap("Your counter proposal to an event invitation has been declined.", link=True) + return self.wrap(_("Your counter proposal to an event invitation has been declined."), link=True) def publish(self): "Register details of any relevant event." + _ = self.get_translator() + if self._publish(): - return self.wrap("Details of an event have been received.") + return self.wrap(_("Details of an event have been received.")) def refresh(self): "Requests to refresh events are handled either here or by the client." + _ = self.get_translator() + if self.is_refreshing(): return self._process(self._refresh, from_organiser=False) else: - return self.wrap("A request for updated event details has been received.") + return self.wrap(_("A request for updated event details has been received.")) def reply(self): "Record replies and notify the recipient." + _ = self.get_translator() + if self._process(self._schedule_for_organiser, from_organiser=False): - return self.wrap("A reply to an event invitation has been received.") + return self.wrap(_("A reply to an event invitation has been received.")) def request(self): "Hold requests and notify the recipient." + _ = self.get_translator() + if self._process(self._schedule_for_attendee, queue=True): - return self.wrap("An event invitation has been received.") + return self.wrap(_("An event invitation has been received.")) class Freebusy(CommonFreebusy, Handler): @@ -304,23 +320,27 @@ "Register free/busy information." + _ = self.get_translator() + self._record_freebusy(from_organiser=True) # Produce a message if configured to do so. if self.is_notifying(): - return self.wrap("A free/busy update has been received.", link=False) + return self.wrap(_("A free/busy update has been received."), link=False) def reply(self): "Record replies and notify the recipient." + _ = self.get_translator() + self._record_freebusy(from_organiser=False) # Produce a message if configured to do so. if self.is_notifying(): - return self.wrap("A reply to a free/busy request has been received.", link=False) + return self.wrap(_("A reply to a free/busy request has been received."), link=False) def request(self): diff -r 4bc73eb1e076 -r 819a0a8a3ae5 imiptools/i18n.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/imiptools/i18n.py Thu Nov 05 00:58:02 2015 +0100 @@ -0,0 +1,37 @@ +#!/usr/bin/env python + +""" +Internationalisation support. + +Copyright (C) 2015 Paul Boddie + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 3 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +details. + +You should have received a copy of the GNU General Public License along with +this program. If not, see . +""" + +from imiptools.config import LOCALE_DIR, TRANS_DOMAIN +from os.path import abspath, exists, join, split +import gettext + +def get_locale_dir(): + locale_dir = abspath(join(split(__file__)[0], "..", "locale")) + if exists(locale_dir): + return locale_dir + else: + return LOCALE_DIR + +def get_translator(languages): + return gettext.translation(TRANS_DOMAIN, get_locale_dir(), languages, + fallback=True).ugettext + +# vim: tabstop=4 expandtab shiftwidth=4 diff -r 4bc73eb1e076 -r 819a0a8a3ae5 imiptools/mail.py --- a/imiptools/mail.py Wed Nov 04 23:35:16 2015 +0100 +++ b/imiptools/mail.py Thu Nov 05 00:58:02 2015 +0100 @@ -25,22 +25,22 @@ from email.mime.text import MIMEText from smtplib import LMTP, SMTP -MESSAGE_SUBJECT = "Calendar system message" +# Fake gettext function for strings to be translated later. + +_ = lambda s: s -MESSAGE_TEXT = """\ -This is a message from the calendar system. -""" +MESSAGE_SUBJECT = _("Calendar system message") -PREAMBLE_TEXT = """\ +PREAMBLE_TEXT = _("""\ This message contains several different parts, one of which will contain calendar information that will only be understood by a suitable program. -""" +""") class Messenger: "Sending of outgoing messages." - def __init__(self, lmtp_socket=None, local_smtp=False, sender=None, subject=None, body_text=None, preamble_text=None): + def __init__(self, lmtp_socket=None, local_smtp=False, sender=None, subject=None, preamble_text=None): """ Deliver to a local mail system using LMTP if 'lmtp_socket' is provided @@ -50,9 +50,13 @@ self.lmtp_socket = lmtp_socket self.local_smtp = local_smtp self.sender = sender or MESSAGE_SENDER - self.subject = subject or MESSAGE_SUBJECT - self.body_text = body_text or MESSAGE_TEXT - self.preamble_text = preamble_text or PREAMBLE_TEXT + self.subject = subject + self.preamble_text = preamble_text + + # The translation method is set by the client once locale information is + # known. + + self.gettext = None def local_delivery(self): @@ -115,7 +119,8 @@ message["To"] = recipient if outgoing_bcc: message["Bcc"] = "%s+%s" % (OUTGOING_PREFIX, outgoing_bcc) - message["Subject"] = self.subject + message["Subject"] = self.subject or \ + self.gettext and self.gettext(MESSAGE_SUBJECT) or MESSAGE_SUBJECT return message @@ -153,7 +158,8 @@ "Return a container for the given 'parts'." message = MIMEMultipart("mixed", _subparts=parts) - message.preamble = self.preamble_text + message.preamble = self.preamble_text or \ + self.gettext and self.gettext(PREAMBLE_TEXT) or PREAMBLE_TEXT return message def _copy_headers(self, message, msg): diff -r 4bc73eb1e076 -r 819a0a8a3ae5 imiptools/profile.py --- a/imiptools/profile.py Wed Nov 04 23:35:16 2015 +0100 +++ b/imiptools/profile.py Thu Nov 05 00:58:02 2015 +0100 @@ -27,6 +27,10 @@ import codecs import pytz +# Fake gettext method for strings to be translated later. + +_ = lambda s: s + def identity_dict(l): return dict([(i, i) for i in l]) @@ -56,45 +60,45 @@ known_key_choices = { "TZID" : identity_dict(pytz.all_timezones), "add_method_response" : { - "add" : "Add events", - "ignore" : "Ignore requests", - "refresh" : "Ask for refreshed event details" + "add" : _("Add events"), + "ignore" : _("Ignore requests"), + "refresh" : _("Ask for refreshed event details"), }, "event_refreshing" : { - "never" : "Do not respond", - "always" : "Always respond" + "never" : _("Do not respond"), + "always" : _("Always respond"), }, "freebusy_bundling" : { - "never" : "Never", - "always" : "Always" + "never" : _("Never"), + "always" : _("Always"), }, "freebusy_messages" : { - "none" : "Do not notify", - "notify" : "Notify" + "none" : _("Do not notify"), + "notify" : _("Notify"), }, "freebusy_publishing" : { - "publish" : "Publish", - "no" : "Do not publish" + "publish" : _("Publish"), + "no" : _("Do not publish"), }, "freebusy_sharing" : { - "share" : "Share", - "no" : "Do not share" + "share" : _("Share"), + "no" : _("Do not share"), }, "incoming" : { - "message-only" : "Original message only", - "message-then-summary" : "Original message followed by a separate summary message", - "summary-then-message" : "Summary message followed by the original message", - "summary-only" : "Summary message only", - "summary-wraps-message" : "Summary message wrapping the original message" + "message-only" : _("Original message only"), + "message-then-summary" : _("Original message followed by a separate summary message"), + "summary-then-message" : _("Summary message followed by the original message"), + "summary-only" : _("Summary message only"), + "summary-wraps-message" : _("Summary message wrapping the original message"), }, "organiser_replacement" : { - "any" : "Anyone", - "attendee" : "Existing attendees only", - "never" : "Never allow organiser replacement" + "any" : _("Anyone"), + "attendee" : _("Existing attendees only"), + "never" : _("Never allow organiser replacement"), }, "participating" : { - "participate" : "Participate", - "no" : "Do not participate" + "participate" : _("Participate"), + "no" : _("Do not participate"), } } diff -r 4bc73eb1e076 -r 819a0a8a3ae5 imipweb/calendar.py --- a/imipweb/calendar.py Wed Nov 04 23:35:16 2015 +0100 +++ b/imipweb/calendar.py Thu Nov 05 00:58:02 2015 +0100 @@ -44,6 +44,8 @@ the event page for further activity. """ + _ = self.get_translator() + # Handle a submitted form. args = self.env.get_args() @@ -128,7 +130,7 @@ user_attr = self.get_user_attributes() rwrite(("UID", {}, uid)) - rwrite(("SUMMARY", {}, summary or ("New event at %s" % utcnow))) + rwrite(("SUMMARY", {}, summary or (_("New event at %s") % utcnow))) rwrite(("DTSTAMP", {}, utcnow)) rwrite(("DTSTART", start_attr, start_value)) rwrite(("DTEND", end_attr, end_value)) @@ -212,6 +214,8 @@ "Show requests for the current user." + _ = self.get_translator() + page = self.page view_period = self.get_view_period() duration = view_period and view_period.get_duration() or timedelta(1) @@ -246,7 +250,7 @@ page.ul.close() else: - page.p("There are no pending requests.") + page.p(_("There are no pending requests.")) page.div.close() @@ -254,6 +258,8 @@ "Show participants for scheduling purposes." + _ = self.get_translator() + page = self.page # Show any specified participants together with controls to remove and @@ -261,17 +267,17 @@ page.div(id="participants") - page.p("Participants for scheduling:") + page.p(_("Participants for scheduling:")) for i, participant in enumerate(participants): page.p() page.input(name="participants", type="text", value=participant) - page.input(name="remove-participant-%d" % i, type="submit", value="Remove") + page.input(name="remove-participant-%d" % i, type="submit", value=_("Remove")) page.p.close() page.p() page.input(name="participants", type="text") - page.input(name="add-participant", type="submit", value="Add") + page.input(name="add-participant", type="submit", value=_("Add")) page.p.close() page.div.close() @@ -286,6 +292,8 @@ tables. """ + _ = self.get_translator() + page = self.page args = self.env.get_args() @@ -293,12 +301,12 @@ self.control("hidebusy", "checkbox", "hide", ("hide" in args.get("hidebusy", [])), id="hidebusy", accesskey="B") page.p(id_="calendar-controls", class_="controls") - page.span("Select days or periods for a new event.") - page.label("Hide busy time periods", for_="hidebusy", class_="hidebusy enable") - page.label("Show busy time periods", for_="hidebusy", class_="hidebusy disable") - page.label("Show empty days", for_="showdays", class_="showdays disable") - page.label("Hide empty days", for_="showdays", class_="showdays enable") - page.input(name="reset", type="submit", value="Clear selections", id="reset") + page.span(_("Select days or periods for a new event.")) + page.label(_("Hide busy time periods"), for_="hidebusy", class_="hidebusy enable") + page.label(_("Show busy time periods"), for_="hidebusy", class_="hidebusy disable") + page.label(_("Show empty days"), for_="showdays", class_="showdays disable") + page.label(_("Hide empty days"), for_="showdays", class_="showdays enable") + page.input(name="reset", type="submit", value=_("Clear selections"), id="reset") page.p.close() def show_time_navigation(self, freebusy, view_period): @@ -308,6 +316,8 @@ 'freebusy' and for the period defined by 'view_period'. """ + _ = self.get_translator() + page = self.page view_start = view_period.get_start() view_end = view_period.get_end() @@ -326,13 +336,13 @@ if last_preceding: preceding_start = last_preceding - duration - page.label("Show earlier events", for_="earlier-events", class_="earlier-events") + page.label(_("Show earlier events"), for_="earlier-events", class_="earlier-events") page.input(name="earlier-events", id_="earlier-events", type="submit") page.input(name="earlier-events-start", type="hidden", value=format_datetime(preceding_start)) page.input(name="earlier-events-end", type="hidden", value=format_datetime(last_preceding)) earlier_start = view_start - duration - page.label("Show earlier", for_="earlier", class_="earlier") + page.label(_("Show earlier"), for_="earlier", class_="earlier") page.input(name="earlier", id_="earlier", type="submit") page.input(name="earlier-start", type="hidden", value=format_datetime(earlier_start)) page.input(name="earlier-end", type="hidden", value=format_datetime(view_start)) @@ -341,14 +351,14 @@ page.input(name="end", type="hidden", value=format_datetime(view_end)) later_end = view_end + duration - page.label("Show later", for_="later", class_="later") + page.label(_("Show later"), for_="later", class_="later") page.input(name="later", id_="later", type="submit") page.input(name="later-start", type="hidden", value=format_datetime(view_end)) page.input(name="later-end", type="hidden", value=format_datetime(later_end)) if first_following: following_end = first_following + duration - page.label("Show later events", for_="later-events", class_="later-events") + page.label(_("Show later events"), for_="later-events", class_="later-events") page.input(name="later-events", id_="later-events", type="submit") page.input(name="later-events-start", type="hidden", value=format_datetime(first_following)) page.input(name="later-events-end", type="hidden", value=format_datetime(following_end)) @@ -414,6 +424,8 @@ "Show a description of the 'view_period'." + _ = self.get_translator() + page = self.page view_start = view_period.get_start() @@ -425,11 +437,13 @@ page.p(class_="view-period") if view_start and view_end: - page.add("Showing events from %s until %s" % (self.format_date(view_start, "full"), self.format_date(view_end, "full"))) + page.add(_("Showing events from %(start)s until %(end)s") % { + "start" : self.format_date(view_start, "full"), + "end" : self.format_date(view_end, "full")}) elif view_start: - page.add("Showing events from %s" % self.format_date(view_start, "full")) + page.add(_("Showing events from %s") % self.format_date(view_start, "full")) elif view_end: - page.add("Showing events until %s" % self.format_date(view_end, "full")) + page.add(_("Showing events until %s") % self.format_date(view_end, "full")) page.p.close() @@ -440,6 +454,8 @@ collections of the given 'participants'. """ + _ = self.get_translator() + # Obtain the user's timezone. tzid = self.get_tzid() @@ -451,7 +467,7 @@ period_groups = [request_summary, freebusy] period_group_types = ["request", "freebusy"] - period_group_sources = ["Pending requests", "Your schedule"] + period_group_sources = [_("Pending requests"), _("Your schedule")] for i, participant in enumerate(participants): period_groups.append(self.store.get_freebusy_for_other(self.user, get_uri(participant))) @@ -568,7 +584,9 @@ "Show the calendar for the current user." - self.new_page(title="Calendar") + _ = self.get_translator() + + self.new_page(title=_("Calendar")) page = self.page if self.handle_newevent(): @@ -756,6 +774,8 @@ the 'group_columns' defining the number of columns in each group. """ + _ = self.get_translator() + page = self.page # Determine the number of columns required. Where participants provide @@ -817,13 +837,13 @@ # Show a button for scheduling a new event. page.p(class_="newevent-with-periods") - page.label("Summary:") + page.label(_("Summary:")) page.input(name="summary-%d" % i, type="text") - page.input(name="newevent-%d" % i, type="submit", value="New event", accesskey="N") + page.input(name="newevent-%d" % i, type="submit", value=_("New event"), accesskey="N") page.p.close() page.p(class_="newevent-with-periods") - page.label("Clear selections", for_="reset", class_="reset") + page.label(_("Clear selections"), for_="reset", class_="reset") page.p.close() page.div.close() @@ -838,6 +858,8 @@ columns given by 'group_columns'. """ + _ = self.get_translator() + page = self.page # Obtain the user's timezone. @@ -968,7 +990,7 @@ if not p.summary or \ group_type != "request" and self._have_request(p.uid, p.recurrenceid, None, True): - page.span(p.summary or "(Participant is busy)") + page.span(p.summary or _("(Participant is busy)")) # Link to requests and events (including ones for # which counter-proposals exist). @@ -1045,11 +1067,13 @@ given 'colspan' configuring the cell's appearance. """ + _ = self.get_translator() + page = self.page page.td(class_="empty%s%s" % (point.indicator == Point.PRINCIPAL and " container" or "", at_end and " padding" or ""), colspan=colspan) if point.indicator == Point.PRINCIPAL: value, identifier = self._slot_value_and_identifier(point, endpoint) - page.label("Select/deselect period", class_="newevent popup", for_=identifier) + page.label(_("Select/deselect period"), class_="newevent popup", for_=identifier) page.td.close() def _day_value_and_identifier(self, day): diff -r 4bc73eb1e076 -r 819a0a8a3ae5 imipweb/event.py --- a/imipweb/event.py Wed Nov 04 23:35:16 2015 +0100 +++ b/imipweb/event.py Thu Nov 05 00:58:02 2015 +0100 @@ -138,6 +138,8 @@ "Show form controls for a request." + _ = self.get_translator() + page = self.page args = self.env.get_args() @@ -145,41 +147,41 @@ is_attendee = self.user in attendees if not self.obj.is_shared(): - page.p("This event has not been shared.") + page.p(_("This event has not been shared.")) # Show appropriate options depending on the role of the user. if is_attendee and not self.is_organiser(): - page.p("An action is required for this request:") + page.p(_("An action is required for this request:")) page.p() - self.control("reply", "submit", "Send reply") + self.control("reply", "submit", _("Send reply")) page.add(" ") - self.control("discard", "submit", "Discard event") + self.control("discard", "submit", _("Discard event")) page.add(" ") - self.control("ignore", "submit", "Return to the calendar", class_="ignore") + self.control("ignore", "submit", _("Return to the calendar"), class_="ignore") page.p.close() if self.is_organiser(): - page.p("As organiser, you can perform the following:") + page.p(_("As organiser, you can perform the following:")) page.p() - self.control("create", "submit", "Update event") + self.control("create", "submit", _("Update event")) page.add(" ") if self._get_counters(self.uid, self.recurrenceid): - self.control("uncounter", "submit", "Ignore counter-proposals") + self.control("uncounter", "submit", _("Ignore counter-proposals")) page.add(" ") if self.obj.is_shared() and not self._is_request(): - self.control("cancel", "submit", "Cancel event") + self.control("cancel", "submit", _("Cancel event")) else: - self.control("discard", "submit", "Discard event") + self.control("discard", "submit", _("Discard event")) page.add(" ") - self.control("ignore", "submit", "Return to the calendar", class_="ignore") + self.control("ignore", "submit", _("Return to the calendar"), class_="ignore") page.add(" ") - self.control("save", "submit", "Save without sending") + self.control("save", "submit", _("Save without sending")) page.p.close() def show_object_on_page(self, errors=None): @@ -189,6 +191,8 @@ a suitable message for the different errors provided. """ + _ = self.get_translator() + page = self.page page.form(method="POST") @@ -216,7 +220,7 @@ page.table(class_="object", cellspacing=5, cellpadding=5) page.thead() page.tr() - page.th("Event", class_="mainheading", colspan=3) + page.th(_("Event"), class_="mainheading", colspan=3) page.tr.close() page.thead.close() page.tbody() @@ -263,7 +267,7 @@ if replaced: page.td(class_="objectvalue %s replaced" % field, rowspan=2, colspan=2) - page.a("First occurrence replaced by a separate event", href=self.link_to(self.uid, replaced)) + page.a(_("First occurrence replaced by a separate event"), href=self.link_to(self.uid, replaced)) page.td.close() # NOTE: Should provide a way of editing recurrences when the @@ -272,7 +276,7 @@ elif excluded: page.td(class_="objectvalue %s excluded" % field, rowspan=2, colspan=2) - page.add("First occurrence excluded") + page.add(_("First occurrence excluded")) page.td.close() page.tr.close() @@ -283,7 +287,7 @@ page.tr() page.td(colspan=2) self.control("recur-add", "submit", "add", id="recur-add", class_="add") - page.label("Add a recurrence", for_="recur-add", class_="add") + page.label(_("Add a recurrence"), for_="recur-add", class_="add") page.td.close() page.tr.close() @@ -324,7 +328,7 @@ page.td(colspan=2) self.control("add", "submit", "add", id="add", class_="add") - page.label("Add attendee", for_="add", class_="add") + page.label(_("Add attendee"), for_="add", class_="add") page.td.close() page.tr.close() @@ -364,6 +368,8 @@ 'attendee' value, having 'attendee_attr' as any stored attributes. """ + _ = self.get_translator() + page = self.page args = self.env.get_args() @@ -411,10 +417,10 @@ remove_type = self.can_remove_attendee(attendee_uri) and "submit" or "checkbox" self.control("remove", remove_type, str(i), str(i) in args.get("remove", []), id="remove-%d" % i, class_="remove") - page.label("Remove", for_="remove-%d" % i, class_="remove") + page.label(_("Remove"), for_="remove-%d" % i, class_="remove") page.label(for_="remove-%d" % i, class_="removed") - page.add("(Uninvited)") - page.span("Re-invite", class_="action") + page.add(_("(Uninvited)")) + page.span(_("Re-invite"), class_="action") page.label.close() page.td.close() @@ -426,6 +432,8 @@ suitable message for the different errors provided. """ + _ = self.get_translator() + page = self.page # Obtain any parent object if this object is a specific recurrence. @@ -436,7 +444,7 @@ return page.p() - page.a("This event modifies a recurring event.", href=self.link_to(self.uid)) + page.a(_("This event modifies a recurring event."), href=self.link_to(self.uid)) page.p.close() # Obtain the periods associated with the event. @@ -446,7 +454,7 @@ if len(recurrences) < 1: return - page.p("This event occurs on the following occasions within the next %d days:" % self.get_window_size()) + page.p(_("This event occurs on the following occasions within the next %d days:") % self.get_window_size()) # Show each recurrence in a separate table. @@ -465,6 +473,8 @@ provided. """ + _ = self.get_translator() + page = self.page args = self.env.get_args() @@ -475,7 +485,7 @@ self.show_object_datetime_controls(period, index) page.table(cellspacing=5, cellpadding=5, class_="recurrence") - page.caption(period.origin == "RRULE" and "Occurrence from rule" or "Occurrence") + page.caption(period.origin == "RRULE" and _("Occurrence from rule") or _("Occurrence")) page.tbody() page.tr() @@ -506,10 +516,10 @@ str(index) in args.get("recur-remove", []), id="recur-remove-%d" % index, class_="remove") - page.label("Remove", for_="recur-remove-%d" % index, class_="remove") + page.label(_("Remove"), for_="recur-remove-%d" % index, class_="remove") page.label(for_="recur-remove-%d" % index, class_="removed") - page.add("(Removed)") - page.span("Re-add", class_="action") + page.add(_("(Removed)")) + page.span(_("Re-add"), class_="action") page.label.close() page.td.close() @@ -524,6 +534,8 @@ "Show any counter-proposals for the current object." + _ = self.get_translator() + page = self.page query = self.env.get_query() counter = query.get("counter", [None])[0] @@ -569,13 +581,13 @@ # Present the suggested attendees. if suggested_attendees: - page.p("The following attendees have been suggested for this event:") + page.p(_("The following attendees have been suggested for this event:")) page.table(cellspacing=5, cellpadding=5, class_="counters") page.thead() page.tr() - page.th("Attendee") - page.th("Suggested by...") + page.th(_("Attendee")) + page.th(_("Suggested by...")) page.tr.close() page.thead.close() page.tbody() @@ -599,17 +611,17 @@ # Present the suggested periods. if suggested_periods: - page.p("The following periods have been suggested for this event:") + page.p(_("The following periods have been suggested for this event:")) page.table(cellspacing=5, cellpadding=5, class_="counters") page.thead() page.tr() - page.th("Periods", colspan=2) - page.th("Suggested by...", rowspan=2) + page.th(_("Periods"), colspan=2) + page.th(_("Suggested by..."), rowspan=2) page.tr.close() page.tr() - page.th("Start") - page.th("End") + page.th(_("Start")) + page.th(_("End")) page.tr.close() page.thead.close() page.tbody() @@ -658,6 +670,8 @@ "Show conflicting events for the current object." + _ = self.get_translator() + page = self.page recurrenceids = self._get_active_recurrences(self.uid) @@ -713,14 +727,14 @@ # Show any conflicts with periods of actual attendance. if conflicts: - page.p("This event conflicts with others:") + page.p(_("This event conflicts with others:")) page.table(cellspacing=5, cellpadding=5, class_="conflicts") page.thead() page.tr() - page.th("Event") - page.th("Start") - page.th("End") + page.th(_("Event")) + page.th(_("Start")) + page.th(_("End")) page.tr.close() page.thead.close() page.tbody() @@ -740,7 +754,7 @@ if p.summary: page.a(p.summary, href=self.link_to(p.uid, p.recurrenceid)) else: - page.add("(Unspecified event)") + page.add(_("(Unspecified event)")) page.td.close() page.td(start) @@ -1265,7 +1279,9 @@ self.update_current_attendees() self.update_current_recurrences() - self.new_page(title="Event") + _ = self.get_translator() + + self.new_page(title=_("Event")) self.show_object_on_page(errors) return True diff -r 4bc73eb1e076 -r 819a0a8a3ae5 imipweb/profile.py --- a/imipweb/profile.py Wed Nov 04 23:35:16 2015 +0100 +++ b/imipweb/profile.py Thu Nov 05 00:58:02 2015 +0100 @@ -115,6 +115,8 @@ "Show the preferences, indicating any 'errors' in the output." + _ = self.get_translator() + page = self.page settings = self.get_current_preferences() prefs = self.get_preferences() @@ -145,10 +147,16 @@ page.th.close() page.td() + # For unrestricted fields, show a text field. + if not choices: page.input(name=name, value=(value or default), type="text", class_="preference", id_=name) + + # Otherwise, obtain the choices, localise the labels and show a + # menu control. + else: - choices = list(choices.items()) + choices = [(key, _(label)) for (key, label) in choices.items()] choices.sort() self.menu(name, default, choices, value is not None and [value] or None, class_="preference") diff -r 4bc73eb1e076 -r 819a0a8a3ae5 messages/en_GB.imip-agent.po --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/messages/en_GB.imip-agent.po Thu Nov 05 00:58:02 2015 +0100 @@ -0,0 +1,309 @@ +# Translations for imip-agent. +# Copyright (C) 2015 Paul Boddie +# This file is distributed under the same license as the imip-agent package. +# Paul Boddie , 2015. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: 4bc73eb1e076+\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-05 00:55+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +msgid "Calendar system message" +msgstr "" + +msgid "" +"This message contains several different parts, one of which will contain\n" +"calendar information that will only be understood by a suitable program.\n" +msgstr "" + +msgid "An addition to an event has been received." +msgstr "" + +msgid "An event cancellation has been received." +msgstr "" + +msgid "A counter proposal to an event invitation has been received." +msgstr "" + +msgid "Your counter proposal to an event invitation has been declined." +msgstr "" + +msgid "Details of an event have been received." +msgstr "" + +msgid "A request for updated event details has been received." +msgstr "" + +msgid "A reply to an event invitation has been received." +msgstr "" + +msgid "An event invitation has been received." +msgstr "" + +msgid "A free/busy update has been received." +msgstr "" + +msgid "A reply to a free/busy request has been received." +msgstr "" + +#, python-format +msgid "" +"If your mail program cannot handle this message, you may view the details " +"here:\n" +"\n" +"%s" +msgstr "" + +msgid "Add events" +msgstr "" + +msgid "Ignore requests" +msgstr "" + +msgid "Ask for refreshed event details" +msgstr "" + +msgid "Do not respond" +msgstr "" + +msgid "Always respond" +msgstr "" + +msgid "Never" +msgstr "" + +msgid "Always" +msgstr "" + +msgid "Do not notify" +msgstr "" + +msgid "Notify" +msgstr "" + +msgid "Publish" +msgstr "" + +msgid "Do not publish" +msgstr "" + +msgid "Share" +msgstr "" + +msgid "Do not share" +msgstr "" + +msgid "Original message only" +msgstr "" + +msgid "Original message followed by a separate summary message" +msgstr "" + +msgid "Summary message followed by the original message" +msgstr "" + +msgid "Summary message only" +msgstr "" + +msgid "Summary message wrapping the original message" +msgstr "" + +msgid "Anyone" +msgstr "" + +msgid "Existing attendees only" +msgstr "" + +msgid "Never allow organiser replacement" +msgstr "" + +msgid "Participate" +msgstr "" + +msgid "Do not participate" +msgstr "" + +#, python-format +msgid "New event at %s" +msgstr "" + +msgid "There are no pending requests." +msgstr "" + +msgid "Participants for scheduling:" +msgstr "" + +msgid "Remove" +msgstr "" + +msgid "Add" +msgstr "" + +msgid "Select days or periods for a new event." +msgstr "" + +msgid "Hide busy time periods" +msgstr "" + +msgid "Show busy time periods" +msgstr "" + +msgid "Show empty days" +msgstr "" + +msgid "Hide empty days" +msgstr "" + +msgid "Clear selections" +msgstr "" + +msgid "Show earlier events" +msgstr "" + +msgid "Show earlier" +msgstr "" + +msgid "Show later" +msgstr "" + +msgid "Show later events" +msgstr "" + +#, python-format +msgid "Showing events from %(start)s until %(end)s" +msgstr "" + +#, python-format +msgid "Showing events from %s" +msgstr "" + +#, python-format +msgid "Showing events until %s" +msgstr "" + +msgid "Pending requests" +msgstr "" + +msgid "Your schedule" +msgstr "" + +msgid "Calendar" +msgstr "" + +msgid "Summary:" +msgstr "" + +msgid "New event" +msgstr "" + +msgid "(Participant is busy)" +msgstr "" + +msgid "Select/deselect period" +msgstr "" + +msgid "This event has not been shared." +msgstr "" + +msgid "An action is required for this request:" +msgstr "" + +msgid "Send reply" +msgstr "" + +msgid "Discard event" +msgstr "" + +msgid "Return to the calendar" +msgstr "" + +msgid "As organiser, you can perform the following:" +msgstr "" + +msgid "Update event" +msgstr "" + +msgid "Ignore counter-proposals" +msgstr "" + +msgid "Cancel event" +msgstr "" + +msgid "Save without sending" +msgstr "" + +msgid "Event" +msgstr "" + +msgid "First occurrence replaced by a separate event" +msgstr "" + +msgid "First occurrence excluded" +msgstr "" + +msgid "Add a recurrence" +msgstr "" + +msgid "Add attendee" +msgstr "" + +msgid "(Uninvited)" +msgstr "" + +msgid "Re-invite" +msgstr "" + +msgid "This event modifies a recurring event." +msgstr "" + +#, python-format +msgid "This event occurs on the following occasions within the next %d days:" +msgstr "" + +msgid "Occurrence from rule" +msgstr "" + +msgid "Occurrence" +msgstr "" + +msgid "(Removed)" +msgstr "" + +msgid "Re-add" +msgstr "" + +msgid "The following attendees have been suggested for this event:" +msgstr "" + +msgid "Attendee" +msgstr "" + +msgid "Suggested by..." +msgstr "" + +msgid "The following periods have been suggested for this event:" +msgstr "" + +msgid "Periods" +msgstr "" + +msgid "Start" +msgstr "" + +msgid "End" +msgstr "" + +msgid "This event conflicts with others:" +msgstr "" + +msgid "(Unspecified event)" +msgstr "" diff -r 4bc73eb1e076 -r 819a0a8a3ae5 tools/i18n_format.sh --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tools/i18n_format.sh Thu Nov 05 00:58:02 2015 +0100 @@ -0,0 +1,8 @@ +#!/bin/sh + +DOMAIN=imip-agent + +for FILENAME in messages/*.po ; do + LOCALE_ID=`basename "$FILENAME" ".$DOMAIN.po"` + msgfmt -o "locale/$LOCALE_ID/LC_MESSAGES/$DOMAIN.mo" "$FILENAME" +done diff -r 4bc73eb1e076 -r 819a0a8a3ae5 tools/i18n_messages.sh --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tools/i18n_messages.sh Thu Nov 05 00:58:02 2015 +0100 @@ -0,0 +1,14 @@ +#!/bin/sh + +find imiptools imipweb -type f -name '*.py' | xargs xgettext -d imip-agent --no-location + + sed "s/SOME DESCRIPTIVE TITLE/Translations for imip-agent/" imip-agent.po \ +| sed "s/YEAR THE PACKAGE'S COPYRIGHT HOLDER/2015 Paul Boddie /" \ +| sed "s/FIRST AUTHOR , YEAR/Paul Boddie , 2015/" \ +| sed "s/PACKAGE VERSION/`hg id | cut -d ' ' -f 1`/" \ +| sed "s/PACKAGE/imip-agent/" \ +> imip-agent.pot + +for FILENAME in messages/*.po ; do + msgmerge --update "$FILENAME" imip-agent.pot +done