Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

key/value field drop-in replacement for `TranslatedField` #1194

Closed
wants to merge 1 commit into from

6 participants

@cvan
Owner

r? @diox

TODO:

  • Use a distributed key-value store (like riak)
  • Cache the calls to redis on the object
  • Key off of language (and have an object of all the {'name': 'Name', 'description': 'Description'} for each {table}:{pk}:{lang} instead of {table}:{pk}:{field}
  • Use pickle instead of JSON
  • Migration for other table's translations FKs
  • Migration that populates the data store from all the translations from the translations table
  • Consider writing a TextField wrapper instead of just registering ordinary TextField fields
  • Add default_language columns

Sample commands:

In [1]: Category.objects.create(type=11, name='The Category')
09:54:25 django.db.backends:DEBUG (0.004) INSERT INTO `categories` (`created`, `modified`, `name`, `slug`, `addontype_id`, `application_id`, `count`, `weight`, `misc`, `carrier`, `region`) VALUES ('2013-09-26 09:54:25', '2013-09-26 09:54:25', 'The Category', '', 11, NULL, 0, 0, 0, NULL, NULL); args=[u'2013-09-26 09:54:25', u'2013-09-26 09:54:25', 'The Category', '', 11, None, 0, 0, False, None, None] :/Users/chris/.virtualenvs/z/lib/python2.6/site-packages/django/db/backends/util.py:50
09:54:26 django.db.backends:DEBUG (0.000) SELECT (1) AS `a` FROM `categories` WHERE `categories`.`id` = 189  LIMIT 1; args=(189,) :/Users/chris/.virtualenvs/z/lib/python2.6/site-packages/django/db/backends/util.py:50
09:54:26 django.db.backends:DEBUG (0.002) UPDATE `categories` SET `created` = '2013-09-26 09:54:25', `modified` = '2013-09-26 09:54:26', `name` = 'The Category', `slug` = '', `addontype_id` = 11, `application_id` = NULL, `count` = 0, `weight` = 0, `misc` = 0, `carrier` = NULL, `region` = NULL WHERE `categories`.`id` = 189 ; args=(u'2013-09-26 09:54:25', u'2013-09-26 09:54:26', 'The Category', '', 11, 0, 0, False, 189) :/Users/chris/.virtualenvs/z/lib/python2.6/site-packages/django/db/backends/util.py:50
Out[1]: <Category: The Category>

In [2]: c=Category.objects.order_by('-id')[0]
09:54:26 django.db.backends:DEBUG (0.000) SELECT `categories`.`id` FROM `categories` ORDER BY `categories`.`id` DESC LIMIT 1; args=() :/Users/chris/.virtualenvs/z/lib/python2.6/site-packages/django/db/backends/util.py:50
09:54:26 django.db.backends:DEBUG (0.000) SELECT `categories`.`id`, `categories`.`created`, `categories`.`modified`, `categories`.`name`, `categories`.`slug`, `categories`.`addontype_id`, `categories`.`application_id`, `categories`.`count`, `categories`.`weight`, `categories`.`misc`, `categories`.`carrier`, `categories`.`region` FROM `categories` WHERE `categories`.`id` IN (189); args=(189,) :/Users/chris/.virtualenvs/z/lib/python2.6/site-packages/django/db/backends/util.py:50

In [3]: c.name
Out[3]: <Trans: The Category>

In [4]: unicode(c.name)
Out[4]: u'The Category'

In [5]: c.name = {'fr': 'Le Category'}

In [6]: c.save()
09:54:31 django.db.backends:DEBUG (0.000) SELECT (1) AS `a` FROM `categories` WHERE `categories`.`id` = 189  LIMIT 1; args=(189,) :/Users/chris/.virtualenvs/z/lib/python2.6/site-packages/django/db/backends/util.py:50
09:54:31 django.db.backends:DEBUG (0.001) UPDATE `categories` SET `created` = '2013-09-26 09:54:25', `modified` = '2013-09-26 09:54:31', `name` = '{\'fr\': \'Le Category\'}', `slug` = '', `addontype_id` = 11, `application_id` = NULL, `count` = 0, `weight` = 0, `misc` = 0, `carrier` = NULL, `region` = NULL WHERE `categories`.`id` = 189 ; args=(u'2013-09-26 09:54:25', u'2013-09-26 09:54:31', u"{'fr': 'Le Category'}", u'', 11, 0, 0, False, 189) :/Users/chris/.virtualenvs/z/lib/python2.6/site-packages/django/db/backends/util.py:50
09:54:31 django.db.backends:DEBUG (0.000) SELECT (1) AS `a` FROM `categories` WHERE `categories`.`id` = 189  LIMIT 1; args=(189,) :/Users/chris/.virtualenvs/z/lib/python2.6/site-packages/django/db/backends/util.py:50
09:54:31 django.db.backends:DEBUG (0.001) UPDATE `categories` SET `created` = '2013-09-26 09:54:25', `modified` = '2013-09-26 09:54:31', `name` = '{\'fr\': \'Le Category\'}', `slug` = '', `addontype_id` = 11, `application_id` = NULL, `count` = 0, `weight` = 0, `misc` = 0, `carrier` = NULL, `region` = NULL WHERE `categories`.`id` = 189 ; args=(u'2013-09-26 09:54:25', u'2013-09-26 09:54:31', u"{'fr': 'Le Category'}", u'', 11, 0, 0, False, 189) :/Users/chris/.virtualenvs/z/lib/python2.6/site-packages/django/db/backends/util.py:50

In [7]: # Still English.

In [8]: unicode(c.name)
Out[8]: u'The Category'

In [9]: c.name.__dict__()
Out[9]: {u'en-us': u'The Category', u'fr': u'Le Category'}

In [10]: import tower

In [11]: tower.activate('fr')

In [12]: unicode(c.name)
Out[12]: u'Le Category'

In [13]: tower.activate('en-US')

In [14]: unicode(c.name)
Out[14]: u'The Category'

In [15]: tower.activate('ar')

In [17]: c.name = 'New Fallback'

In [18]: c.save()
09:55:06 django.db.backends:DEBUG (0.000) SELECT (1) AS `a` FROM `categories` WHERE `categories`.`id` = 189  LIMIT 1; args=(189,) :/Users/chris/.virtualenvs/z/lib/python2.6/site-packages/django/db/backends/util.py:50
09:55:06 django.db.backends:DEBUG (0.001) UPDATE `categories` SET `created` = '2013-09-26 09:54:25', `modified` = '2013-09-26 09:55:06', `name` = 'New Fallback', `slug` = '', `addontype_id` = 11, `application_id` = NULL, `count` = 0, `weight` = 0, `misc` = 0, `carrier` = NULL, `region` = NULL WHERE `categories`.`id` = 189 ; args=(u'2013-09-26 09:54:25', u'2013-09-26 09:55:06', 'New Fallback', u'', 11, 0, 0, False, 189) :/Users/chris/.virtualenvs/z/lib/python2.6/site-packages/django/db/backends/util.py:50
09:55:06 django.db.backends:DEBUG (0.000) SELECT (1) AS `a` FROM `categories` WHERE `categories`.`id` = 189  LIMIT 1; args=(189,) :/Users/chris/.virtualenvs/z/lib/python2.6/site-packages/django/db/backends/util.py:50
09:55:06 django.db.backends:DEBUG (0.002) UPDATE `categories` SET `created` = '2013-09-26 09:54:25', `modified` = '2013-09-26 09:55:06', `name` = 'New Fallback', `slug` = '', `addontype_id` = 11, `application_id` = NULL, `count` = 0, `weight` = 0, `misc` = 0, `carrier` = NULL, `region` = NULL WHERE `categories`.`id` = 189 ; args=(u'2013-09-26 09:54:25', u'2013-09-26 09:55:06', 'New Fallback', u'', 11, 0, 0, False, 189) :/Users/chris/.virtualenvs/z/lib/python2.6/site-packages/django/db/backends/util.py:50

In [19]: c.name.__dict__()
Out[19]: {u'ar': u'New Fallback', u'en-us': u'The Category', u'fr': u'Le Category'}

In [20]: c.name
Out[20]: <Trans: New Fallback>

In [21]: unicode(c.name)
Out[21]: u'New Fallback'

In [22]: c = Category.objects.order_by('-id')[0]
09:55:23 django.db.backends:DEBUG (0.000) SELECT `categories`.`id` FROM `categories` ORDER BY `categories`.`id` DESC LIMIT 1; args=() :/Users/chris/.virtualenvs/z/lib/python2.6/site-packages/django/db/backends/util.py:50
09:55:23 django.db.backends:DEBUG (0.000) SELECT `categories`.`id`, `categories`.`created`, `categories`.`modified`, `categories`.`name`, `categories`.`slug`, `categories`.`addontype_id`, `categories`.`application_id`, `categories`.`count`, `categories`.`weight`, `categories`.`misc`, `categories`.`carrier`, `categories`.`region` FROM `categories` WHERE `categories`.`id` IN (189); args=(189,) :/Users/chris/.virtualenvs/z/lib/python2.6/site-packages/django/db/backends/util.py:50

In [23]: c.name
Out[23]: <Trans: New Fallback>

In [24]: unicode(c.name)
Out[24]: u'New Fallback'

In [25]: c.name.__dict__()
Out[25]: {u'ar': u'New Fallback', u'en-us': u'The Category', u'fr': u'Le Category'}

In [26]: c.delete()
09:55:27 django.db.backends:DEBUG (0.003) SELECT `zadmin_featuredapp`.`id` FROM `zadmin_featuredapp` INNER JOIN `addons` ON (`zadmin_featuredapp`.`app_id` = `addons`.`id`) WHERE (NOT (`addons`.`status` = 11 ) AND `zadmin_featuredapp`.`category_id` IN (189)); args=(11, 189) :/Users/chris/.virtualenvs/z/lib/python2.6/site-packages/django/db/backends/util.py:50
09:55:27 django.db.backends:DEBUG (0.001) SELECT `addons_categories`.`id` FROM `addons_categories` WHERE `addons_categories`.`category_id` IN (189); args=(189,) :/Users/chris/.virtualenvs/z/lib/python2.6/site-packages/django/db/backends/util.py:50
09:55:27 django.db.backends:DEBUG (0.002) SELECT `categories_supervisors`.`id` FROM `categories_supervisors` WHERE `categories_supervisors`.`category_id` IN (189); args=(189,) :/Users/chris/.virtualenvs/z/lib/python2.6/site-packages/django/db/backends/util.py:50
09:55:27 django.db.backends:DEBUG (0.001) SELECT `app_collections`.`id` FROM `app_collections` WHERE ("ar"="ar" AND `app_collections`.`category_id` IN (189)) ORDER BY `app_collections`.`id` DESC; args=(189,) :/Users/chris/.virtualenvs/z/lib/python2.6/site-packages/django/db/backends/util.py:50
09:55:27 django.db.backends:DEBUG (0.002) DELETE FROM `categories` WHERE `id` IN (189); args=(189,) :/Users/chris/.virtualenvs/z/lib/python2.6/site-packages/django/db/backends/util.py:50
@diox
Collaborator

the-matrix-whoa

@washort
Collaborator

what is the expected performance impact of this change?
How can we measure it?
How does this impact our ops situation (in terms of either continuing use of redis or in deploying a new storage component)?

@clouserw
Owner

Allen asks good questions. What other options do we have for the backend besides redis? Is the goal here to simplify code and get rid of our fugly queries?

@cvan
Owner

Allen asks good questions. What other options do we have for the backend besides redis? Is the goal here to simplify code and get rid of our fugly queries?

@robhudson had a pretty good idea which was to replace all the current translations FKs with TextFields and just store JSON blobs of the translations. I'm not opposed to that solution, and it'd actually be relatively painless to convert my patch to do that instead of using a key-value store.

@diox
Collaborator

A few random thoughts:

  • The current code problems are:
    • It's very magical, and un-DRY at times
    • It generates big, ugly queries (but is that really a problem ?)
    • It doesn't fall back to other languages when current language / fallback language on the object returns no results
    • It's difficult to delete individual translations (we now have code that works for it but it's a bit complex, and the previous point makes it dangerous)
  • The current code advantages are:
    • You only fetch what you need
    • Somewhat efficient at storing (both in memory in the python process and in database) : you can easily change individual translations, affect a bunch of them etc without touching the rest, and it's easy to cache

