Baptiste Jonglez il y a 10 ans
Parent
commit
b6d77a9a99

+ 2 - 1
coin/billing/apps.py

@@ -1,8 +1,9 @@
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 from django.apps import AppConfig
 from django.apps import AppConfig
+from django.utils.translation import ugettext_lazy as _
 
 
 
 
 class BillingConfig(AppConfig):
 class BillingConfig(AppConfig):
         name = "coin.billing"
         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 import models
 from django.db.models.signals import post_save
 from django.db.models.signals import post_save
+from django.utils.translation import ugettext_lazy as _
 from django.dispatch import receiver
 from django.dispatch import receiver
 
 
 from coin.offers.models import OfferSubscription
 from coin.offers.models import OfferSubscription
@@ -36,14 +37,15 @@ def invoice_pdf_filename(instance, filename):
 class Invoice(models.Model):
 class Invoice(models.Model):
 
 
     INVOICES_STATUS_CHOICES = (
     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,
     number = models.CharField(max_length=25,
                               default=next_invoice_number,
                               default=next_invoice_number,
                               unique=True,
                               unique=True,
@@ -133,7 +135,8 @@ class Invoice(models.Model):
         return '#%s %0.2f€ %s' % (self.number, self.amount(), self.date_due)
         return '#%s %0.2f€ %s' % (self.number, self.amount(), self.date_due)
 
 
     class Meta:
     class Meta:
-        verbose_name = 'facture'
+        verbose_name = _('invoice')
+        verbose_name_plural = _('invoices')
 
 
 
 
 class InvoiceDetail(models.Model):
 class InvoiceDetail(models.Model):

+ 2 - 1
coin/configuration/admin.py

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

+ 2 - 1
coin/configuration/apps.py

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

+ 10 - 8
coin/isp_database/admin.py

@@ -2,9 +2,16 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
 from django.contrib import admin
 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
 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):
 class SingleInstanceAdminMixin(object):
     """Hides the "Add" button when there is already an instance"""
     """Hides the "Add" button when there is already an instance"""
@@ -25,11 +32,10 @@ class RegisteredOfficeInline(admin.StackedInline):
              ('region', 'country_name'))}),
              ('region', 'country_name'))}),
         ('Extras', {
         ('Extras', {
              'fields': ('siret',),
              '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):
 class OtherWebsiteInline(admin.StackedInline):
     model = OtherWebsite
     model = OtherWebsite
     extra = 0
     extra = 0
@@ -51,10 +57,7 @@ class BankInfoInline(admin.StackedInline):
 
 
     fieldsets = (('', {
     fieldsets = (('', {
                 'fields': ('iban', 'bic', 'bank_name', 'check_order'),
                 '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')}),
             'website')}),
         ('Extras', {
         ('Extras', {
             'fields': ('administrative_email', 'support_email', 'lists_url'),
             '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 -*-
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 from django.apps import AppConfig
 from django.apps import AppConfig
+from django.utils.translation import ugettext_lazy as _
 
 
 
 
 class ISPDatabaseConfig(AppConfig):
 class ISPDatabaseConfig(AppConfig):
         name = "coin.isp_database"
         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.db import models
 from django.core.validators import MaxValueValidator
 from django.core.validators import MaxValueValidator
 from django.core.exceptions import ValidationError
 from django.core.exceptions import ValidationError
-
+from django.utils.translation import ugettext_lazy as _
 from localflavor.generic.models import IBANField, BICField
 from localflavor.generic.models import IBANField, BICField
 from localflavor.fr.models import FRSIRETField
 from localflavor.fr.models import FRSIRETField
 
 
@@ -15,18 +15,20 @@ from coin import utils
 # API version, see http://db.ffdn.org/format
 # API version, see http://db.ffdn.org/format
 API_VERSION = 0.1
 API_VERSION = 0.1
 
 
-TECHNOLOGIES = (('ftth', 'FTTH'),
-                ('dsl', '*DSL'),
-                ('wifi', 'WiFi'))
+TECHNOLOGIES = (('ftth', _('FTTH')),
+                ('dsl', _('xDSL')),
+                ('wifi', _('Wi-Fi')))
 
 
 
 
 class SingleInstanceMixin(object):
 class SingleInstanceMixin(object):
     """Makes sure that no more than one instance of a given model is created."""
     """Makes sure that no more than one instance of a given model is created."""
 
 
     def clean(self):
     def clean(self):
+        from django.utils.translation import ugettext as _
         model = self.__class__
         model = self.__class__
         if (model.objects.count() > 0 and self.id != model.objects.get().id):
         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()
         super(SingleInstanceMixin, self).clean()
 
 
 
 
@@ -36,39 +38,43 @@ class ISPInfo(SingleInstanceMixin, models.Model):
     The naming convention is different from Python/django so that it
     The naming convention is different from Python/django so that it
     matches exactly the format (which uses CamelCase...)
     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
     # Length required by the spec
     shortname = models.CharField(max_length=15, blank=True,
     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,
     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,
     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,
     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
     # TODO: choice field
     progressStatus = models.PositiveSmallIntegerField(
     progressStatus = models.PositiveSmallIntegerField(
         validators=[MaxValueValidator(7)],
         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
     # TODO: better model for coordinates
     latitude = models.FloatField(blank=True, null=True,
     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,
     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.
     # Uncomment this if you want to manage these counters by hand.
     #member_count = models.PositiveIntegerField(help_text="Number of members")
     #member_count = models.PositiveIntegerField(help_text="Number of members")
@@ -77,16 +83,17 @@ class ISPInfo(SingleInstanceMixin, models.Model):
 
 
     # field outside of db-ffdn format:
     # field outside of db-ffdn format:
     administrative_email = models.EmailField(
     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(
     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(
     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
     @property
     def memberCount(self):
     def memberCount(self):
@@ -105,8 +112,9 @@ class ISPInfo(SingleInstanceMixin, models.Model):
 
 
     @property
     @property
     def main_chat_verbose(self):
     def main_chat_verbose(self):
+        from django.utils.translation import ugettext as _
         m = utils.re_chat_url.match(self.chatroom_set.first().url)
         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):
     def get_absolute_url(self):
         return '/isp.json'
         return '/isp.json'
@@ -149,23 +157,36 @@ class ISPInfo(SingleInstanceMixin, models.Model):
     def __unicode__(self):
     def __unicode__(self):
         return self.name
         return self.name
 
 
+    class Meta:
+        verbose_name = _('ISP details')
+        verbose_name_plural = _('ISP details')
+
 
 
 class OtherWebsite(models.Model):
 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):
 class RegisteredOffice(models.Model):
     """ http://json-schema.org/address """
     """ 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
     # not in db.ffdn.org spec
     siret = FRSIRETField('SIRET')
     siret = FRSIRETField('SIRET')
@@ -179,41 +200,54 @@ class RegisteredOffice(models.Model):
                 d[key] = getattr(self, field)
                 d[key] = getattr(self, field)
         return d
         return d
 
 
+    class Meta:
+        verbose_name = _('registered office')
+        verbose_name_plural = _('registered offices')
+
 
 
 class ChatRoom(models.Model):
 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):
 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
     # 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
     # TODO: find a geojson library
     #area =
     #area =
-    isp = models.ForeignKey(ISPInfo)
+    isp = models.ForeignKey(ISPInfo, verbose_name=_("ISP"))
 
 
     def to_dict(self):
     def to_dict(self):
         return {"name": self.name,
         return {"name": self.name,
                 "technologies": [self.technologies]}
                 "technologies": [self.technologies]}
 
 
+    class Meta:
+        verbose_name = _('covered area')
+        verbose_name_plural = _('covered areas')
+
 
 
 class BankInfo(models.Model):
 class BankInfo(models.Model):
     """Information about bank account and the bank itself
     """Information about bank account and the bank itself
 
 
     This is out of the scope of db.ffdn.org spec.
     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,
                                    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:
     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 -*-
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 from django.apps import AppConfig
 from django.apps import AppConfig
+from django.utils.translation import ugettext_lazy as _
 
 
 
 
 class MembersConfig(AppConfig):
 class MembersConfig(AppConfig):
         name = "coin.members"
         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 import models
 from django.db.models import Q
 from django.db.models import Q
 from django.db.models.signals import pre_save
 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.dispatch import receiver
 from django.contrib.auth.models import AbstractUser
 from django.contrib.auth.models import AbstractUser
 from django.conf import settings
 from django.conf import settings
@@ -25,51 +27,51 @@ class Member(CoinLdapSyncMixin, AbstractUser):
     REQUIRED_FIELDS = ['first_name', 'last_name', 'email', ]
     REQUIRED_FIELDS = ['first_name', 'last_name', 'email', ]
 
 
     MEMBER_TYPE_CHOICES = (
     MEMBER_TYPE_CHOICES = (
-        ('natural_person', 'Personne physique'),
-        ('legal_entity', 'Personne morale'),
+        ('natural_person', _("Individual")),
+        ('legal_entity', _("Organisation")),
     )
     )
     MEMBER_STATUS_CHOICES = (
     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,
     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,
     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,
     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,
     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,
     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,
     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
     # TODO: use a django module that provides an address model? (would
     # support more countries and address types)
     # support more countries and address types)
     address = models.TextField(
     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,
     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,
     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,
     country = models.CharField(max_length=200, blank=True, null=True,
                                default='France',
                                default='France',
-                               verbose_name='pays')
+                               verbose_name=_("country"))
     resign_date = models.DateField(null=True, blank=True,
     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 :
     # Following fields are managed by the parent class AbstractUser :
     # username, first_name, last_name, email
     # username, first_name, last_name, email
@@ -82,22 +84,26 @@ class Member(CoinLdapSyncMixin, AbstractUser):
     _password_ldap = None
     _password_ldap = None
 
 
     def clean(self):
     def clean(self):
+        from django.utils.translation import ugettext as _
         if self.type == 'legal_entity':
         if self.type == 'legal_entity':
             if not self.organization_name:
             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':
         elif self.type == 'natural_person':
             if not (self.first_name and self.last_name):
             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):
     def __unicode__(self):
+        from django.utils.translation import ugettext as _
         if self.type == 'legal_entity':
         if self.type == 'legal_entity':
             return self.organization_name
             return self.organization_name
         elif self.nickname:
         elif self.nickname:
             return self.nickname
             return self.nickname
         else:
         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):
     def get_full_name(self):
         return str(self)
         return str(self)
@@ -110,7 +116,7 @@ class Member(CoinLdapSyncMixin, AbstractUser):
         x = self.membership_fees.order_by('-end_date')
         x = self.membership_fees.order_by('-end_date')
         if x:
         if x:
             return self.membership_fees.order_by('-end_date')[0].end_date
             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):
     def is_paid_up(self):
         """
         """
@@ -167,7 +173,7 @@ class Member(CoinLdapSyncMixin, AbstractUser):
         """
         """
         Update LDAP data when a member is saved
         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
         # Do not perform LDAP query if no usefull fields to update are specified
         # in update_fields
         # in update_fields
         # Ex : at login, last_login field is updated by django auth module.
         # Ex : at login, last_login field is updated by django auth module.
@@ -175,8 +181,8 @@ class Member(CoinLdapSyncMixin, AbstractUser):
             return
             return
 
 
         # Fail if no username specified
         # 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
         # If try to sync a superuser in creation mode
         # Try to retrieve the user in ldap. If exists, switch to update 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
         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
         # Delete user from LDAP
         ldap_user = LdapUser.objects.get(pk=self.username)
         ldap_user = LdapUser.objects.get(pk=self.username)
@@ -256,7 +263,8 @@ class Member(CoinLdapSyncMixin, AbstractUser):
                                    context={'member': self, 'branding':ISPInfo.objects.first()})
                                    context={'member': self, 'branding':ISPInfo.objects.first()})
 
 
     class Meta:
     class Meta:
-        verbose_name = 'membre'
+        verbose_name = _('member')
+        verbose_name_plural = _('members')
 
 
 # Hack to force email to be required by Member model
 # Hack to force email to be required by Member model
 Member._meta.get_field('email')._unique = True
 Member._meta.get_field('email')._unique = True
@@ -289,7 +297,8 @@ def get_automatic_username(member):
         # Concaténer avec nom de famille
         # Concaténer avec nom de famille
         username = ('%s%s' % (first_name_letters, member.last_name))
         username = ('%s%s' % (first_name_letters, member.last_name))
     else:
     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
     # Remplacer ou enlever les caractères non ascii
     username = unicodedata.normalize('NFD', username)\
     username = unicodedata.normalize('NFD', username)\
@@ -321,12 +330,12 @@ def get_automatic_username(member):
 
 
 class CryptoKey(CoinLdapSyncMixin, models.Model):
 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,
     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):
     def sync_to_ldap(self, creation, *args, **kwargs):
         """Simply tell the member object to resync all its SSH keys to LDAP"""
         """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()
         self.member.sync_ssh_keys()
 
 
     def __unicode__(self):
     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:
     class Meta:
-        verbose_name = 'clé'
+        verbose_name = _('key')
+        verbose_name_plural = _('keys')
 
 
 
 
 class MembershipFee(models.Model):
 class MembershipFee(models.Model):
     PAYMENT_METHOD_CHOICES = (
     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',
     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,
     amount = models.DecimalField(null=False, max_digits=5, decimal_places=2,
                                  default=settings.MEMBER_DEFAULT_COTISATION,
                                  default=settings.MEMBER_DEFAULT_COTISATION,
-                                 verbose_name='montant', help_text='en €')
+                                 verbose_name=_('amount'),
+                                 help_text=_('in €'))
     start_date = models.DateField(
     start_date = models.DateField(
         null=False,
         null=False,
         blank=False,
         blank=False,
-        verbose_name='date de début de cotisation')
+        verbose_name=_('start date of membership fee'))
     end_date = models.DateField(
     end_date = models.DateField(
         null=False,
         null=False,
         blank=True,
         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,
     payment_method = models.CharField(max_length=100, null=True, blank=True,
                                       choices=PAYMENT_METHOD_CHOICES,
                                       choices=PAYMENT_METHOD_CHOICES,
-                                      verbose_name='moyen de paiement')
+                                      verbose_name=_('payment method'))
     reference = models.CharField(max_length=125, null=True, blank=True,
     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,
     payment_date = models.DateField(null=True, blank=True,
-                                    verbose_name='date du paiement')
+                                    verbose_name=_('date of payment'))
 
 
     def clean(self):
     def clean(self):
         if self.end_date is None:
         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)
         return '%s - %s - %i€' % (self.member, self.start_date, self.amount)
 
 
     class Meta:
     class Meta:
-        verbose_name = 'cotisation'
+        verbose_name = _('membership fee')
+        verbose_name_plural = _('membership fees')
 
 
 
 
 class LdapUser(ldapdb.models.Model):
 class LdapUser(ldapdb.models.Model):

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

@@ -1,21 +1,22 @@
 {% extends "base.html" %}
 {% extends "base.html" %}
+{% load i18n %}
 
 
 {% block content %}
 {% block content %}
 <div class="row">
 <div class="row">
     <div class="large-12 columns">
     <div class="large-12 columns">
-        <h2>Contact / Support</h2>
+        <h2>{% trans 'Contact and support' %}</h2>
         <div class="panel">
         <div class="panel">
-            <h3>Courriel</h3>
+            <h3>{% trans 'By email' %}</h3>
             <p>
             <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>
             </p>
             {% if branding.lists_url %}
             {% 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 %}
             {% endif %}
-            <h3>IRC</h3>
+            <h3>{% trans 'IRC' %}</h3>
             <p><a href="{{ branding.chatroom_set.first.url }}">{{ branding.main_chat_verbose }}</a></p>
             <p><a href="{{ branding.chatroom_set.first.url }}">{{ branding.main_chat_verbose }}</a></p>
         </div>
         </div>
     </div>
     </div>

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

@@ -1,20 +1,21 @@
 {% extends "base.html" %}
 {% extends "base.html" %}
+{% load i18n %}
 
 
 {% block content %}
 {% block content %}
 <div class="row">
 <div class="row">
     <div class="large-12 columns">
     <div class="large-12 columns">
-        <h2>Tableau de bord</h2>
+        <h2>{% trans "Dashboard" %}</h2>
     </div>
     </div>
 </div>
 </div>
 
 
 <div class="row">
 <div class="row">
     <div class="large-6 columns">
     <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>
     <div class="large-6 columns">
     <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>
     </div>
     {% if has_isp_feed %}
     {% if has_isp_feed %}
     <div class="large-6 columns">
     <div class="large-6 columns">

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

@@ -1,15 +1,16 @@
 {% extends "base.html" %}
 {% extends "base.html" %}
+{% load i18n %}
 
 
 {% block content %}
 {% block content %}
-<h2>Mes factures</h2>
+<h2>{% trans "My invoices" %}</h2>
 
 
 <table id="member_invoices" class="full-width">
 <table id="member_invoices" class="full-width">
     <thead>
     <thead>
         <tr>
         <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>
             <th></th>
         </tr>
         </tr>
     </thead>
     </thead>
@@ -20,7 +21,7 @@
             <td>{{ invoice.date }}</td>
             <td>{{ invoice.date }}</td>
             <td>{{ invoice.amount }}</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.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>
         </tr>
         {% endfor %}
         {% endfor %}
     </tbody>
     </tbody>

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

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

+ 7 - 4
coin/mixins.py

@@ -3,6 +3,7 @@ from __future__ import unicode_literals
 
 
 from django.db import transaction
 from django.db import transaction
 from django.conf import settings
 from django.conf import settings
+from django.utils.translation import ugettext_lazy as _
 
 
 
 
 class CoinLdapSyncMixin(object):
 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
     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
     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):
     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):
     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
     @transaction.atomic
     def save(self, *args, **kwargs):
     def save(self, *args, **kwargs):

+ 2 - 1
coin/offers/apps.py

@@ -1,8 +1,9 @@
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 from django.apps import AppConfig
 from django.apps import AppConfig
+from django.utils.translation import ugettext_lazy as _
 
 
 
 
 class OffersConfig(AppConfig):
 class OffersConfig(AppConfig):
         name = "coin.offers"
         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 import models
 from django.db.models import Q
 from django.db.models import Q
 from django.core.validators import MinValueValidator
 from django.core.validators import MinValueValidator
+from django.utils.translation import ugettext_lazy as _
 
 
 
 
 class Offer(models.Model):
 class Offer(models.Model):
@@ -18,27 +19,29 @@ class Offer(models.Model):
     """
     """
 
 
     name = models.CharField(max_length=255, blank=False, null=False,
     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,
     configuration_type = models.CharField(max_length=50,
                             blank=True,
                             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,
     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)])
                                          validators=[MinValueValidator(1)])
     period_fees = models.DecimalField(max_digits=5, decimal_places=2,
     period_fees = models.DecimalField(max_digits=5, decimal_places=2,
                                       blank=False, null=False,
                                       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,
     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):
     def get_configuration_type_display(self):
         """
         """
@@ -49,12 +52,13 @@ class Offer(models.Model):
             if item and self.configuration_type in item:
             if item and self.configuration_type in item:
                 return item[1]
                 return item[1]
         return self.configuration_type
         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):
     def display_price(self):
         """Displays the price of an offer in a human-readable manner
         """Displays the price of an offer in a human-readable manner
         (for instance "30€ / month")
         (for instance "30€ / month")
         """
         """
+        from django.utils.translation import ugettext as _
         if int(self.period_fees) == self.period_fees:
         if int(self.period_fees) == self.period_fees:
             fee = int(self.period_fees)
             fee = int(self.period_fees)
         else:
         else:
@@ -63,7 +67,8 @@ class Offer(models.Model):
             period = ""
             period = ""
         else:
         else:
             period = self.billing_period
             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,
             period_fee=fee,
             billing_period=period)
             billing_period=period)
 
 
@@ -72,7 +77,8 @@ class Offer(models.Model):
                                          price=self.display_price())
                                          price=self.display_price())
 
 
     class Meta:
     class Meta:
-        verbose_name = 'offre'
+        verbose_name = _("offer")
+        verbose_name_plural = _("offers")
 
 
 
 
 class OfferSubscription(models.Model):
 class OfferSubscription(models.Model):
@@ -87,28 +93,29 @@ class OfferSubscription(models.Model):
         null=False,
         null=False,
         blank=False,
         blank=False,
         default=datetime.date.today,
         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
     # TODO: for data retention, prevent deletion of a subscription object
     # while the resign date is recent enough (e.g. one year in France).
     # while the resign date is recent enough (e.g. one year in France).
     resign_date = models.DateField(
     resign_date = models.DateField(
         null=True,
         null=True,
         blank=True,
         blank=True,
-        verbose_name='date de résiliation')
+        verbose_name=_("resign date"))
     # TODO: move this to offers?
     # TODO: move this to offers?
     commitment = models.IntegerField(blank=False, null=False,
     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)],
                                      validators=[MinValueValidator(0)],
                                      default=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):
     def __unicode__(self):
         return '%s - %s - %s' % (self.member, self.offer.name,
         return '%s - %s - %s' % (self.member, self.offer.name,
                                        self.subscription_date)
                                        self.subscription_date)
 
 
     class Meta:
     class Meta:
-        verbose_name = 'abonnement'
+        verbose_name = _("subscription")
+        verbose_name_plural = _("subscriptions")
 
 
 
 
 def count_active_subscriptions():
 def count_active_subscriptions():

+ 2 - 1
coin/resources/apps.py

@@ -1,8 +1,9 @@
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 from django.apps import AppConfig
 from django.apps import AppConfig
+from django.utils.translation import ugettext_lazy as _
 
 
 
 
 class ResourcesConfig(AppConfig):
 class ResourcesConfig(AppConfig):
         name = "coin.resources"
         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 __future__ import unicode_literals
 
 
 from django.db import models
 from django.db import models
+from django.db.models import Q
 from django.core.exceptions import ValidationError
 from django.core.exceptions import ValidationError
 from django.core.validators import MaxValueValidator
 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 netfields import CidrAddressField, NetManager
 from netaddr import IPNetwork, IPSet
 from netaddr import IPNetwork, IPSet
 
 
@@ -12,67 +13,77 @@ from netaddr import IPNetwork, IPSet
 def validate_subnet(cidr):
 def validate_subnet(cidr):
     """Checks that a CIDR object is indeed a subnet, i.e. the host bits are
     """Checks that a CIDR object is indeed a subnet, i.e. the host bits are
     all set to zero."""
     all set to zero."""
+    from django.utils.translation import ugettext as _
     if not isinstance(cidr, IPNetwork):
     if not isinstance(cidr, IPNetwork):
-        raise ValidationError("Erreur, objet IPNetwork attendu.")
+        raise ValidationError(_("Error: expected IPNetwork object."))
     if cidr.ip != cidr.network:
     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):
 class IPPool(models.Model):
     """Pool of IP addresses (either v4 or v6)."""
     """Pool of IP addresses (either v4 or v6)."""
     name = models.CharField(max_length=255, blank=False, null=False,
     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],
     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()
     objects = NetManager()
 
 
     def clean(self):
     def clean(self):
+        from django.utils.translation import ugettext as _
         if self.inet:
         if self.inet:
             max_subnetsize = 64 if self.inet.version == 6 else 32
             max_subnetsize = 64 if self.inet.version == 6 else 32
             if not self.inet.prefixlen <= self.default_subnetsize <= max_subnetsize:
             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
             # Check that related subnet are in the pool (useful when
             # modifying an existing pool that already has subnets
             # modifying an existing pool that already has subnets
             # allocated in it)
             # allocated in it)
             incorrect = [str(subnet) for subnet in self.ipsubnet_set.all()
             incorrect = [str(subnet) for subnet in self.ipsubnet_set.all()
                          if not subnet.inet in self.inet]
                          if not subnet.inet in self.inet]
             if incorrect:
             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):
     def __unicode__(self):
         return self.name
         return self.name
 
 
     class Meta:
     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):
 class IPSubnet(models.Model):
     inet = CidrAddressField(blank=True, validators=[validate_subnet],
     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()
     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',
     configuration = models.ForeignKey('configuration.Configuration',
                                       related_name='ip_subnet',
                                       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):
     def allocate(self):
         """Automatically allocate a free subnet"""
         """Automatically allocate a free subnet"""
+        from django.utils.translation import ugettext as _
         pool = IPSet([self.ip_pool.inet])
         pool = IPSet([self.ip_pool.inet])
         used = IPSet((s.inet for s in self.ip_pool.ipsubnet_set.all()))
         used = IPSet((s.inet for s in self.ip_pool.ipsubnet_set.all()))
         free = pool.difference(used)
         free = pool.difference(used)
@@ -85,26 +96,33 @@ class IPSubnet(models.Model):
         try:
         try:
             first_free = available.next()
             first_free = available.next()
         except StopIteration:
         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.
         # first_free is a subnet, but it might be too large for our needs.
         # This selects the first sub-subnet of the right size.
         # This selects the first sub-subnet of the right size.
         self.inet = first_free.subnet(self.ip_pool.default_subnetsize, 1).next()
         self.inet = first_free.subnet(self.ip_pool.default_subnetsize, 1).next()
 
 
     def validate_inclusion(self):
     def validate_inclusion(self):
         """Check that we are included in the IP pool"""
         """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:
         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.
         # Check that we don't conflict with existing subnets.
         conflicting = self.ip_pool.ipsubnet_set.filter(Q(inet__net_contained_or_equal=self.inet) |
         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)
                                                        Q(inet__net_contains_or_equals=self.inet)).exclude(id=self.id)
         if conflicting:
         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):
     def validate_reverse_dns(self):
         """Check that reverse DNS entries, if any, are included in the subnet"""
         """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]
         incorrect = [str(rev.ip) for rev in self.reversednsentry_set.all() if not rev.ip in self.inet]
         if incorrect:
         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):
     def clean(self):
         if not self.inet:
         if not self.inet:
@@ -117,5 +135,5 @@ class IPSubnet(models.Model):
         return str(self.inet)
         return str(self.inet)
 
 
     class Meta:
     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 -*-
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 from django.apps import AppConfig
 from django.apps import AppConfig
+from django.utils.translation import ugettext_lazy as _
 
 
 
 
 class ReverseDNSConfig(AppConfig):
 class ReverseDNSConfig(AppConfig):
         name = "coin.reverse_dns"
         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.db import models
 from django.core.exceptions import ValidationError
 from django.core.exceptions import ValidationError
 from django.core.validators import MinValueValidator
 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 netfields import InetAddressField, NetManager
 from netaddr import IPAddress
 from netaddr import IPAddress
 import ldapdb.models
 import ldapdb.models

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

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

+ 3 - 2
coin/templates/base.html

@@ -1,5 +1,6 @@
 <!doctype html>
 <!doctype html>
 {% load staticfiles %}
 {% load staticfiles %}
+{% load i18n %}
 <html class="no-js" lang="en">
 <html class="no-js" lang="en">
 <head>
 <head>
     <meta charset="utf-8" />
     <meta charset="utf-8" />
@@ -20,7 +21,7 @@
         {% if user.is_authenticated %}
         {% if user.is_authenticated %}
         <aside class="left-off-canvas-menu">
         <aside class="left-off-canvas-menu">
             <ul class="off-canvas-list">
             <ul class="off-canvas-list">
