Baptiste Jonglez 10 years ago
parent
commit
b6d77a9a99

+ 2 - 1
coin/billing/apps.py

@@ -1,8 +1,9 @@
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
 from django.apps import AppConfig
+from django.utils.translation import ugettext_lazy as _
 
 
 class BillingConfig(AppConfig):
         name = "coin.billing"
-        verbose_name = "Billing"
+        verbose_name = _("Billing")

+ 10 - 7
coin/billing/models.py

@@ -9,6 +9,7 @@ from decimal import Decimal
 
 from django.db import models
 from django.db.models.signals import post_save
+from django.utils.translation import ugettext_lazy as _
 from django.dispatch import receiver
 
 from coin.offers.models import OfferSubscription
@@ -36,14 +37,15 @@ def invoice_pdf_filename(instance, filename):
 class Invoice(models.Model):
 
     INVOICES_STATUS_CHOICES = (
-        ('open', 'A payer'),
-        ('closed', 'Reglée'),
-        ('trouble', 'Litige')
+        ('open', _('To pay')),
+        ('closed', _('Paid')),
+        ('trouble', _('Trouble'))
     )
 
-    validated = models.BooleanField(default=False, verbose_name='validée',
-                                    help_text='Once validated, a PDF is generated'
-                                    ' and the invoice cannot be modified')
+    validated = models.BooleanField(default=False, verbose_name=_('validated'),
+                                    help_text=_('Once validated, a PDF '
+                                                'is generated and the invoice '
+                                                'cannot be modified.'))
     number = models.CharField(max_length=25,
                               default=next_invoice_number,
                               unique=True,
@@ -133,7 +135,8 @@ class Invoice(models.Model):
         return '#%s %0.2f€ %s' % (self.number, self.amount(), self.date_due)
 
     class Meta:
-        verbose_name = 'facture'
+        verbose_name = _('invoice')
+        verbose_name_plural = _('invoices')
 
 
 class InvoiceDetail(models.Model):

+ 2 - 1
coin/configuration/admin.py

@@ -2,6 +2,7 @@
 from __future__ import unicode_literals
 
 from django.contrib import admin
+from django.utils.translation import ugettext_lazy as _
 from polymorphic.admin import PolymorphicParentModelAdmin, PolymorphicChildModelAdmin
 
 from coin.resources.models import IPSubnet
@@ -26,7 +27,7 @@ class ParentConfigurationAdmin(PolymorphicParentModelAdmin):
 
     def offer_subscription_member(self, config):
         return config.offersubscription.member
-    offer_subscription_member.short_description = 'Membre'
+    offer_subscription_member.short_description = _("Member")
 
     def get_child_models(self):
         """

+ 2 - 1
coin/configuration/apps.py

@@ -1,8 +1,9 @@
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
 from django.apps import AppConfig
+from django.utils.translation import ugettext_lazy as _
 
 
 class ConfigurationConfig(AppConfig):
         name = "coin.configuration"
-        verbose_name = "Configurations"
+        verbose_name = _("Configurations")

+ 9 - 7
coin/configuration/models.py

@@ -2,12 +2,13 @@
 from __future__ import unicode_literals
 
 from django.db import models
-from polymorphic import PolymorphicModel
-from coin.offers.models import OfferSubscription
 from django.db.models.signals import post_save, post_delete
+from django.utils.translation import ugettext_lazy as _
 from django.core.exceptions import ObjectDoesNotExist
 from django.dispatch import receiver
+from polymorphic import PolymorphicModel
 
+from coin.offers.models import OfferSubscription
 from coin.resources.models import IPSubnet
 
 """
@@ -25,9 +26,9 @@ class Configuration(PolymorphicModel):
 
     offersubscription = models.OneToOneField(OfferSubscription,
                                              related_name='configuration',
-                                             verbose_name='abonnement')
+                                             verbose_name=_('subscription'))
     comment = models.CharField(blank=True, max_length=512,
-                               verbose_name="commentaire")
+                               verbose_name=_("comment"))
 
     @staticmethod
     def get_configurations_choices_list():
@@ -40,11 +41,11 @@ class Configuration(PolymorphicModel):
     
     def model_name(self):
         return self.__class__.__name__
-    model_name.short_description = 'Nom du modèle'
+    model_name.short_description = _('Model name')
 
     def configuration_type_name(self):
         return self._meta.verbose_name
-    configuration_type_name.short_description = 'Type'
+    configuration_type_name.short_description = _('Type')
 
     def get_absolute_url(self):
         """
@@ -67,7 +68,8 @@ class Configuration(PolymorphicModel):
             return self.model_name().lower()
 
     class Meta:
-        verbose_name = 'configuration'
+        verbose_name = _('configuration')
+        verbose_name_plural = _('configurations')
 
 
 @receiver(post_save, sender=IPSubnet)

+ 10 - 8
coin/isp_database/admin.py

@@ -2,9 +2,16 @@
 from __future__ import unicode_literals
 
 from django.contrib import admin
+from django.utils.translation import ugettext_lazy as _
 
 from coin.isp_database.models import ISPInfo, RegisteredOffice, OtherWebsite, ChatRoom, CoveredArea, BankInfo
 
+extra_desc = _("These fields are not part of the db.ffdn.org specification, "
+               "but are used in a few places on the website.")
+bankinfo_desc = _("Bank information are not part of the db.ffdn.org "
+                  "specification, but are used in a few places on the website "
+                  "(for billing, for instance).")
+
 
 class SingleInstanceAdminMixin(object):
     """Hides the "Add" button when there is already an instance"""
@@ -25,11 +32,10 @@ class RegisteredOfficeInline(admin.StackedInline):
              ('region', 'country_name'))}),
         ('Extras', {
              'fields': ('siret',),
-             'description': 'Ces champs ne font pas partie de la spécification db.ffdn.org mais sont utilisés sur le site'})
+             'description': extra_desc})
         )
 
 
-
 class OtherWebsiteInline(admin.StackedInline):
     model = OtherWebsite
     extra = 0
@@ -51,10 +57,7 @@ class BankInfoInline(admin.StackedInline):
 
     fieldsets = (('', {
                 'fields': ('iban', 'bic', 'bank_name', 'check_order'),
-                'description': (
-                    'Les coordonnées bancaires ne font pas partie de la'+
-                    ' spécification db.ffdn.org mais sont utilisées par le'+
-                    ' site (facturation notamment).')
+                'description': bankinfo_desc
     }),)
 
 
@@ -73,8 +76,7 @@ class ISPInfoAdmin(SingleInstanceAdminMixin, admin.ModelAdmin):
             'website')}),
         ('Extras', {
             'fields': ('administrative_email', 'support_email', 'lists_url'),
-            'description':
-                'Ces champs ne font pas partie de la spécification db.ffdn.org mais sont utilisés sur le site'
+            'description': extra_desc
         }),
     )
 

+ 2 - 1
coin/isp_database/apps.py

@@ -1,8 +1,9 @@
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
 from django.apps import AppConfig
+from django.utils.translation import ugettext_lazy as _
 
 
 class ISPDatabaseConfig(AppConfig):
         name = "coin.isp_database"
-        verbose_name = "ISP database"
+        verbose_name = _("ISP database")

+ 95 - 61
coin/isp_database/models.py

@@ -4,7 +4,7 @@ from __future__ import unicode_literals
 from django.db import models
 from django.core.validators import MaxValueValidator
 from django.core.exceptions import ValidationError
-
+from django.utils.translation import ugettext_lazy as _
 from localflavor.generic.models import IBANField, BICField
 from localflavor.fr.models import FRSIRETField
 
@@ -15,18 +15,20 @@ from coin import utils
 # API version, see http://db.ffdn.org/format
 API_VERSION = 0.1
 
-TECHNOLOGIES = (('ftth', 'FTTH'),
-                ('dsl', '*DSL'),
-                ('wifi', 'WiFi'))
+TECHNOLOGIES = (('ftth', _('FTTH')),
+                ('dsl', _('xDSL')),
+                ('wifi', _('Wi-Fi')))
 
 
 class SingleInstanceMixin(object):
     """Makes sure that no more than one instance of a given model is created."""
 
     def clean(self):
+        from django.utils.translation import ugettext as _
         model = self.__class__
         if (model.objects.count() > 0 and self.id != model.objects.get().id):
-            raise ValidationError("Can only create 1 instance of %s" % model.__name__)
+            error = _("Can only create 1 instance of {model}.")
+            raise ValidationError(error.format(model=model.__name__))
         super(SingleInstanceMixin, self).clean()
 
 
@@ -36,39 +38,43 @@ class ISPInfo(SingleInstanceMixin, models.Model):
     The naming convention is different from Python/django so that it
     matches exactly the format (which uses CamelCase...)
     """
