deschler / django-modeltranslation

Translates Django models using a registration approach.
BSD 3-Clause "New" or "Revised" License
1.39k stars 257 forks source link

Support for languages per country (per model instance) #356

Open pv-g opened 8 years ago

pv-g commented 8 years ago

I have this special use case :

# settings.py

COUNTRY_NAMES = {
    'ch': 'Switzerland',
    'fr': 'France',
    'jp': 'Japan',
}
# settings.py

COUNTRY_LANGUAGES = {
    'ch': ('fr-ch', 'it-ch', 'de-ch', 'en-us'),
    'fr': ('fr-fr', 'en-us'),
    'jp': ('ja-jp', 'en-us'),
}

LANGUAGE_NAMES = {
    'de-ch': 'Deutsche',
    'en-us': 'English',
    'fr-ch': 'Français',
    'fr-fr': 'Français',
    'it-ch': 'Italiano',
    'ja-jp': '日本語',
}
# settings.py

LANGUAGES = LANGUAGE_NAMES.items()
class CountrySpecific(models.Model):

    country_code = models.CharField('Country', max_length=2,
        choices=settings.COUNTRY_NAMES.items(),
    )

    class Meta:
        abstract = True

In the admin site, users should be able to edit CountrySpecific models in their languages only.

With modeltranslation :

If I use it the standard way :

class Collection(CountrySpecific):
    title = models.CharField(max_length=255)

class CollectionTranslationOptions(TranslationOptions):
    fields = ('title',)

translator.register(Collection, CollectionTranslationOptions)

My Collection instances have translated fields for all possible languages, as specified in settings.LANGUAGES, and all translatable fields are visible in the admin site.

P.S. I'm running Djangae on Google's NoSQL Datastore so I would't even need all translatable fields in the database, but I guess we have no choice because modeltranslation hack the model classes.

Anyway, I need at least the admin site with each user able to edit models in the languages based on their country only.

The hacked solution I did :

I noticed modeltranslation always loop on modeltranslation.settings.AVAILABLE_LANGUAGES. So I made my own TranslationAdmin class, and I overrode all methods this way :

class MyTranslationAdmin(MethodWrappable, TranslationAdmin):

    _country_languages = None

    def _wrap_method(self, name, method, *args, **kwargs):

        if args and isinstance(args[0], HttpRequest):
            user = args[0].user
            self._country_languages = list(settings.COUNTRY_LANGUAGES[user.country_code])

        if self._country_languages:
            mt_lang_path = 'modeltranslation.settings.AVAILABLE_LANGUAGES'
            with sleuth.switch(mt_lang_path, self._country_languages):
                return method(*args, **kwargs)
        else:
            return method(*args, **kwargs)

With MethodWrappable a utility mixin I did to override/wrap class methods, and sleuth, this mocking library.

This is a brute force hack, I confess! :stuck_out_tongue: But this way, I can do what I need : Define the translated field languages based on a request obj.

Do you have a better idea? Or plan to support that in the future?

I saw you have a get_translation_field_excludes(self, exclude_languages=None) method in TranslationBaseModelAdmin with a TODO note : TODO: Currently unused?.

pv-g commented 8 years ago

OK, I had many problems with this hack, but finally came up with a better solution. So I'm sharing it here so it can be useful for others.

class MyTranslationAdmin(TranslationAdmin):
    '''
    This is a custom version of `TranslationAdmin`.
    `TranslationAdmin` create translated fields for all languages,
    but we only want the ones of the current country,
    so we take the `fieldsets` and remove the unwanted fields.
    '''

    def get_fieldsets(self, request, obj=None):

        # get fieldsets from super, containing translated fields for all languages
        fieldsets = super(PTWTranslationAdmin, self).get_fieldsets(request, obj)

        # we'll manipulate the fields for this request only,
        # so let's not change the source objects
        fieldsets = deepcopy(fieldsets)

        # we don't want the fields ending with a language code
        # that belongs to the unavailable_language_codes list
        exclude_suffixes = [
            '_%s' %ln.replace('-', '_')
            for ln in request.unavailable_language_codes
        ]

        # ok, let's find & remove the bad fields
        for fs_name, fs_def in fieldsets:
            fields = fs_def['fields']
            fields_to_remove = []
            for field in fields:
                for suffix in exclude_suffixes:
                    if field.endswith(suffix):
                        fields_to_remove.append(field)
            for field in fields_to_remove:
                fields.remove(field)
        return field sets

I think that's not too bad, but a better way to do it directly with modeltranslation would be great in the future.