I spent some time starting to document how it works in https://gist.github.com/diox/e99d74a8e445032b49fa and I concluded when writing this that the current problems are all fixable, it's just hard.

IMHO, If we want to have a replacement, it needs to fix those problems and not regress on those advantages. In addition, one important thing it needs to support is order_by_translation() - which, essentially, allows you to sort something that has translations by the translated field or your choice, like name. There are a bunch of views using it in AMO, and a couple left in Marketplace.

@robhudson
Collaborator

Yeah, as @diox points out TextFields wouldn't be able to sort.

If keeping the translations app is the way to go I'd encourage us to make it a django package and release it. It has some obvious advantages to other translation apps I've seen in the django community and we could clean it up and possibly get some contributors. If we go that way I'd be happy to help do this.

@cvan
Owner
@andymckay
Owner

Whats the status here, we going to do this?

@cvan
Owner

Whats the status here, we going to do this?

let's leave it until the work week

@robhudson
Collaborator

let's leave it until the work week

Work week is over. Is there a bug? Should we close this but point to it in the bug?

@diox
Collaborator

FWIW:

  • mozilla/fireplace#404 will make the Categories static (and translated through regular .po files)
  • mozilla/olympia#18 will make the translation app handle fallback in a much better way and removing the last big obstacle concerning deletion