-    name = models.CharField(max_length=512,
-                            help_text="The ISP's name")
+    name = models.CharField(max_length=512, verbose_name=_("name"),
+                            help_text=_("The ISP's name"))
     # Length required by the spec
     shortname = models.CharField(max_length=15, blank=True,
-                                 help_text="Shorter name")
-    description = models.TextField(blank=True,
-                                   help_text="Short text describing the project")
-    logoURL = models.URLField(blank=True,
-                              verbose_name="logo URL",
-                              help_text="HTTP(S) URL of the ISP's logo")
-    website = models.URLField(blank=True,
-                              help_text='URL to the official website')
-    email = models.EmailField(max_length=254,
-                              help_text="Contact email address")
+                                 verbose_name=_("short name"),
+                                 help_text=_("Shorter name"))
+    description = models.TextField(blank=True, verbose_name=_("description"),
+                                   help_text=_("Short text describing the project"))
+    logoURL = models.URLField(blank=True, verbose_name=_("logo URL"),
+                              help_text=_("HTTP(S) URL of the ISP's logo"))
+    website = models.URLField(blank=True, verbose_name=_("website"),
+                              help_text=_('URL to the official website'))
+    email = models.EmailField(max_length=254, verbose_name=_("email"),
+                              help_text=_("Contact email address"))
     mainMailingList = models.EmailField(max_length=254, blank=True,
-                                        verbose_name="main mailing list",
-                                        help_text="Main public mailing-list")
+                                        verbose_name=_("main mailing list"),
+                                        help_text=_("Main public mailing-list"))
     creationDate = models.DateField(blank=True, null=True,
-                                    verbose_name="creation date",
-                                     help_text="Date of creation for legal structure")
+                                    verbose_name=_("creation date"),
+                                    help_text=_("Date of creation for legal structure"))
     ffdnMemberSince = models.DateField(blank=True, null=True,
-                                       verbose_name="FFDN member since",
-                                       help_text="Date at wich the ISP joined the Federation")
+                                       verbose_name=_("FFDN member since"),
+                                       help_text=_("Date at wich the ISP joined the Federation"))
     # TODO: choice field
     progressStatus = models.PositiveSmallIntegerField(
         validators=[MaxValueValidator(7)],
-        blank=True, null=True, verbose_name='progress status',
-        help_text="Progression status of the ISP")
+        blank=True, null=True, verbose_name=_('progress status'),
+        help_text=_("Progression status of the ISP"))
     # TODO: better model for coordinates
     latitude = models.FloatField(blank=True, null=True,
-        help_text="Coordinates of the registered office (latitude)")
+                                 verbose_name=_("latitude"),
+                                 help_text=_("Coordinates of the registered "
+                                 "office (latitude)"))
     longitude = models.FloatField(blank=True, null=True,
-        help_text="Coordinates of the registered office (longitude)")
+                                  verbose_name=_("longitude"),
+                                  help_text=_("Coordinates of the registered "
+                                  "office (longitude)"))
 
     # Uncomment this if you want to manage these counters by hand.
     #member_count = models.PositiveIntegerField(help_text="Number of members")
@@ -77,16 +83,17 @@ class ISPInfo(SingleInstanceMixin, models.Model):
 
     # field outside of db-ffdn format:
     administrative_email = models.EmailField(
-        max_length=254, blank=True, verbose_name="contact administratif",
-        help_text='Adresse email pour les contacts administratifs (ex: bureau)')
+        max_length=254, blank=True, verbose_name=_("administrative contact email"),
+        help_text=_("Email address to use for administrative inquiries "
+                    "(e.g. membership, personal information)"))
 
     support_email = models.EmailField(
-        max_length=254, blank=True, verbose_name="contact de support",
-        help_text="Adresse email pour les demandes de support technique")
+        max_length=254, blank=True, verbose_name=_("support contact email"),
+        help_text=_("Email address to use for technical support"))
 
     lists_url = models.URLField(
-        verbose_name="serveur de listes", blank=True,
-        help_text="URL du serveur de listes de discussions/diffusion")
+        verbose_name=_("mailing list server"), blank=True,
+        help_text=_("URL of the mailing list server"))
 
     @property
     def memberCount(self):
@@ -105,8 +112,9 @@ class ISPInfo(SingleInstanceMixin, models.Model):
 
     @property
     def main_chat_verbose(self):
+        from django.utils.translation import ugettext as _
         m = utils.re_chat_url.match(self.chatroom_set.first().url)
-        return '{channel} sur {server}'.format(**(m.groupdict()))
+        return _('{channel} on {server}').format(**(m.groupdict()))
 
     def get_absolute_url(self):
         return '/isp.json'
@@ -149,23 +157,36 @@ class ISPInfo(SingleInstanceMixin, models.Model):
     def __unicode__(self):
         return self.name
 
+    class Meta:
+        verbose_name = _('ISP details')
+        verbose_name_plural = _('ISP details')
+
 
 class OtherWebsite(models.Model):
-    name = models.CharField(max_length=512)
-    url = models.URLField(verbose_name="URL")
-    isp = models.ForeignKey(ISPInfo)
+    name = models.CharField(max_length=512, verbose_name=_("name"))
+    url = models.URLField(verbose_name=_("URL"))
+    isp = models.ForeignKey(ISPInfo, verbose_name=_("ISP"))
+
+    class Meta:
+        verbose_name = _('other website')
+        verbose_name_plural = _('other websites')
 
 
 class RegisteredOffice(models.Model):
     """ http://json-schema.org/address """
-    post_office_box = models.CharField(max_length=512, blank=True)
-    extended_address = models.CharField(max_length=512, blank=True)
-    street_address = models.CharField(max_length=512, blank=True)
-    locality = models.CharField(max_length=512)
-    region = models.CharField(max_length=512)
-    postal_code = models.CharField(max_length=512, blank=True)
-    country_name = models.CharField(max_length=512)
-    isp = models.OneToOneField(ISPInfo)
+    post_office_box = models.CharField(max_length=512, blank=True,
+                                       verbose_name=_("post office box"))
+    extended_address = models.CharField(max_length=512, blank=True,
+                                        verbose_name=_("extended address"))
+    street_address = models.CharField(max_length=512, blank=True,
+                                      verbose_name=_("street address"))
+    locality = models.CharField(max_length=512, verbose_name=_("locality"))
+    region = models.CharField(max_length=512, verbose_name=_("region"))
+    postal_code = models.CharField(max_length=512, blank=True,
+                                   verbose_name=_("postal code"))
+    country_name = models.CharField(max_length=512,
+                                    verbose_name=_("country name"))
+    isp = models.OneToOneField(ISPInfo, verbose_name=_("ISP"))
 
     # not in db.ffdn.org spec
     siret = FRSIRETField('SIRET')