-                <li><label>Menu</label></li>
+                <li><label>{% trans 'Menu' %}</label></li>
                 {% include "menu_items.html" %}
                 {% include "menu_items.html" %}
             </ul>
             </ul>
         </aside>
         </aside>
@@ -30,7 +31,7 @@
             <div class="show-for-medium-up">
             <div class="show-for-medium-up">
                 <div class="row">
                 <div class="row">
                     <div class="large-12">
                     <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>
                 </div>
             </div>
             </div>

+ 9 - 8
coin/templates/menu_items.html

@@ -1,11 +1,12 @@
 {% load activelink %}
 {% 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>
 <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="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.loader import get_template
 from django.template import Context, TemplateDoesNotExist
 from django.template import Context, TemplateDoesNotExist
 from django.contrib.sites.models import Site
 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)
 # Stockage des fichiers privés (comme les factures par exemple)
@@ -92,7 +93,7 @@ def delete_selected(modeladmin, request, queryset):
     for obj in queryset:
     for obj in queryset:
         obj.delete()
         obj.delete()
 
 
-delete_selected.short_description = "Supprimer tous les objets sélectionnés."
+delete_selected.short_description = _("Delete all selected objects.")
 
 
 # Time-related functions
 # Time-related functions
 
 

+ 2 - 1
coin/vpn/apps.py