Once those are merged I intend to move the translation app to a separate package released on pypi. The only real pain points remaining will be that the app is fairly magical and un-dry, and that it's specific to MySQL for now, but that's fixable.

@cvan cvan closed this
@cvan cvan deleted the branch
@cvan cvan restored the branch
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Sep 26, 2013
  1. @cvan

    l10n k1n6

    cvan authored
This page is out of date. Refresh to see the latest.
View
6 apps/addons/models.py
@@ -44,6 +44,7 @@
from tags.models import Tag
from translations.fields import (LinkifiedField, PurifiedField, save_signal,
TranslatedField, Translation)
+from translations.fields_new import register_trans
from translations.query import order_by_translation
from users.models import UserForeignKey, UserProfile
from versions.compare import version_int
@@ -2002,7 +2003,7 @@ def __unicode__(self):
class Category(amo.models.OnChangeMixin, amo.models.ModelBase):
- name = TranslatedField()
+ name = models.TextField(default='', blank=True)
slug = models.SlugField(max_length=50, help_text='Used in Category URLs.')
type = models.PositiveIntegerField(db_column='addontype_id',
choices=do_dictsort(amo.ADDON_TYPE))
@@ -2058,6 +2059,9 @@ def clean(self):
raise ValidationError('Slugs cannot be all numbers.')
+register_trans(Category, ('name',))
+
+
@Category.on_change
def reindex_cat_slug(old_attr=None, new_attr=None, instance=None,
sender=None, **kw):
View
284 apps/translations/fields_new.py
@@ -0,0 +1,284 @@
+import json
+
+from django.db.models.signals import post_delete, post_save, pre_save
+from django.utils import translation as translation_utils
+
+import redisutils
+
+
+KEY_PREFIX = 'l10n'
+REGISTRY = {}
+
+
+# Exception for being unable to communicate with the L10n data store.
+TransConnectionError = redisutils.redislib.ConnectionError
+
+# Exception for trying to save a localised string of an incorrect data type.
+TransValueError = ValueError('Must be a "str" or "dict"')
+
+
+class SafeRedis(object):
+
+ def __init__(self, obj):
+ self._wrapped_obj = obj
+
+ def __getattr__(self, attr):
+ if attr in self.__dict__:
+ return getattr(self, attr)
+
+ # TODO: Catch redisutils.redislib.ConnectionError
+ # and raise a generic exception. And add logging.
+ return getattr(self._wrapped_obj, attr)
+
+
+redis = SafeRedis(redisutils.connections['master'])
+
+
+class Trans(object):
+
+ def __init__(self, instance, name, *args, **kwargs):
+ self.instance = instance
+ self.name = name
+
+ def __dict__(self):
+ return get_translations(self.instance, self.name)
+
+ def __repr__(self):
+ return '<Trans: %s>' % self.instance.__dict__[self.name]
+
+ def __str__(self):
+ return self.__unicode__()
+
+ def __unicode__(self):
+ """
+ (1) Try the currently activated language.
+ (2) Try the instance's default language (if available).
+ (3) Use the fallback in the database.
+
+ TODO: Handle locales such as pt-BR, es-ES, etc.
+ """
+
+ data = get_translations(self.instance, self.name) or {}
+
+ langs_to_try = [
+ translation_utils.get_language(),
+ get_default_lang(self.instance),
+ ]
+
+ for lang in langs_to_try:
+ try:
+ if lang is not None:
+ return data[lang]
+ except KeyError:
+ continue
+
+ # Return the fallback in the database.
+ return self.instance.__dict__[self.name]
+
+ def get_key(self):
+ return get_key(self.instance, self.name)
+
+
+def get_default_lang(instance):
+ return (getattr(instance, 'default_lang', None) or
+ getattr(instance, 'default_locale', None))
+
+
+def get_key(instance, name):
+ return '%s:%s:%s:%s' % (KEY_PREFIX, instance._meta.db_table, instance.pk,
+ name)
+
+
+def get_translations(instance, name):
+ key = get_key(instance, name)
+
+ data = {}
+
+ try:
+ data = redis.get(key)
+ except TransConnectionError:
+ # Return the fallback in the database.
+ default_lang = get_default_lang(instance)
+
+ # If we somehow don't have a `default_lang` field, this key
+ # will be called `None`. So please remember to make a column.
+ data = {default_lang: instance.__dict__[name]}
+ else:
+ try:
+ data = json.loads(data)
+ except (TypeError, ValueError):
+ # It was invalid JSON.
+ data = {}
+
+ return data
+
+
+def has_translation(instance, name):
+ key = get_key(instance, name)
+
+ try:
+ data = redis.get(key)
+ except TransConnectionError:
+ # We can't be sure; let's assume yes until the data store is online.
+ return True
+ else:
+ try:
+ data = json.loads(data)
+ except (TypeError, ValueError):
+ # It was invalid JSON or no key exists.
+ return False
+ else:
+ # It was a non-empty dict.
+ return bool(data) and type(data) is dict
+
+
+def get_lazy_translation(instance, name):
+ """Return an object that lets us do lazy translations."""
+ return Trans(instance=instance, name=name)
+
+
+def save_translation(instance, name, value):
+ if instance.pk is None:
+ return
+
+ key = get_key(instance, name)
+ if value is None:
+ delete_translation(instance, name)
+ return
+ elif isinstance(value, basestring):
+ value = {translation_utils.get_language(): value}
+ elif isinstance(value, dict):
+ # Lowercase the locale keys.
+ value = dict((k.lower(), v) for k, v in value.iteritems())
+ else:
+ raise TransValueError
+
+ try:
+ old_value = json.loads(redis.get(key))
+ except (TypeError, ValueError, TransConnectionError):
+ pass
+ else:
+ value = dict(old_value, **value)
+
+ try:
+ redis.set(key, json.dumps(value))
+ except TransConnectionError:
+ pass
+
+
+def delete_translation(instance, name):
+ key = get_key(instance, name)
+ try:
+ return redis.delete(key)
+ except TransConnectionError:
+ pass
+
+
+def _trans_pre_save(sender, instance, **kwargs):
+ setattr(instance, '_trans_saving', True)
+
+
+def _trans_post_save(sender, instance, **kwargs):
+ delattr(instance, '_trans_saving')
+
+
+def _trans_post_delete(sender, instance, **kwargs):
+ for field_name in REGISTRY[instance.__class__]:
+ delete_translation(instance, field_name)
+
+
+def register_trans(model, fields):
+ global REGISTRY
+ REGISTRY[model] = fields
+
+ for field in fields:
+ setattr(model, field, TransDescriptor(field))
+
+ pre_save.connect(_trans_pre_save, sender=model)
+ post_save.connect(_trans_post_save, sender=model)
+ post_delete.connect(_trans_post_delete, sender=model)
+
+
+class TransDescriptor(object):
+
+ def __init__(self, name):
+ self.name = name
+
+ def __get__(self, obj, type_=None):
+ if obj:
+ key = obj.__dict__[self.name]
+ else:
+ return getattr(type_, self.name)
+
+ if not key:
+ return key
+
+ if not isinstance(key, (basestring, dict)):
+ raise TransValueError
+
+ if getattr(obj, '_trans_saving', False):
+ save_translation(obj, self.name, key)
+
+ # Save this fallback value to the database if this is the first
+ # translation.
+ if not has_translation(obj, self.name):
+ return key
+
+ current_lang = translation_utils.get_language()
+ default_lang = get_default_lang(obj)
+
+ # Replace the fallback value if we're replacing the translation
+ # for the default locale.
+ if default_lang and default_lang == current_lang:
+ if isinstance(key, basestring):
+ return key
+ elif isinstance(key, dict):
+ # Lowercase the locale keys.
+ key = dict((k.lower(), v) for k, v in key.iteritems())
+ if current_lang in key:
+ return key[current_lang]
+
+ # If there's not a `default_lang` and we did
+ #
+ # tower.activate('fr')
+ # c.name = 'New Default Translated Name'
+ # c.save()
+ #
+ # ... then assume that should be the new fallback value.
+ return key
+
+ return get_lazy_translation(obj, self.name)
+
+ def __set__(self, obj, value):
+ obj.__dict__[self.name] = value
+
+
+def get_all_translation_keys():
+ """Returns a dict of all the translation keys across all the models."""
+ data = {}
+ for model, fields in REGISTRY.items():
+ objs = model.objects.only('pk')
+ data[model._meta.db_table] = {}
+ for obj in objs:
+ data[model._meta.db_table][obj.pk] = dict(
+ (f, get_key(obj, f)) for f in fields
+ )
+ return data
+
+
+def get_all_translation_strings():
+ """Returns a dict of all the translated strings across all the models."""
+ data = {}
+ for model, fields in REGISTRY.items():
+ objs = model.objects.only('pk')
+ data[model._meta.db_table] = {}
+ for obj in objs:
+ data[model._meta.db_table][obj.pk] = {}
+ for f in fields:
+ val = getattr(obj, f)
+ if val is not None:
+ translations = val.__dict__()
+ else:
+ translations = None
+ data[model._meta.db_table][obj.pk][f] = translations
+ return data
View
8 migrations/658-change-transfields.sql
@@ -0,0 +1,8 @@
+-- * categories.name *
+
+-- TODO: Before proceeding, migrate all categories names in
+-- Translations table to L10n data store.
+
+alter table categories drop foreign key name_refs_id_e052037f;
+alter table categories drop column name;
+alter table categories add column name text;
View
22 mkt/api/resources.py
@@ -7,10 +7,12 @@
import waffle
from celery_tasktree import TaskTree
import raven.base
-from rest_framework.decorators import api_view, permission_classes
+from rest_framework.decorators import (api_view, permission_classes,
+ renderer_classes)
from rest_framework import generics
from rest_framework.mixins import ListModelMixin, RetrieveModelMixin
from rest_framework.permissions import AllowAny
+from rest_framework.renderers import UnicodeJSONRenderer
from rest_framework.response import Response
from rest_framework.serializers import (BooleanField, CharField,
ChoiceField,
@@ -31,6 +33,8 @@
from addons.models import Addon, AddonUser, Category, Webapp
from amo.decorators import write
from amo.utils import no_translation
+from apps.translations.fields_new import (get_all_translation_keys,
+ get_all_translation_strings)
from constants.applications import DEVICE_TYPES
from constants.payments import PAYMENT_METHOD_CHOICES, PROVIDER_CHOICES
from files.models import Platform
@@ -442,6 +446,22 @@ def error_reporter(request):
return Response(status=204)
+@api_view(['GET'])
+@permission_classes([AllowAny])
+@renderer_classes([UnicodeJSONRenderer])
+def l10n_keys(request):
+ request._request.CORS = ['GET', 'POST']
+ return Response(get_all_translation_keys())
+
+
+@api_view(['GET'])
+@permission_classes([AllowAny])
+@renderer_classes([UnicodeJSONRenderer])
+def l10n_strings(request):
+ request._request.CORS = ['GET', 'POST']
+ return Response(get_all_translation_strings())
+
+
class RefreshManifestViewSet(GenericViewSet, CORSMixin):
model = Webapp
permission_classes = (AllowAppOwner, AllowReviewerReadOnly)
View
11 mkt/api/urls.py
@@ -8,9 +8,10 @@
from mkt.submit.api import PreviewResource, StatusResource, ValidationResource
from mkt.api.base import AppRouter, handle_500, SlugRouter
from mkt.api.resources import (AppResource, CarrierResource, CategoryViewSet,
- ConfigResource, error_reporter,
- PriceTierViewSet, PriceCurrencyViewSet,
- RefreshManifestViewSet, RegionResource)
+ ConfigResource, error_reporter, l10n_keys,
+ l10n_strings, PriceTierViewSet,
+ PriceCurrencyViewSet, RefreshManifestViewSet,
+ RegionResource)
from mkt.collections.views import CollectionImageViewSet, CollectionViewSet
from mkt.features.views import AppFeaturesList
from mkt.ratings.resources import RatingResource
@@ -67,5 +68,7 @@
url(r'^rocketfuel/collections/', include(subcollections.urls)),
url(r'^apps/', include('mkt.versions.urls')),
url(r'^apps/features/', AppFeaturesList.as_view(),
- name='api-features-feature-list')
+ name='api-features-feature-list'),
+ url(r'^l10n/keys', l10n_keys, name='l10n-keys'),
+ url(r'^l10n/strings', l10n_strings, name='l10n-strings'),
)
View
2  mkt/webapps/models.py
@@ -1261,6 +1261,7 @@ def extract_document(cls, pk, obj=None):
'has_info_request': None,
}
d['manifest_url'] = obj.get_manifest_url()
+ # TODO: Use obj.name.__dict__.values().
d['name'] = list(set(string for _, string
in translations[obj.name_id]))
d['name_sort'] = unicode(obj.name).lower()
@@ -1334,6 +1335,7 @@ def extract_document(cls, pk, obj=None):
analyzer in amo.SEARCH_ANALYZER_PLUGINS):
continue
+ # TODO: Use obj.name.__dict__.values().
d['name_' + analyzer] = list(
set(string for locale, string in translations[obj.name_id]
if locale.lower() in languages))
Something went wrong with that request. Please try again.