@@ -179,41 +200,54 @@ class RegisteredOffice(models.Model):
                 d[key] = getattr(self, field)
         return d
 
+    class Meta:
+        verbose_name = _('registered office')
+        verbose_name_plural = _('registered offices')
+
 
 class ChatRoom(models.Model):
-    url = models.CharField(verbose_name="URL", max_length=256)
-    isp = models.ForeignKey(ISPInfo)
+    url = models.CharField(verbose_name=_("URL"), max_length=256)
+    isp = models.ForeignKey(ISPInfo, verbose_name=_("ISP"))
+
+    class Meta:
+        verbose_name = _('chat room')
+        verbose_name_plural = _('chat rooms')
 
 
 class CoveredArea(models.Model):
-    name = models.CharField(max_length=512)
+    name = models.CharField(max_length=512, verbose_name=_("name"))
     # TODO: we must allow multiple values
-    technologies = models.CharField(choices=TECHNOLOGIES, max_length=16)
+    technologies = models.CharField(choices=TECHNOLOGIES, max_length=16,
+                                    verbose_name=_("technologies"))
     # TODO: find a geojson library
     #area =
-    isp = models.ForeignKey(ISPInfo)
+    isp = models.ForeignKey(ISPInfo, verbose_name=_("ISP"))
 
     def to_dict(self):
         return {"name": self.name,
                 "technologies": [self.technologies]}
 
+    class Meta:
+        verbose_name = _('covered area')
+        verbose_name_plural = _('covered areas')
+
 
 class BankInfo(models.Model):
     """Information about bank account and the bank itself
 
     This is out of the scope of db.ffdn.org spec.
     """
-    isp = models.OneToOneField(ISPInfo)
-    iban = IBANField('IBAN')
-    bic = BICField('BIC', blank=True, null=True)
-    bank_name = models.CharField('établissement bancaire',
-                                 max_length=100, blank=True, null=True)
-    check_order = models.CharField('ordre',
+    isp = models.OneToOneField(ISPInfo, verbose_name=_("ISP"))
+    iban = IBANField(verbose_name=_('IBAN'))
+    bic = BICField(verbose_name=_('BIC'), blank=True, null=True)
+    bank_name = models.CharField(verbose_name=_("bank name"), max_length=100,
+                                 blank=True, null=True)
+    check_order = models.CharField(verbose_name=_("check recipient"),
                                    max_length=100, blank=False, null=False,
-                                   help_text='Ordre devant figurer sur un \
-                                   chèque bancaire à destination de\
-                                   l\'association')
+                                   help_text=_("Recipient that must appear on "
+                                               "a bank check destinated to "
+                                               "the ISP"))
 
     class Meta:
-        verbose_name = 'coordonnées bancaires'
-        verbose_name_plural = verbose_name
+        verbose_name = _('bank information')
+        verbose_name_plural = _('banks information')

+ 2 - 1
coin/members/apps.py

@@ -1,8 +1,9 @@
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
 from django.apps import AppConfig
+from django.utils.translation import ugettext_lazy as _
 
 
 class MembersConfig(AppConfig):
         name = "coin.members"
-        verbose_name = "Members"
+        verbose_name = _("Members")

+ 76 - 60
coin/members/models.py

@@ -7,6 +7,8 @@ import datetime
 from django.db import models
 from django.db.models import Q
 from django.db.models.signals import pre_save
+from django.utils.translation import ugettext_lazy as _
+from django.utils.text import capfirst
 from django.dispatch import receiver
 from django.contrib.auth.models import AbstractUser
 from django.conf import settings
@@ -25,51 +27,51 @@ class Member(CoinLdapSyncMixin, AbstractUser):
     REQUIRED_FIELDS = ['first_name', 'last_name', 'email', ]
 
     MEMBER_TYPE_CHOICES = (
-        ('natural_person', 'Personne physique'),
-        ('legal_entity', 'Personne morale'),
+        ('natural_person', _("Individual")),
+        ('legal_entity', _("Organisation")),
     )
     MEMBER_STATUS_CHOICES = (
-        ('member', 'Adhérent'),
-        ('not_member', 'Non adhérent'),
-        ('pending', "Demande d'adhésion"),
+        ('member', _("Member")),
+        ('not_member', _("Not a member")),
+        ('pending', _("Pending membership")),
     )
 
     status = models.CharField(max_length=50, choices=MEMBER_STATUS_CHOICES,
-                              default='member', verbose_name='statut')
+                              default='member', verbose_name=_('status'))
     type = models.CharField(max_length=20, choices=MEMBER_TYPE_CHOICES,
-                            default='natural_person', verbose_name='type')
+                            default='natural_person', verbose_name=_("type"))
 
     nickname = models.CharField(max_length=64, blank=True,
-                                verbose_name="nom d'usage",
-                                help_text='Pseudonyme, …')
+                                verbose_name=_("nickname"),
+                                help_text=_("Pseudonym, …"))
     organization_name = models.CharField(max_length=200, blank=True,
-                                         verbose_name="nom de l'organisme",
-                                         help_text='Pour une personne morale')
+                                         verbose_name=_("organisation name"),
+                                         help_text=_("For organisations"))
     home_phone_number = models.CharField(max_length=25, blank=True,
-                                         verbose_name='téléphone fixe')
+                                         verbose_name=_("home phone number"))
     mobile_phone_number = models.CharField(max_length=25, blank=True,
-                                           verbose_name='téléphone mobile')
+                                           verbose_name=_("mobile phone number"))
     # TODO: use a django module that provides an address model? (would
     # support more countries and address types)
     address = models.TextField(
-        verbose_name='adresse postale', blank=True, null=True)
+        verbose_name=_("physical address"), blank=True, null=True)
+    validator = RegexValidator(regex=r'^\d{5}$',
+                               message=_("Invalid postal code."))
     postal_code = models.CharField(max_length=5, blank=True, null=True,
-                                   validators=[RegexValidator(regex=r'^\d{5}$',
-                                                              message='Code postal non valide.')],
-                                   verbose_name='code postal')
+                                   validators=[validator],
+                                   verbose_name=_("postal code"))
     city = models.CharField(max_length=200, blank=True, null=True,
-                            verbose_name='commune')
+                            verbose_name=_("city"))
     country = models.CharField(max_length=200, blank=True, null=True,
                                default='France',
-                               verbose_name='pays')
+                               verbose_name=_("country"))
     resign_date = models.DateField(null=True, blank=True,
-                                   verbose_name="date de départ de "
-                                   "l'association",
-                                   help_text="En cas de départ prématuré")
-    comments = models.TextField(blank=True, verbose_name='commentaires',
-                                help_text="Commentaires libres (informations"
-                                " spécifiques concernant l'adhésion,"
-                                " raison du départ, etc)")
+                                   verbose_name=_("resign date of membership"),
+                                   help_text=_("In case of premature leaving"))
+    comments = models.TextField(blank=True, verbose_name=_("comments"),
+                                help_text=_("Free-form comments (specific"
+                                            " membership information, reason"
+                                            " for leaving, etc)"))
 
     # Following fields are managed by the parent class AbstractUser :
     # username, first_name, last_name, email
@@ -82,22 +84,26 @@ class Member(CoinLdapSyncMixin, AbstractUser):
     _password_ldap = None
 
     def clean(self):
+        from django.utils.translation import ugettext as _
         if self.type == 'legal_entity':
             if not self.organization_name:
-                raise ValidationError("Le nom de l'organisme est obligatoire "
-                                      "pour une personne morale")
+                raise ValidationError(_("An organisation name is mandatory for"
+                                        " organisations."))
         elif self.type == 'natural_person':
             if not (self.first_name and self.last_name):
-                raise ValidationError("Le nom et prénom sont obligatoires "
-                                      "pour une personne physique")
+                raise ValidationError(_("First name and last name are mandatory"
+                                        " for individuals."))
 
     def __unicode__(self):
