diff --git a/settings/common.py b/settings/common.py
index 3be896a0..2a219fe2 100644
--- a/settings/common.py
+++ b/settings/common.py
@@ -221,6 +221,7 @@ DEFAULT_FILE_STORAGE = "taiga.base.storage.FileSystemStorage"
LOCALE_PATHS = (
os.path.join(BASE_DIR, "locale"),
+ os.path.join(BASE_DIR, "taiga", "locale"),
)
SECRET_KEY = "aw3+t2r(8(0kkrhg8)gx6i96v5^kv%6cfep9wxfom0%7dy0m9e"
diff --git a/taiga/base/api/fields.py b/taiga/base/api/fields.py
index 48a2250c..942878f7 100644
--- a/taiga/base/api/fields.py
+++ b/taiga/base/api/fields.py
@@ -38,6 +38,7 @@ from django.utils.encoding import smart_text
from django.utils.encoding import force_text
from django.utils.encoding import is_protected_type
from django.utils.functional import Promise
+from django.utils.translation import ugettext
from django.utils.translation import ugettext_lazy as _
from django.utils.datastructures import SortedDict
@@ -133,6 +134,7 @@ def humanize_strptime(format_string):
class Field(object):
read_only = True
+ i18n = False
creation_counter = 0
empty = ""
type_name = None
@@ -142,7 +144,7 @@ class Field(object):
type_label = "field"
widget = None
- def __init__(self, source=None, label=None, help_text=None):
+ def __init__(self, source=None, label=None, help_text=None, i18n=False):
self.parent = None
self.creation_counter = Field.creation_counter
@@ -159,6 +161,7 @@ class Field(object):
self._errors = []
self._value = None
self._name = None
+ self.i18n = i18n
@property
def errors(self):
@@ -271,7 +274,7 @@ class WritableField(Field):
def __init__(self, source=None, label=None, help_text=None,
read_only=False, write_only=False, required=None,
validators=[], error_messages=None, widget=None,
- default=None, blank=None):
+ default=None, blank=None, i18n=False):
# "blank" is to be deprecated in favor of "required"
if blank is not None:
@@ -280,7 +283,8 @@ class WritableField(Field):
DeprecationWarning, stacklevel=2)
required = not(blank)
- super(WritableField, self).__init__(source=source, label=label, help_text=help_text)
+ super(WritableField, self).__init__(source=source, label=label,
+ help_text=help_text, i18n=i18n)
self.read_only = read_only
self.write_only = write_only
@@ -486,6 +490,13 @@ class CharField(WritableField):
return value
return smart_text(value)
+ def to_native(self, value):
+ ret = super(CharField, self).to_native(value)
+ if self.i18n:
+ ret = ugettext(ret)
+
+ return ret
+
class URLField(CharField):
type_name = "URLField"
diff --git a/taiga/base/api/serializers.py b/taiga/base/api/serializers.py
index f82dcd2c..979fb520 100644
--- a/taiga/base/api/serializers.py
+++ b/taiga/base/api/serializers.py
@@ -673,6 +673,7 @@ class ModelSerializerOptions(SerializerOptions):
def __init__(self, meta):
super(ModelSerializerOptions, self).__init__(meta)
self.model = getattr(meta, "model", None)
+ self.i18n_fields = getattr(meta, "i18n_fields", ())
self.read_only_fields = getattr(meta, "read_only_fields", ())
self.write_only_fields = getattr(meta, "write_only_fields", ())
@@ -837,6 +838,11 @@ class ModelSerializer((six.with_metaclass(SerializerMetaclass, BaseSerializer)))
(field_name, self.__class__.__name__))
ret[field_name].write_only = True
+ # Add the `i18n` flag to any fields that have been specified
+ # in the `i18n_fields` option
+ for field_name in self.opts.i18n_fields:
+ ret[field_name].i18n = True
+
return ret
def get_pk_field(self, model_field):
@@ -929,6 +935,9 @@ class ModelSerializer((six.with_metaclass(SerializerMetaclass, BaseSerializer)))
for attribute in attributes:
kwargs.update({attribute: getattr(model_field, attribute)})
+ if model_field.name in self.opts.i18n_fields:
+ kwargs["i18n"] = True
+
try:
return self.field_mapping[model_field.__class__](**kwargs)
except KeyError:
diff --git a/taiga/base/fields.py b/taiga/base/fields.py
index 8c49283f..69ce3868 100644
--- a/taiga/base/fields.py
+++ b/taiga/base/fields.py
@@ -15,6 +15,7 @@
# along with this program. If not, see .
from django.forms import widgets
+from django.utils.translation import ugettext as _
from taiga.base.api import serializers
@@ -36,6 +37,37 @@ class JsonField(serializers.WritableField):
return data
+class I18NJsonField(JsonField):
+ """
+ Json objects serializer.
+ """
+ widget = widgets.Textarea
+
+ def __init__(self, i18n_fields=(), *args, **kwargs):
+ super(I18NJsonField, self).__init__(*args, **kwargs)
+ self.i18n_fields = i18n_fields
+
+ def translate_values(self, d):
+ i18n_d = {}
+ for key, value in d.items():
+ if isinstance(value, dict):
+ i18n_d[key] = self.translate_values(value)
+
+ if key in self.i18n_fields:
+ if isinstance(value, list):
+ i18n_d[key] = [_(e) for e in value]
+ if isinstance(value, str):
+ i18n_d[key] = _(value)
+ else:
+ i18n_d[key] = value
+
+ return i18n_d
+
+ def to_native(self, obj):
+ i18n_obj = self.translate_values(obj)
+ return i18n_obj
+
+
class PgArrayField(serializers.WritableField):
"""
PgArray objects serializer.
diff --git a/taiga/locale/en/LC_MESSAGES/django.po b/taiga/locale/en/LC_MESSAGES/django.po
index db271a69..dd309a6c 100644
--- a/taiga/locale/en/LC_MESSAGES/django.po
+++ b/taiga/locale/en/LC_MESSAGES/django.po
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2015-04-14 16:26+0200\n"
+"POT-Creation-Date: 2015-04-14 18:00+0200\n"
"PO-Revision-Date: 2015-03-25 20:09+0100\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Taiga Dev Team \n"
@@ -1618,160 +1618,236 @@ msgid ""
"[Taiga] Added to the project '%(project)s'\n"
msgstr ""
-#: taiga/projects/translations.py:3
-msgid "?"
-msgstr ""
-
-#: taiga/projects/translations.py:4
-msgid "0"
-msgstr ""
-
-#: taiga/projects/translations.py:5
-msgid "1/2"
-msgstr ""
-
-#: taiga/projects/translations.py:6
-msgid "1"
-msgstr ""
-
-#: taiga/projects/translations.py:7
-msgid "2"
-msgstr ""
-
-#: taiga/projects/translations.py:8
-msgid "3"
-msgstr ""
-
-#: taiga/projects/translations.py:9
-msgid "5"
-msgstr ""
-
-#: taiga/projects/translations.py:10
-msgid "8"
-msgstr ""
-
-#: taiga/projects/translations.py:11
-msgid "10"
-msgstr ""
-
-#: taiga/projects/translations.py:12
-msgid "15"
-msgstr ""
-
-#: taiga/projects/translations.py:13
-msgid "20"
-msgstr ""
-
-#: taiga/projects/translations.py:14
-msgid "40"
-msgstr ""
-
-#: taiga/projects/translations.py:15
+#. Translators: Name of scrum project template.
+#: taiga/projects/translations.py:28
msgid "Scrum"
msgstr ""
-#: taiga/projects/translations.py:16
-msgid "Archived"
-msgstr ""
-
-#: taiga/projects/translations.py:17
-msgid "Back"
-msgstr ""
-
-#: taiga/projects/translations.py:18
-msgid "Bug"
-msgstr ""
-
-#: taiga/projects/translations.py:19
-msgid "Closed"
-msgstr ""
-
-#: taiga/projects/translations.py:20
-msgid "Critical"
-msgstr ""
-
-#: taiga/projects/translations.py:21
-msgid "Design"
-msgstr ""
-
-#: taiga/projects/translations.py:22
-msgid "Done"
-msgstr ""
-
-#: taiga/projects/translations.py:23
-msgid "Enhancement"
-msgstr ""
-
-#: taiga/projects/translations.py:24
-msgid "Front"
-msgstr ""
-
-#: taiga/projects/translations.py:25
-msgid "High"
-msgstr ""
-
-#: taiga/projects/translations.py:26
-msgid "Important"
-msgstr ""
-
-#: taiga/projects/translations.py:27
-msgid "In progress"
-msgstr ""
-
-#: taiga/projects/translations.py:28
-msgid "Low"
-msgstr ""
-
-#: taiga/projects/translations.py:29
-msgid "Minor"
-msgstr ""
-
+#. Translators: Description of scrum project template.
#: taiga/projects/translations.py:30
-msgid "Needs Info"
+msgid ""
+"The agile product backlog in Scrum is a prioritized features list, "
+"containing short descriptions of all functionality desired in the product. "
+"When applying Scrum, it's not necessary to start a project with a lengthy, "
+"upfront effort to document all requirements. The Scrum product backlog is "
+"then allowed to grow and change as more is learned about the product and its "
+"customers"
msgstr ""
-#: taiga/projects/translations.py:31
+#. Translators: Name of kanban project template.
+#: taiga/projects/translations.py:33
+msgid "Kanban"
+msgstr ""
+
+#. Translators: Description of kanban project template.
+#: taiga/projects/translations.py:35
+msgid ""
+"Kanban is a method for managing knowledge work with an emphasis on just-in-"
+"time delivery while not overloading the team members. In this approach, the "
+"process, from definition of a task to its delivery to the customer, is "
+"displayed for participants to see and team members pull work from a queue."
+msgstr ""
+
+#. Translators: User story point value (value = undefined)
+#: taiga/projects/translations.py:43
+msgid "?"
+msgstr ""
+
+#. Translators: User story point value (value = 0)
+#: taiga/projects/translations.py:45
+msgid "0"
+msgstr ""
+
+#. Translators: User story point value (value = 0.5)
+#: taiga/projects/translations.py:47
+msgid "1/2"
+msgstr ""
+
+#. Translators: User story point value (value = 1)
+#: taiga/projects/translations.py:49
+msgid "1"
+msgstr ""
+
+#. Translators: User story point value (value = 2)
+#: taiga/projects/translations.py:51
+msgid "2"
+msgstr ""
+
+#. Translators: User story point value (value = 3)
+#: taiga/projects/translations.py:53
+msgid "3"
+msgstr ""
+
+#. Translators: User story point value (value = 5)
+#: taiga/projects/translations.py:55
+msgid "5"
+msgstr ""
+
+#. Translators: User story point value (value = 8)
+#: taiga/projects/translations.py:57
+msgid "8"
+msgstr ""
+
+#. Translators: User story point value (value = 10)
+#: taiga/projects/translations.py:59
+msgid "10"
+msgstr ""
+
+#. Translators: User story point value (value = 15)
+#: taiga/projects/translations.py:61
+msgid "15"
+msgstr ""
+
+#. Translators: User story point value (value = 20)
+#: taiga/projects/translations.py:63
+msgid "20"
+msgstr ""
+
+#. Translators: User story point value (value = 40)
+#: taiga/projects/translations.py:65
+msgid "40"
+msgstr ""
+
+#. Translators: User story status
+#. Translators: Task status
+#. Translators: Issue status
+#: taiga/projects/translations.py:73 taiga/projects/translations.py:96
+#: taiga/projects/translations.py:112
msgid "New"
msgstr ""
-#: taiga/projects/translations.py:32
-msgid "Normal"
-msgstr ""
-
-#: taiga/projects/translations.py:33
-msgid "Postponed"
-msgstr ""
-
-#: taiga/projects/translations.py:34
-msgid "Product Owner"
-msgstr ""
-
-#: taiga/projects/translations.py:35
-msgid "Question"
-msgstr ""
-
-#: taiga/projects/translations.py:36
+#. Translators: User story status
+#: taiga/projects/translations.py:76
msgid "Ready"
msgstr ""
-#: taiga/projects/translations.py:37
+#. Translators: User story status
+#. Translators: Task status
+#. Translators: Issue status
+#: taiga/projects/translations.py:79 taiga/projects/translations.py:98
+#: taiga/projects/translations.py:114
+msgid "In progress"
+msgstr ""
+
+#. Translators: User story status
+#. Translators: Task status
+#. Translators: Issue status
+#: taiga/projects/translations.py:82 taiga/projects/translations.py:100
+#: taiga/projects/translations.py:116
msgid "Ready for test"
msgstr ""
-#: taiga/projects/translations.py:38
+#. Translators: User story status
+#: taiga/projects/translations.py:85
+msgid "Done"
+msgstr ""
+
+#. Translators: User story status
+#: taiga/projects/translations.py:88
+msgid "Archived"
+msgstr ""
+
+#. Translators: Task status
+#. Translators: Issue status
+#: taiga/projects/translations.py:102 taiga/projects/translations.py:118
+msgid "Closed"
+msgstr ""
+
+#. Translators: Task status
+#. Translators: Issue status
+#: taiga/projects/translations.py:104 taiga/projects/translations.py:120
+msgid "Needs Info"
+msgstr ""
+
+#. Translators: Issue status
+#: taiga/projects/translations.py:122
+msgid "Postponed"
+msgstr ""
+
+#. Translators: Issue status
+#: taiga/projects/translations.py:124
msgid "Rejected"
msgstr ""
-#: taiga/projects/translations.py:39
-msgid "Stakeholder"
+#. Translators: Issue type
+#: taiga/projects/translations.py:132
+msgid "Bug"
msgstr ""
-#: taiga/projects/translations.py:40
+#. Translators: Issue type
+#: taiga/projects/translations.py:134
+msgid "Question"
+msgstr ""
+
+#. Translators: Issue type
+#: taiga/projects/translations.py:136
+msgid "Enhancement"
+msgstr ""
+
+#. Translators: Issue priority
+#: taiga/projects/translations.py:144
+msgid "Low"
+msgstr ""
+
+#. Translators: Issue priority
+#. Translators: Issue severity
+#: taiga/projects/translations.py:146 taiga/projects/translations.py:159
+msgid "Normal"
+msgstr ""
+
+#. Translators: Issue priority
+#: taiga/projects/translations.py:148
+msgid "High"
+msgstr ""
+
+#. Translators: Issue severity
+#: taiga/projects/translations.py:155
+msgid "Wishlist"
+msgstr ""
+
+#. Translators: Issue severity
+#: taiga/projects/translations.py:157
+msgid "Minor"
+msgstr ""
+
+#. Translators: Issue severity
+#: taiga/projects/translations.py:161
+msgid "Important"
+msgstr ""
+
+#. Translators: Issue severity
+#: taiga/projects/translations.py:163
+msgid "Critical"
+msgstr ""
+
+#. Translators: User role
+#: taiga/projects/translations.py:170
msgid "UX"
msgstr ""
-#: taiga/projects/translations.py:41
-msgid "Wishlist"
+#. Translators: User role
+#: taiga/projects/translations.py:172
+msgid "Design"
+msgstr ""
+
+#. Translators: User role
+#: taiga/projects/translations.py:174
+msgid "Front"
+msgstr ""
+
+#. Translators: User role
+#: taiga/projects/translations.py:176
+msgid "Back"
+msgstr ""
+
+#. Translators: User role
+#: taiga/projects/translations.py:178
+msgid "Product Owner"
+msgstr ""
+
+#. Translators: User role
+#: taiga/projects/translations.py:180
+msgid "Stakeholder"
msgstr ""
#: taiga/projects/userstories/api.py:170
diff --git a/taiga/projects/history/serializers.py b/taiga/projects/history/serializers.py
index 6e0b0911..4834e504 100644
--- a/taiga/projects/history/serializers.py
+++ b/taiga/projects/history/serializers.py
@@ -15,20 +15,19 @@
# along with this program. If not, see .
from taiga.base.api import serializers
-
-from taiga.base.fields import JsonField
+from taiga.base.fields import JsonField, I18NJsonField
from . import models
+HISTORY_ENTRY_I18N_FIELDS=("points", "status", "severity", "priority", "type")
class HistoryEntrySerializer(serializers.ModelSerializer):
diff = JsonField()
snapshot = JsonField()
- values = JsonField()
- values_diff = JsonField()
+ values = I18NJsonField(i18n_fields=HISTORY_ENTRY_I18N_FIELDS)
+ values_diff = I18NJsonField(i18n_fields=HISTORY_ENTRY_I18N_FIELDS)
user = JsonField()
delete_comment_user = JsonField()
class Meta:
model = models.HistoryEntry
-
diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py
index f4d51b0a..28f9cad5 100644
--- a/taiga/projects/serializers.py
+++ b/taiga/projects/serializers.py
@@ -47,6 +47,7 @@ from .custom_attributes.serializers import IssueCustomAttributeSerializer
class PointsSerializer(serializers.ModelSerializer):
class Meta:
model = models.Points
+ i18n_fields = ("name",)
def validate_name(self, attrs, source):
"""
@@ -67,8 +68,10 @@ class PointsSerializer(serializers.ModelSerializer):
class UserStoryStatusSerializer(serializers.ModelSerializer):
+
class Meta:
model = models.UserStoryStatus
+ i18n_fields = ("name",)
def validate_name(self, attrs, source):
"""
@@ -93,6 +96,7 @@ class UserStoryStatusSerializer(serializers.ModelSerializer):
class TaskStatusSerializer(serializers.ModelSerializer):
class Meta:
model = models.TaskStatus
+ i18n_fields = ("name",)
def validate_name(self, attrs, source):
"""
@@ -115,16 +119,19 @@ class TaskStatusSerializer(serializers.ModelSerializer):
class SeveritySerializer(serializers.ModelSerializer):
class Meta:
model = models.Severity
+ i18n_fields = ("name",)
class PrioritySerializer(serializers.ModelSerializer):
class Meta:
model = models.Priority
+ i18n_fields = ("name",)
class IssueStatusSerializer(serializers.ModelSerializer):
class Meta:
model = models.IssueStatus
+ i18n_fields = ("name",)
def validate_name(self, attrs, source):
"""
@@ -147,6 +154,7 @@ class IssueStatusSerializer(serializers.ModelSerializer):
class IssueTypeSerializer(serializers.ModelSerializer):
class Meta:
model = models.IssueType
+ i18n_fields = ("name",)
######################################################
@@ -380,3 +388,4 @@ class ProjectTemplateSerializer(serializers.ModelSerializer):
class Meta:
model = models.ProjectTemplate
read_only_fields = ("created_date", "modified_date")
+ i18n_fields = ("name", "description")
diff --git a/taiga/projects/translations.py b/taiga/projects/translations.py
new file mode 100644
index 00000000..347eb6c8
--- /dev/null
+++ b/taiga/projects/translations.py
@@ -0,0 +1,180 @@
+# Copyright (C) 2015 Andrey Antukh
+# Copyright (C) 2015 Jesús Espino
+# Copyright (C) 2015 David Barragán
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+# NOTE: This file is useful to translate default projects templates. Remember update
+# when taiga/projects/fixtures/initial_project_templates.json change.
+
+from django.utils.translation import ugettext as _
+
+
+##########################
+## Default template information
+##########################
+
+# Translators: Name of scrum project template.
+_("Scrum")
+# Translators: Description of scrum project template.
+_("The agile product backlog in Scrum is a prioritized features list, containing short descriptions of all functionality desired in the product. When applying Scrum, it's not necessary to start a project with a lengthy, upfront effort to document all requirements. The Scrum product backlog is then allowed to grow and change as more is learned about the product and its customers")
+
+# Translators: Name of kanban project template.
+_("Kanban")
+# Translators: Description of kanban project template.
+_("Kanban is a method for managing knowledge work with an emphasis on just-in-time delivery while not overloading the team members. In this approach, the process, from definition of a task to its delivery to the customer, is displayed for participants to see and team members pull work from a queue.")
+
+
+##########################
+## US Points
+##########################
+
+# Translators: User story point value (value = undefined)
+_("?")
+# Translators: User story point value (value = 0)
+_("0")
+# Translators: User story point value (value = 0.5)
+_("1/2")
+# Translators: User story point value (value = 1)
+_("1")
+# Translators: User story point value (value = 2)
+_("2")
+# Translators: User story point value (value = 3)
+_("3")
+# Translators: User story point value (value = 5)
+_("5")
+# Translators: User story point value (value = 8)
+_("8")
+# Translators: User story point value (value = 10)
+_("10")
+# Translators: User story point value (value = 15)
+_("15")
+# Translators: User story point value (value = 20)
+_("20")
+# Translators: User story point value (value = 40)
+_("40")
+
+
+##########################
+## US Statuses
+##########################
+
+# Translators: User story status
+_("New")
+
+# Translators: User story status
+_("Ready")
+
+# Translators: User story status
+_("In progress")
+
+# Translators: User story status
+_("Ready for test")
+
+# Translators: User story status
+_("Done")
+
+# Translators: User story status
+_("Archived")
+
+
+##########################
+## Task Statuses
+##########################
+
+# Translators: Task status
+_("New")
+# Translators: Task status
+_("In progress")
+# Translators: Task status
+_("Ready for test")
+# Translators: Task status
+_("Closed")
+# Translators: Task status
+_("Needs Info")
+
+
+##########################
+## Issue Statuses
+##########################
+
+# Translators: Issue status
+_("New")
+# Translators: Issue status
+_("In progress")
+# Translators: Issue status
+_("Ready for test")
+# Translators: Issue status
+_("Closed")
+# Translators: Issue status
+_("Needs Info")
+# Translators: Issue status
+_("Postponed")
+# Translators: Issue status
+_("Rejected")
+
+
+##########################
+## Issue Statuses
+##########################
+
+# Translators: Issue type
+_("Bug")
+# Translators: Issue type
+_("Question")
+# Translators: Issue type
+_("Enhancement")
+
+
+##########################
+## Priorities
+##########################
+
+# Translators: Issue priority
+_("Low")
+# Translators: Issue priority
+_("Normal")
+# Translators: Issue priority
+_("High")
+
+
+##########################
+## Severities
+##########################
+# Translators: Issue severity
+_("Wishlist")
+# Translators: Issue severity
+_("Minor")
+# Translators: Issue severity
+_("Normal")
+# Translators: Issue severity
+_("Important")
+# Translators: Issue severity
+_("Critical")
+
+
+##########################
+## Roles
+##########################
+# Translators: User role
+_("UX")
+# Translators: User role
+_("Design")
+# Translators: User role
+_("Front")
+# Translators: User role
+_("Back")
+# Translators: User role
+_("Product Owner")
+# Translators: User role
+_("Stakeholder")
diff --git a/tests/integration/resources_permissions/test_users_resources.py b/tests/integration/resources_permissions/test_users_resources.py
index 7e3f9659..6c1270ad 100644
--- a/tests/integration/resources_permissions/test_users_resources.py
+++ b/tests/integration/resources_permissions/test_users_resources.py
@@ -72,6 +72,7 @@ def test_user_update(client, data):
user_data = UserSerializer(data.registered_user).data
user_data["full_name"] = "test"
user_data = json.dumps(user_data)
+
results = helper_test_http_method(client, 'put', url, user_data, users)
assert results == [401, 200, 403, 200]