@@ -1,8 +1,9 @@
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 from django.apps import AppConfig
 from django.apps import AppConfig
+from django.utils.translation import ugettext_lazy as _
 
 
 
 
 class VPNConfig(AppConfig):
 class VPNConfig(AppConfig):
         name = "coin.vpn"
         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 __future__ import unicode_literals
 
 
 from django.db import models
 from django.db import models
+from django.utils.translation import ugettext_lazy as _
 from django.core.exceptions import ValidationError
 from django.core.exceptions import ValidationError
 from django.conf import settings
 from django.conf import settings
 from django.core.urlresolvers import reverse
 from django.core.urlresolvers import reverse
@@ -24,20 +25,22 @@ class VPNConfiguration(CoinLdapSyncMixin, Configuration):
     #     'offers.OfferSubscription',
     #     'offers.OfferSubscription',
     #     related_name=backend_name,
     #     related_name=backend_name,
     #     validators=[ValidateBackendType(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,
     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)
                                 blank=True, null=True)
     ipv4_endpoint = InetAddressField(validators=[validation.validate_v4],
     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],
     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()
     objects = NetManager()
 
 
@@ -108,24 +111,26 @@ class VPNConfiguration(CoinLdapSyncMixin, Configuration):
         If [delete] is True, then simply delete the faulty endpoints
         If [delete] is True, then simply delete the faulty endpoints
         instead of raising an exception.
         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()
         subnets = self.ip_subnet.all()
         is_faulty = lambda endpoint : endpoint and not any([endpoint in subnet.inet for subnet in subnets])
         is_faulty = lambda endpoint : endpoint and not any([endpoint in subnet.inet for subnet in subnets])
         if is_faulty(self.ipv4_endpoint):
         if is_faulty(self.ipv4_endpoint):
             if delete:
             if delete:
                 self.ipv4_endpoint = None
                 self.ipv4_endpoint = None
             else:
             else:
-                raise ValidationError(error.format(self.ipv4_endpoint))
+                raise ValidationError(error.format(ip=self.ipv4_endpoint))
         if is_faulty(self.ipv6_endpoint):
         if is_faulty(self.ipv6_endpoint):
             if delete:
             if delete:
                 self.ipv6_endpoint = None
                 self.ipv6_endpoint = None
             else:
             else:
-                raise ValidationError(error.format(self.ipv6_endpoint))
+                raise ValidationError(error.format(ip=self.ipv6_endpoint))
 
 
     def clean(self):
     def clean(self):
         # Generate VPN login, of the form "login-vpnX".  The resulting
         # Generate VPN login, of the form "login-vpnX".  The resulting
         # login should not contain any ".", because graphite uses "." as a
         # login should not contain any ".", because graphite uses "." as a
         # separator.
         # separator.
+        from django.utils.translation import ugettext as _
         if not self.login:
         if not self.login:
             username = self.offersubscription.member.username
             username = self.offersubscription.member.username
             vpns = VPNConfiguration.objects.filter(offersubscription__member__username=username)
             vpns = VPNConfiguration.objects.filter(offersubscription__member__username=username)
@@ -138,7 +143,7 @@ class VPNConfiguration(CoinLdapSyncMixin, Configuration):
                     break
                     break
             # We may have failed.
             # We may have failed.
             if not self.login:
             if not self.login:
-                ValidationError("Impossible de générer un login VPN")
+                ValidationError(_("Unable to allocate a VPN login."))
         # Hash password if needed
         # Hash password if needed
         self.password = utils.ldap_hash(self.password)
         self.password = utils.ldap_hash(self.password)
         # If saving for the first time and IP endpoints are not specified,
         # If saving for the first time and IP endpoints are not specified,
@@ -148,10 +153,12 @@ class VPNConfiguration(CoinLdapSyncMixin, Configuration):
         self.check_endpoints()
         self.check_endpoints()
 
 
     def __unicode__(self):
     def __unicode__(self):
-        return 'VPN ' + self.login
+        from django.utils.translation import ugettext as _
+        return _('VPN <{login}>').format(login=self.login)
 
 
     class Meta:
     class Meta:
-        verbose_name = 'VPN'
+        verbose_name = _('VPN')
+        verbose_name_plural = _('VPNs')
 
 
 
 
 class LdapVPNConfig(ldapdb.models.Model):
 class LdapVPNConfig(ldapdb.models.Model):

+ 2 - 1
simple_dsl/apps.py

@@ -1,8 +1,9 @@
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 from django.apps import AppConfig
 from django.apps import AppConfig
+from django.utils.translation import ugettext_lazy as _
 
 
 
 
 class SimpleDSLConfig(AppConfig):
 class SimpleDSLConfig(AppConfig):
         name = "simple_dsl"
         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 __future__ import unicode_literals
 
 
 from django.db import models
 from django.db import models
+from django.utils.translation import ugettext_lazy as _
 
 
 from coin.configuration.models import Configuration
 from coin.configuration.models import Configuration
 
 
@@ -14,17 +15,16 @@ class SimpleDSL(Configuration):
     DSL reselling.
     DSL reselling.
     """
     """
     class Meta:
     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
     # URL namespace associated to this configuration type, to build URLs
     # in various view.  Should also be defined in urls.py.  Here, we don't
     # 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.
     # define any view, so there's no need for an URL namespace.
     #url_namespace = "dsl"
     #url_namespace = "dsl"
     phone_number = models.CharField(max_length=20,
     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):
     def __unicode__(self):
         return self.phone_number
         return self.phone_number