+        from django.utils.translation import ugettext as _
         if self.type == 'legal_entity':
             return self.organization_name
         elif self.nickname:
             return self.nickname
         else:
-            return self.first_name + ' ' + self.last_name
+            return _("{first_name} {last_name}").format(
+                first_name=self.first_name,
+                last_name=self.last_name)
 
     def get_full_name(self):
         return str(self)
@@ -110,7 +116,7 @@ class Member(CoinLdapSyncMixin, AbstractUser):
         x = self.membership_fees.order_by('-end_date')
         if x:
             return self.membership_fees.order_by('-end_date')[0].end_date
-    end_date_of_membership.short_description = "Date de fin d'adhésion"
+    end_date_of_membership.short_description = _("End date of membership")
 
     def is_paid_up(self):
         """
@@ -167,7 +173,7 @@ class Member(CoinLdapSyncMixin, AbstractUser):
         """
         Update LDAP data when a member is saved
         """
-
+        from django.utils.translation import ugettext as _
         # Do not perform LDAP query if no usefull fields to update are specified
         # in update_fields
         # Ex : at login, last_login field is updated by django auth module.
@@ -175,8 +181,8 @@ class Member(CoinLdapSyncMixin, AbstractUser):
             return
 
         # Fail if no username specified
-        assert self.username, ('Can\'t sync with LDAP because missing username '
-                               'value for the Member : %s' % self)
+        assert self.username, _("Can't sync with LDAP because missing username "
+                                "value for Member <{name}>").format(name=str(self))
 
         # If try to sync a superuser in creation mode
         # Try to retrieve the user in ldap. If exists, switch to update mode
@@ -231,8 +237,9 @@ class Member(CoinLdapSyncMixin, AbstractUser):
         """
         Delete member from the LDAP
         """
-        assert self.username, ('Can\'t delete from LDAP because missing '
-                               'username value for the Member : %s' % self)
+        from django.utils.translation import ugettext as _
+        assert self.username, _("Can't delete from LDAP because missing username "
+                                "value for Member <{name}>").format(name=str(self))
 
         # Delete user from LDAP
         ldap_user = LdapUser.objects.get(pk=self.username)
@@ -256,7 +263,8 @@ class Member(CoinLdapSyncMixin, AbstractUser):
                                    context={'member': self, 'branding':ISPInfo.objects.first()})
 
     class Meta:
-        verbose_name = 'membre'
+        verbose_name = _('member')
+        verbose_name_plural = _('members')
 
 # Hack to force email to be required by Member model
 Member._meta.get_field('email')._unique = True
@@ -289,7 +297,8 @@ def get_automatic_username(member):
         # Concaténer avec nom de famille
         username = ('%s%s' % (first_name_letters, member.last_name))
     else:
-        raise Exception('Il n\'y a pas sufissement d\'informations pour déterminer un login automatiquement')
+        raise Exception(_("Not enough information provided to generate "
+                          "a login for the member."))
 
     # Remplacer ou enlever les caractères non ascii
     username = unicodedata.normalize('NFD', username)\
@@ -321,12 +330,12 @@ def get_automatic_username(member):
 
 class CryptoKey(CoinLdapSyncMixin, models.Model):
 
-    KEY_TYPE_CHOICES = (('RSA', 'RSA'), ('GPG', 'GPG'))
+    KEY_TYPE_CHOICES = (('RSA', _('RSA')), ('GPG', _('GPG')))
 
     type = models.CharField(max_length=3, choices=KEY_TYPE_CHOICES,
-                            verbose_name='type')
-    key = models.TextField(verbose_name='clé')
-    member = models.ForeignKey('Member', verbose_name='membre')
+                            verbose_name=_('type'))
+    key = models.TextField(verbose_name=_('key'))
+    member = models.ForeignKey('Member', verbose_name=_('member'))
 
     def sync_to_ldap(self, creation, *args, **kwargs):
         """Simply tell the member object to resync all its SSH keys to LDAP"""
@@ -336,44 +345,50 @@ class CryptoKey(CoinLdapSyncMixin, models.Model):
         self.member.sync_ssh_keys()
 
     def __unicode__(self):
-        return 'Clé %s de %s' % (self.type, self.member)
+        from django.utils.translation import ugettext as _
+        readable_type = capfirst(self.get_type_display())
+        return _('{type} key of {user}').format(type=readable_type,
+                                                user=self.member)
 
     class Meta:
-        verbose_name = 'clé'
+        verbose_name = _('key')
+        verbose_name_plural = _('keys')
 
 
 class MembershipFee(models.Model):
     PAYMENT_METHOD_CHOICES = (
-        ('cash', 'Espèces'),
-        ('check', 'Chèque'),
-        ('transfer', 'Virement'),
-        ('other', 'Autre')
+        ('cash', _('Cash')),
+        ('check', _('Check')),
+        ('transfer', _('Transfer')),
+        ('other', _('Other'))
     )
 
     member = models.ForeignKey('Member', related_name='membership_fees',
-                               verbose_name='membre')
+                               verbose_name=_('member'))
     amount = models.DecimalField(null=False, max_digits=5, decimal_places=2,
                                  default=settings.MEMBER_DEFAULT_COTISATION,
-                                 verbose_name='montant', help_text='en €')
+                                 verbose_name=_('amount'),
+                                 help_text=_('in €'))
     start_date = models.DateField(
         null=False,
         blank=False,
-        verbose_name='date de début de cotisation')
+        verbose_name=_('start date of membership fee'))
     end_date = models.DateField(
         null=False,
         blank=True,
-        verbose_name='date de fin de cotisation',
-        help_text='par défaut, la cotisation dure un an')
+        verbose_name=_('end date of membership fee'),
+        help_text=_('By default, a membership fee covers one year'))
 
     payment_method = models.CharField(max_length=100, null=True, blank=True,
                                       choices=PAYMENT_METHOD_CHOICES,
-                                      verbose_name='moyen de paiement')
+                                      verbose_name=_('payment method'))
     reference = models.CharField(max_length=125, null=True, blank=True,
-                                 verbose_name='référence du paiement',
-                                 help_text='numéro de chèque, '
-                                 'référence de virement, commentaire...')
+                                 verbose_name=_('payment reference'),
+                                 help_text=_('Check number, '
+                                             'reference of bank transfer, '
+                                             'comment…'))
     payment_date = models.DateField(null=True, blank=True,
-                                    verbose_name='date du paiement')
+                                    verbose_name=_('date of payment'))
 
     def clean(self):
         if self.end_date is None:
@@ -383,7 +398,8 @@ class MembershipFee(models.Model):
         return '%s - %s - %i€' % (self.member, self.start_date, self.amount)
 
     class Meta:
-        verbose_name = 'cotisation'
+        verbose_name = _('membership fee')
+        verbose_name_plural = _('membership fees')
 
 
 class LdapUser(ldapdb.models.Model):

+ 9 - 8
coin/members/templates/members/contact.html

@@ -1,21 +1,22 @@
 {% extends "base.html" %}
+{% load i18n %}
 
 {% block content %}
 <div class="row">
     <div class="large-12 columns">
-        <h2>Contact / Support</h2>
+        <h2>{% trans 'Contact and support' %}</h2>
         <div class="panel">
-            <h3>Courriel</h3>
+            <h3>{% trans 'By email' %}</h3>
             <p>
-              <a href="mailto:{{ branding.email }}">{{ branding.email }}</a> (questions générales)<br />
-              <a href="mailto:{{ branding.administrative_email }}">{{ branding.administrative_email }}</a> (questions administratives)<br />
-              <a href="mailto:{{ branding.support_email }}">{{ branding.support_email }}</a> (support technique)
+              <a href="mailto:{{ branding.email }}">{{ branding.email }}</a> {% trans '(general questions)' %}<br />
+              <a href="mailto:{{ branding.administrative_email }}">{{ branding.administrative_email }}</a> {% trans '(administrative questions)' %}<br />
+              <a href="mailto:{{ branding.support_email }}">{{ branding.support_email }}</a> {% trans '(technical support)' %}
             </p>
             {% if branding.lists_url %}
-            <h3>Listes de discussion</h3>
-            <p>Gérer ses abonnements aux listes de discussion et diffusion : <a href="{{ branding.lists_url }}">{{ branding.lists_url }}</a></p>
+            <h3>{% trans 'Mailing lists' %}</h3>
+            <p>{% trans 'Manage your subscription to the various mailing lists at:' %} <a href="{{ branding.lists_url }}">{{ branding.lists_url }}</a></p>
             {% endif %}
-            <h3>IRC</h3>
+            <h3>{% trans 'IRC' %}</h3>
             <p><a href="{{ branding.chatroom_set.first.url }}">{{ branding.main_chat_verbose }}</a></p>
         </div>
     </div>

+ 6 - 5
coin/members/templates/members/index.html

@@ -1,20 +1,21 @@
 {% extends "base.html" %}
+{% load i18n %}
 
 {% block content %}
 <div class="row">
     <div class="large-12 columns">
-        <h2>Tableau de bord</h2>
+        <h2>{% trans "Dashboard" %}</h2>
     </div>
 </div>
 
 <div class="row">
     <div class="large-6 columns">
-        <h3>Alertes</h3>
-        <div class="panel">Ici les news de santé des serveurs, etc.</div>
+        <h3>{% trans "Flash alerts" %}</h3>
+        <div class="panel">{% trans "News of services outages, servers, etc." %}</div>
     </div>
     <div class="large-6 columns">
-        <h3>Stats</h3>
-        <div class="panel">Use MOAR bandwidth !</div>
+        <h3>{% trans "Statistics" %}</h3>
+        <div class="panel">{% trans "Use MOAR bandwidth!" %}</div>
     </div>
     {% if has_isp_feed %}
     <div class="large-6 columns">

+ 7 - 6
coin/members/templates/members/invoices.html

@@ -1,15 +1,16 @@
 {% extends "base.html" %}
+{% load i18n %}
 
 {% block content %}
-<h2>Mes factures</h2>
+<h2>{% trans "My invoices" %}</h2>
 
 <table id="member_invoices" class="full-width">
     <thead>
         <tr>
-            <th>Numéro</th>
-            <th>Date</th>
-            <th>Montant</th>
-            <th>Reste à payer</th>
+            <th>{% trans "Identifier" %}</th>
+            <th>{% trans "Date" %}</th>
+            <th>{% trans "Amount" %}</th>
+            <th>{% trans "Amount still due" %}</th>
             <th></th>
         </tr>
     </thead>
@@ -20,7 +21,7 @@
             <td>{{ invoice.date }}</td>
             <td>{{ invoice.amount }}</td>
             <td{% if invoice.amount_remaining_to_pay > 0 %} class="unpaid"{% endif %}>{{ invoice.amount_remaining_to_pay }}</td>
-            <td>{% if invoice.is_pdf_exists %}<a href="{% url 'billing:invoice_pdf' id=invoice.number %}"><i class="fa fa-file-pdf-o"></i> PDF</a>{% endif %}</td>
+            <td>{% if invoice.is_pdf_exists %}<a href="{% url 'billing:invoice_pdf' id=invoice.number %}"><i class="fa fa-file-pdf-o"></i> {% trans "PDF" %}</a>{% endif %}</td>
         </tr>
         {% endfor %}
     </tbody>

+ 2 - 1
coin/members/templates/members/subscriptions.html

@@ -1,7 +1,8 @@
 {% extends "base.html" %}
+{% load i18n %}
 
 {% block content %}
-<h2>Mes abonnements</h2>
+<h2>{% trans "My subscriptions" %}</h2>
 
 <table id="member_subscriptions" class="full-width">
     <thead>

+ 7 - 4
coin/mixins.py

@@ -3,6 +3,7 @@ from __future__ import unicode_literals
 
 from django.db import transaction
 from django.conf import settings
+from django.utils.translation import ugettext_lazy as _
 
 
 class CoinLdapSyncMixin(object):
@@ -15,14 +16,16 @@ class CoinLdapSyncMixin(object):
     base de données échoue à son tour et rien n'est sauvegardé afin de conserver
     la cohérence entre base de donnée et LDAP
     """
+    error = _("You must implement the {method} method when inheriting "
+              "from {class_name}.")
 
     def sync_to_ldap(self, creation, *args, **kwargs):
-        raise NotImplementedError('Using CoinLdapSyncModel require '
-                                  'sync_to_ldap method being implemented')
+        raise NotImplementedError(self.error.format(method="sync_to_ldap",
+                                                    class_name=self.__class__.__name__))
 
     def delete_from_ldap(self, *args, **kwargs):
-        raise NotImplementedError('Using CoinLdapSyncModel require '
-                                  'delete_from_ldap method being implemented')
+        raise NotImplementedError(self.error.format(method="delete_from_ldap",
+                                                    class_name=self.__class__.__name__))
 
     @transaction.atomic
     def save(self, *args, **kwargs):

+ 2 - 1
coin/offers/apps.py

@@ -1,8 +1,9 @@
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
 from django.apps import AppConfig
+from django.utils.translation import ugettext_lazy as _
 
 
 class OffersConfig(AppConfig):
         name = "coin.offers"
-        verbose_name = "Offers"
+        verbose_name = _("Offers")

+ 31 - 24
coin/offers/models.py

@@ -6,6 +6,7 @@ import datetime
 from django.db import models
 from django.db.models import Q
 from django.core.validators import MinValueValidator
+from django.utils.translation import ugettext_lazy as _
 
 
 class Offer(models.Model):
@@ -18,27 +19,29 @@ class Offer(models.Model):
     """
 
     name = models.CharField(max_length=255, blank=False, null=False,
-                            verbose_name="nom de l'offre")
+                            verbose_name=_("offer name"))
     configuration_type = models.CharField(max_length=50,
                             blank=True,
-                            verbose_name='type de configuration',
-                            help_text="Type de configuration à utiliser avec cette offre")
+                            verbose_name=_("type of configuration"),
+                            help_text=_("Type of configuration to use with "
+                                        "this offer"))
     billing_period = models.IntegerField(blank=False, null=False, default=1,
-                                         verbose_name='période de facturation',
-                                         help_text='en mois',
+                                         verbose_name=_("billing period"),
+                                         help_text=_("in months"),
                                          validators=[MinValueValidator(1)])
     period_fees = models.DecimalField(max_digits=5, decimal_places=2,
                                       blank=False, null=False,
-                                      verbose_name='montant par période de '
-                                                   'facturation',
-                                      help_text='en €')
+                                      verbose_name=_("fee per billing period"),
+                                      help_text=_("in €"))
     initial_fees = models.DecimalField(max_digits=5, decimal_places=2,
-                                      blank=False, null=False,
-                                      verbose_name='frais de mise en service',
-                                      help_text='en €')
-    non_billable = models.BooleanField(default=False,
-                                       verbose_name='n\'est pas facturable',
-                                       help_text='L\'offre ne sera pas facturée par la commande charge_members')
+                                       blank=False, null=False,
+                                       verbose_name=_("initial fee"),
+                                       help_text=_("in €"))
+    non_billable = models.BooleanField(
+        default=False,
+        verbose_name=_("not billable"),
+        help_text=_("This offer will not be billed by the 'charge_members' "
+                    "command"))
 
     def get_configuration_type_display(self):
         """
@@ -49,12 +52,13 @@ class Offer(models.Model):
             if item and self.configuration_type in item:
                 return item[1]
         return self.configuration_type
-    get_configuration_type_display.short_description = 'type de configuration'
+    get_configuration_type_display.short_description = _("type of configuration")
 
     def display_price(self):
         """Displays the price of an offer in a human-readable manner
         (for instance "30€ / month")
         """
+        from django.utils.translation import ugettext as _
         if int(self.period_fees) == self.period_fees:
             fee = int(self.period_fees)
         else:
@@ -63,7 +67,8 @@ class Offer(models.Model):
             period = ""
         else:
             period = self.billing_period
-        return "{period_fee}€ / {billing_period} mois".format(
+        # Translators: 'billing_period' may be the empty string (if equal to 1)
+        return _("{period_fee}€ / {billing_period} month").format(
             period_fee=fee,
             billing_period=period)
 
@@ -72,7 +77,8 @@ class Offer(models.Model):
                                          price=self.display_price())
 
     class Meta:
-        verbose_name = 'offre'
+        verbose_name = _("offer")
+        verbose_name_plural = _("offers")
 
 
 class OfferSubscription(models.Model):
@@ -87,28 +93,29 @@ class OfferSubscription(models.Model):
         null=False,
         blank=False,
         default=datetime.date.today,
-        verbose_name="date de souscription à l'offre")
+        verbose_name=_("subscription date"))
     # TODO: for data retention, prevent deletion of a subscription object
     # while the resign date is recent enough (e.g. one year in France).
     resign_date = models.DateField(
         null=True,
         blank=True,
-        verbose_name='date de résiliation')
+        verbose_name=_("resign date"))
     # TODO: move this to offers?
     commitment = models.IntegerField(blank=False, null=False,
-                                     verbose_name="période d'engagement",
-                                     help_text='en mois',
+                                     verbose_name=_("commitment duration"),
+                                     help_text=_("in months"),
                                      validators=[MinValueValidator(0)],
                                      default=0)
-    member = models.ForeignKey('members.Member', verbose_name='membre')
-    offer = models.ForeignKey('Offer', verbose_name='offre')
+    member = models.ForeignKey('members.Member', verbose_name=_("member"))
+    offer = models.ForeignKey('Offer', verbose_name=_("offer"))
 
     def __unicode__(self):
         return '%s - %s - %s' % (self.member, self.offer.name,
                                        self.subscription_date)
 
     class Meta:
-        verbose_name = 'abonnement'
+        verbose_name = _("subscription")
+        verbose_name_plural = _("subscriptions")
 
 
 def count_active_subscriptions():

+ 2 - 1
coin/resources/apps.py

@@ -1,8 +1,9 @@
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
 from django.apps import AppConfig
+from django.utils.translation import ugettext_lazy as _
 
 
 class ResourcesConfig(AppConfig):
         name = "coin.resources"
-        verbose_name = "Resources"
+        verbose_name = _("Resources")

+ 51 - 33
coin/resources/models.py

@@ -2,9 +2,10 @@
 from __future__ import unicode_literals
 
 from django.db import models
+from django.db.models import Q
 from django.core.exceptions import ValidationError
 from django.core.validators import MaxValueValidator
-from django.db.models import Q
+from django.utils.translation import ugettext_lazy as _
 from netfields import CidrAddressField, NetManager
 from netaddr import IPNetwork, IPSet
 
@@ -12,67 +13,77 @@ from netaddr import IPNetwork, IPSet
 def validate_subnet(cidr):
     """Checks that a CIDR object is indeed a subnet, i.e. the host bits are
     all set to zero."""
+    from django.utils.translation import ugettext as _
     if not isinstance(cidr, IPNetwork):
-        raise ValidationError("Erreur, objet IPNetwork attendu.")
+        raise ValidationError(_("Error: expected IPNetwork object."))
     if cidr.ip != cidr.network:
-        raise ValidationError("{} n'est pas un sous-réseau valide, voulez-vous dire {} ?".format(cidr, cidr.cidr))
+        error = _("{subnet} is not a proper subnet, did you mean {correct}?")
+        raise ValidationError(error.format(subnet=cidr, correct=cidr.cidr))
 
 
 class IPPool(models.Model):
     """Pool of IP addresses (either v4 or v6)."""
     name = models.CharField(max_length=255, blank=False, null=False,
-                            verbose_name='nom',
-                            help_text="Nom du pool d'IP")
-    default_subnetsize = models.PositiveSmallIntegerField(blank=False,
-                                                          verbose_name='taille de sous-réseau par défaut',
-                                                          help_text='Taille par défaut du sous-réseau à allouer aux abonnés dans ce pool',
-                                                          validators=[MaxValueValidator(64)])
+                            verbose_name=_('name'),
+                            help_text=_("Name of the IP pool"))
+    default_subnetsize = models.PositiveSmallIntegerField(
+        blank=False,
+        verbose_name=_("default subnet size"),
+        help_text=_("Default size of subnets allocated to subscribers "
+                    "from this pool"),
+        validators=[MaxValueValidator(64)])
     inet = CidrAddressField(validators=[validate_subnet],
-                            verbose_name='réseau',
-                            help_text="Bloc d'adresses IP du pool")
+                            verbose_name=_("network"),
+                            help_text=_("Address space to use"))
     objects = NetManager()
 
     def clean(self):
+        from django.utils.translation import ugettext as _
         if self.inet:
             max_subnetsize = 64 if self.inet.version == 6 else 32
             if not self.inet.prefixlen <= self.default_subnetsize <= max_subnetsize:
-                raise ValidationError('Taille de sous-réseau invalide')
+                raise ValidationError(_("Invalid default subnet size."))
             # Check that related subnet are in the pool (useful when
             # modifying an existing pool that already has subnets
             # allocated in it)
             incorrect = [str(subnet) for subnet in self.ipsubnet_set.all()
                          if not subnet.inet in self.inet]
             if incorrect:
-                err = "Des sous-réseaux se retrouveraient en-dehors du bloc d'IP: {}".format(incorrect)
-                raise ValidationError(err)
+                err = _("Some subnets allocated in this pool are outside "
+                        "the pool: {subnets}.")
+                raise ValidationError(err.format(subnets=incorrect))
 
     def __unicode__(self):
         return self.name
 
     class Meta:
-        verbose_name = "pool d'IP"
-        verbose_name_plural = "pools d'IP"
+        verbose_name = _("IP pool")
+        verbose_name_plural = _("IP pools")
 
 
 class IPSubnet(models.Model):
     inet = CidrAddressField(blank=True, validators=[validate_subnet],
-                            unique=True, verbose_name="sous-réseau",
-                            help_text="Laisser vide pour allouer automatiquement")
+                            unique=True, verbose_name=_("subnet"),
+                            help_text=_("Leave empty for automatic allocation"))
     objects = NetManager()
-    ip_pool = models.ForeignKey(IPPool, verbose_name="pool d'IP")
+    ip_pool = models.ForeignKey(IPPool, verbose_name=_("IP pool"))
     configuration = models.ForeignKey('configuration.Configuration',
                                       related_name='ip_subnet',
-                                      verbose_name='configuration')
-    delegate_reverse_dns = models.BooleanField(default=False,
-                                               verbose_name='déléguer le reverse DNS',
-                                               help_text='Déléguer la résolution DNS inverse de ce sous-réseau à un ou plusieurs serveurs de noms')
-    name_server = models.ManyToManyField('reverse_dns.NameServer',
-                                         blank=True,
-                                         verbose_name='serveur de noms',
-                                         help_text="Serveur de noms à qui déléguer la résolution DNS inverse")
+                                      verbose_name=_("configuration"))
+    delegate_reverse_dns = models.BooleanField(
+        default=False,
+        verbose_name=_("delegate reverse DNS"),
+        help_text=_("Whever to delegate reverse DNS resolution for this "
+                    "subnet to one or more name servers"))
+    name_server = models.ManyToManyField(
+        'reverse_dns.NameServer',
+        blank=True,
+        verbose_name=_("name server"),
+        help_text=_("Name server to use for the delegation of reverse DNS"))
 
     def allocate(self):
         """Automatically allocate a free subnet"""
+        from django.utils.translation import ugettext as _
         pool = IPSet([self.ip_pool.inet])
         used = IPSet((s.inet for s in self.ip_pool.ipsubnet_set.all()))
         free = pool.difference(used)
@@ -85,26 +96,33 @@ class IPSubnet(models.Model):
         try:
             first_free = available.next()
         except StopIteration:
-            raise ValidationError("Impossible d'allouer un sous-réseau : bloc d'IP rempli.")
+            raise ValidationError(_("Unable to allocate an IP subnet in the "
+                                    "specified pool: not enough space left."))
         # first_free is a subnet, but it might be too large for our needs.
         # This selects the first sub-subnet of the right size.
         self.inet = first_free.subnet(self.ip_pool.default_subnetsize, 1).next()
 
     def validate_inclusion(self):
         """Check that we are included in the IP pool"""
+        from django.utils.translation import ugettext as _
         if not self.inet in self.ip_pool.inet:
-            raise ValidationError("Le sous-réseau doit être inclus dans le bloc d'IP.")
+            raise ValidationError(_("Subnet must be included in the IP pool."))
         # Check that we don't conflict with existing subnets.
         conflicting = self.ip_pool.ipsubnet_set.filter(Q(inet__net_contained_or_equal=self.inet) |
                                                        Q(inet__net_contains_or_equals=self.inet)).exclude(id=self.id)
         if conflicting:
-            raise ValidationError("Le sous-réseau est en conflit avec des sous-réseaux existants: {}.".format(conflicting))
+            error = _("Subnet must not intersect with existing subnets.\n"
+                      "Intersected subnets: {subnets}.")
+            raise ValidationError(error.format(subnets=conflicting))
 
     def validate_reverse_dns(self):
         """Check that reverse DNS entries, if any, are included in the subnet"""
+        from django.utils.translation import ugettext as _
         incorrect = [str(rev.ip) for rev in self.reversednsentry_set.all() if not rev.ip in self.inet]
         if incorrect:
-            raise ValidationError("Des entrées DNS inverse ne sont pas dans le sous-réseau: {}.".format(incorrect))
+            error = _("Some reverse DNS entries are not in the subnet: "
+                      "{entries}.")
+            raise ValidationError(error.format(entries=incorrect))
 
     def clean(self):
         if not self.inet:
@@ -117,5 +135,5 @@ class IPSubnet(models.Model):
         return str(self.inet)
 
     class Meta:
-        verbose_name = "sous-réseau IP"
-        verbose_name_plural = "sous-réseaux IP"
+        verbose_name = _("IP subnet")
+        verbose_name_plural = _("IP subnets")

+ 2 - 1
coin/reverse_dns/apps.py

@@ -1,8 +1,9 @@
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
 from django.apps import AppConfig
+from django.utils.translation import ugettext_lazy as _
 
 
 class ReverseDNSConfig(AppConfig):
         name = "coin.reverse_dns"
-        verbose_name = "Reverse DNS"
+        verbose_name = _("Reverse DNS")

+ 2 - 0
coin/reverse_dns/models.py

@@ -4,6 +4,8 @@ from __future__ import unicode_literals
 from django.db import models
 from django.core.exceptions import ValidationError
 from django.core.validators import MinValueValidator
+from django.utils.translation import ugettext as _
+from django.utils.translation import ugettext_lazy as __
 from netfields import InetAddressField, NetManager
 from netaddr import IPAddress
 import ldapdb.models

+ 2 - 2
coin/templates/admin/base_site.html

@@ -10,12 +10,12 @@
 {% block title %}COIN ☺ Admin{% endblock %}
 
 {% block branding %}
-<h1 id="site-name">Administration de COIN</h1>
+<h1 id="site-name">{% trans "COIN admin site" %}</h1>
 {% endblock %}
 
 {% block userlinks %}
     {{ block.super }}
-    / <a href="{% url 'home' %}">Retour au site</a>
+    / <a href="{% url 'home' %}">{% trans "Back to main site" %}</a>
 {% endblock %}
 
 {% block nav-global %}{% endblock %}

+ 3 - 2
coin/templates/base.html

@@ -1,5 +1,6 @@
 <!doctype html>
 {% load staticfiles %}
+{% load i18n %}
 <html class="no-js" lang="en">
 <head>
     <meta charset="utf-8" />
@@ -20,7 +21,7 @@
         {% if user.is_authenticated %}
         <aside class="left-off-canvas-menu">
             <ul class="off-canvas-list">
-                <li><label>Menu</label></li>
+                <li><label>{% trans 'Menu' %}</label></li>
                 {% include "menu_items.html" %}
             </ul>
         </aside>
@@ -30,7 +31,7 @@
             <div class="show-for-medium-up">
                 <div class="row">
                     <div class="large-12">
-                        <h1><a href="{% url 'home' %}">COIN est un Outil pour un Internet Neutre</a></h1>
+                        <h1><a href="{% url 'home' %}">{% trans 'COIN is an Optimistic tool for Internet Neutrality' %}</a></h1>
                     </div>
                 </div>
             </div>

+ 9 - 8
coin/templates/menu_items.html

@@ -1,11 +1,12 @@
 {% load activelink %}
-<li class="{% ifactive 'home' %}active{% endifactive %}"><a href="{% url 'home' %}"><i class="fa fa-home fa-fw"></i> Tableau de bord</a></li>
-<li class="{% ifactive 'members:detail' %}active{% endifactive %}"><a href="{% url 'members:detail' %}"><i class="fa fa-user fa-fw"></i> Mes informations</a></li>
-<li class="{% ifactive 'members:subscriptions' %}active{% endifactive %}"><a href="{% url 'members:subscriptions' %}"><i class="fa fa-cog fa-fw"></i> Mes abonnements</a></li>
-<li class="{% ifactive 'members:invoices' %}active{% endifactive %}"><a href="{% url 'members:invoices' %}"><i class="fa fa-eur fa-fw"></i> Mes factures</a></li>
-<li class="{% ifactive 'members:contact' %}active{% endifactive %}"><a href="{% url 'members:contact' %}"><i class="fa fa-life-ring fa-fw"></i> Contact / Support</a></li>
+{% load i18n %}
+<li class="{% ifactive 'home' %}active{% endifactive %}"><a href="{% url 'home' %}"><i class="fa fa-home fa-fw"></i> {% trans 'Dashboard' %}</a></li>
+<li class="{% ifactive 'members:detail' %}active{% endifactive %}"><a href="{% url 'members:detail' %}"><i class="fa fa-user fa-fw"></i> {% trans 'My details' %}</a></li>
+<li class="{% ifactive 'members:subscriptions' %}active{% endifactive %}"><a href="{% url 'members:subscriptions' %}"><i class="fa fa-cog fa-fw"></i> {% trans 'My subscriptions' %}</a></li>
+<li class="{% ifactive 'members:invoices' %}active{% endifactive %}"><a href="{% url 'members:invoices' %}"><i class="fa fa-eur fa-fw"></i> {% trans 'My invoices' %}</a></li>
+<li class="{% ifactive 'members:contact' %}active{% endifactive %}"><a href="{% url 'members:contact' %}"><i class="fa fa-life-ring fa-fw"></i> {% trans 'Contact and support' %}</a></li>
 <li class="divider"></li>
-{% if user.is_staff %}<li><a href="{% url 'admin:index' %}"><i class="fa fa-cogs fa-fw"></i> Administration</a></li>{% endif %}
-<li class="{% ifactive 'members:password_change' %}active{% endifactive %}"><a href="{% url 'members:password_change' %}"><i class="fa fa-key fa-fw"></i> Modifier mon mot de passe</a></li>
+{% if user.is_staff %}<li><a href="{% url 'admin:index' %}"><i class="fa fa-cogs fa-fw"></i> {% trans 'Administration' %}</a></li>{% endif %}
+<li class="{% ifactive 'members:password_change' %}active{% endifactive %}"><a href="{% url 'members:password_change' %}"><i class="fa fa-key fa-fw"></i> {% trans 'Change password' %}</a></li>
 <li class="divider"></li>
-<li class="{% ifactive '' %}active{% endifactive %}"><a href="{% url 'members:logout' %}"><i class="fa fa-power-off fa-fw"></i> Dépannexion</a></li>
+<li class="{% ifactive '' %}active{% endifactive %}"><a href="{% url 'members:logout' %}"><i class="fa fa-power-off fa-fw"></i> {% trans 'Logout' %}</a></li>

+ 2 - 1
coin/utils.py

@@ -15,6 +15,7 @@ from django.conf import settings
 from django.template.loader import get_template
 from django.template import Context, TemplateDoesNotExist
 from django.contrib.sites.models import Site
+from django.utils.translation import ugettext_lazy as _
 
 
 # Stockage des fichiers privés (comme les factures par exemple)
@@ -92,7 +93,7 @@ def delete_selected(modeladmin, request, queryset):
     for obj in queryset:
         obj.delete()
 
-delete_selected.short_description = "Supprimer tous les objets sélectionnés."
+delete_selected.short_description = _("Delete all selected objects.")
 
 # Time-related functions
 

+ 2 - 1
coin/vpn/apps.py

@@ -1,8 +1,9 @@
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
 from django.apps import AppConfig
+from django.utils.translation import ugettext_lazy as _
 
 
 class VPNConfig(AppConfig):
         name = "coin.vpn"
-        verbose_name = "VPN"
+        verbose_name = _("VPN")

+ 23 - 16
coin/vpn/models.py

@@ -2,6 +2,7 @@
 from __future__ import unicode_literals
 
 from django.db import models
+from django.utils.translation import ugettext_lazy as _
 from django.core.exceptions import ValidationError
 from django.conf import settings
 from django.core.urlresolvers import reverse
@@ -24,20 +25,22 @@ class VPNConfiguration(CoinLdapSyncMixin, Configuration):
     #     'offers.OfferSubscription',
     #     related_name=backend_name,
     #     validators=[ValidateBackendType(backend_name)])
-    activated = models.BooleanField(default=False, verbose_name='activé')
+    activated = models.BooleanField(default=False, verbose_name=_('activated'))
     login = models.CharField(max_length=50, unique=True, blank=True,
-                             verbose_name="identifiant",
-                             help_text="leave empty for automatic generation")
-    password = models.CharField(max_length=256, verbose_name="mot de passe",
+                             verbose_name=_("login"),
+                             help_text=_("Leave empty for automatic generation"))
+    password = models.CharField(max_length=256, verbose_name=_("password"),
                                 blank=True, null=True)
     ipv4_endpoint = InetAddressField(validators=[validation.validate_v4],
-                                     verbose_name="IPv4", blank=True, null=True,
-                                     help_text="Adresse IPv4 utilisée par "
-                                     "défaut sur le VPN")
+                                     blank=True, null=True,
+                                     verbose_name=_("IPv4"),
+                                     help_text=_("IPv4 address to use by default"
+                                                 " on the VPN"))
     ipv6_endpoint = InetAddressField(validators=[validation.validate_v6],
-                                     verbose_name="IPv6", blank=True, null=True,
-                                     help_text="Adresse IPv6 utilisée par "
-                                     "défaut sur le VPN")
+                                     blank=True, null=True,
+                                     verbose_name=_("IPv6"),
+                                     help_text=_("IPv6 address to use by default"
+                                                 " on the VPN"))
 
     objects = NetManager()
 
@@ -108,24 +111,26 @@ class VPNConfiguration(CoinLdapSyncMixin, Configuration):
         If [delete] is True, then simply delete the faulty endpoints
         instead of raising an exception.
         """
-        error = "L'IP {} n'est pas dans un réseau attribué."
+        from django.utils.translation import ugettext as _
+        error = _("Endpoint {ip} is not in an attributed subnet.")
         subnets = self.ip_subnet.all()
         is_faulty = lambda endpoint : endpoint and not any([endpoint in subnet.inet for subnet in subnets])
         if is_faulty(self.ipv4_endpoint):
             if delete:
                 self.ipv4_endpoint = None
             else:
-                raise ValidationError(error.format(self.ipv4_endpoint))
+                raise ValidationError(error.format(ip=self.ipv4_endpoint))
         if is_faulty(self.ipv6_endpoint):
             if delete:
                 self.ipv6_endpoint = None
             else:
-                raise ValidationError(error.format(self.ipv6_endpoint))
+                raise ValidationError(error.format(ip=self.ipv6_endpoint))
 
     def clean(self):
         # Generate VPN login, of the form "login-vpnX".  The resulting
         # login should not contain any ".", because graphite uses "." as a
         # separator.
+        from django.utils.translation import ugettext as _
         if not self.login:
             username = self.offersubscription.member.username
             vpns = VPNConfiguration.objects.filter(offersubscription__member__username=username)
@@ -138,7 +143,7 @@ class VPNConfiguration(CoinLdapSyncMixin, Configuration):
                     break
             # We may have failed.
             if not self.login:
-                ValidationError("Impossible de générer un login VPN")
+                ValidationError(_("Unable to allocate a VPN login."))
         # Hash password if needed
         self.password = utils.ldap_hash(self.password)
         # If saving for the first time and IP endpoints are not specified,
@@ -148,10 +153,12 @@ class VPNConfiguration(CoinLdapSyncMixin, Configuration):
         self.check_endpoints()
 
     def __unicode__(self):
-        return 'VPN ' + self.login
+        from django.utils.translation import ugettext as _
+        return _('VPN <{login}>').format(login=self.login)
 
     class Meta:
-        verbose_name = 'VPN'
+        verbose_name = _('VPN')
+        verbose_name_plural = _('VPNs')
 
 
 class LdapVPNConfig(ldapdb.models.Model):

+ 2 - 1
simple_dsl/apps.py

@@ -1,8 +1,9 @@
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
 from django.apps import AppConfig
+from django.utils.translation import ugettext_lazy as _
 
 
 class SimpleDSLConfig(AppConfig):
         name = "simple_dsl"
-        verbose_name = "Simple DSL"
+        verbose_name = _("Simple DSL")

+ 5 - 5
simple_dsl/models.py

@@ -2,6 +2,7 @@
 from __future__ import unicode_literals
 
 from django.db import models
+from django.utils.translation import ugettext_lazy as _
 
 from coin.configuration.models import Configuration
 
@@ -14,17 +15,16 @@ class SimpleDSL(Configuration):
     DSL reselling.
     """
     class Meta:
-        verbose_name = 'DSL line'
-        # If Django's default pluralisation is not satisfactory
-        #verbose_name_plural = 'very many DSL lines'
+        verbose_name = _('DSL line')
+        verbose_name_plural = _('DSL lines')
 
     # URL namespace associated to this configuration type, to build URLs
     # in various view.  Should also be defined in urls.py.  Here, we don't
     # define any view, so there's no need for an URL namespace.
     #url_namespace = "dsl"
     phone_number = models.CharField(max_length=20,
-                                    verbose_name='phone number',
-                                    help_text="Phone number associated to the DSL line")
+                                    verbose_name=_('phone number'),
+                                    help_text=_("Phone number associated to the DSL line"))
     
     def __unicode__(self):
         return self.phone_number