Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pierremonico/1190ab45da52ab59a3e403bd8c97772d to your computer and use it in GitHub Desktop.
Save pierremonico/1190ab45da52ab59a3e403bd8c97772d to your computer and use it in GitHub Desktop.
Localize Vue files with Django `makemessages`

NB: the whole approach of serving a Vue project through Django is less than ideal, so this can be seen as deprecated.

Intro

The goal of this gist is to be able to use the built-in makemessages command of Django on your Vue frontend. You can then expose a JavaScript catalog and localize your frontend using gettext.

Following assumptions are made:

  • The directory for the frontend lives in your Django project
  • You are using components (.vue files)

Include the JS Catalog view

Somewhere in urls.py:


from django.views.i18n import JavaScriptCatalog
urlspatterns += [
    path('jsi18n/', JavaScriptCatalog.as_view(), {
       'domain': 'djangojs',
       'packages': None,
    }
 ]

Since 'djangojs' is the default here, you can just do:

urlspatterns += [path('jsi18n/', JavaScriptCatalog.as_view()]

The catalog and the gettext utility are now available in the JS file served at that URL.

Load the catalog

You have to load the catalog synchronously, otherwise your frontend will try to access functions that are undefined.

In index.html (or the root file in which you load your bundled frontend):

<script src="/api/v1/jsi18n"></script>

Assign gettext to Vue prototype

In index.js(or your main frontend app file):

Vue.prototype._ = window.gettext  // choose anything you like, _ is a common option

You can now use _('La vie est belle') in your frontend.

Override the makemessages command

The makemessages command is based on the GNU xgettext tool. Even though it works with JS files, it will not work properly with .vue files (especially in the <template> part. That's why you need to use another tool for that, namely gettext-vue.

Install gettext-vue: npm install -g xgettext-template gettext-vue

Create a file for overriding the command: mkdir someapplication/management/commands/makemessages.py

Copy paste the file contents of the makemessages.py file of this gist. The important override is this part:

if self.domain == 'djangojs' and ('.vue' in self.extensions):
    args = [
        'yarn',
        '--silent',
        'xgettext-template',
        '-L',
        'Vue',
        '--keyword=_',
        '--keyword=gettext',
        '--keyword=$gettext',
        '--output=-'
    ]

Make sure you replace the --keyword arguments by whatever function name you use in your templates.

Use the localization function

In your .vue components, all of the following will now work:

<template>
    <SomeComponent :label="_('Traduisez moi`)"/>
    <a>{{ _('Le lien vers le paradis') }}</a>
</template>

<script>
    export default {
        computed: {
            title() {
                return this._('Une belle application')
            }
        }
    }
</script>

Collect and compile messages as usual

You can use the standard commands to collect messages. node_modules will be ignored automatically if you use the file provided in this gist.

./manage.py makemessages -d djangojs -e vue
./manage.py compilemessages

The djangojs .po and .mo files will now be available as per your settings.

import os
import re
from django.core.files.temp import NamedTemporaryFile
from django.core.management.base import CommandError
from django.core.management.commands import makemessages
from django.core.management.commands.makemessages import write_pot_file
from django.core.management.utils import popen_wrapper
plural_forms_re = re.compile(r'^(?P<value>"Plural-Forms.+?\\n")\s*$', re.MULTILINE | re.DOTALL)
STATUS_OK = 0
NO_LOCALE_DIR = object()
class Command(makemessages.Command):
def handle(self, *args, **options):
if options.get('domain') == 'djangojs':
if 'vue' in options.get('extensions'):
options['ignore_patterns'] += ['node_modules']
super().handle(*args, **options)
def process_locale_dir(self, locale_dir, files): # noqa
"""
Extract translatable literals from the specified files, creating or
updating the POT file for a given locale directory.
Use the xgettext GNU gettext utility.
"""
build_files = []
for translatable in files:
if self.verbosity > 1:
self.stdout.write('processing file %s in %s\n' % (
translatable.file, translatable.dirpath
))
if self.domain not in ('djangojs', 'django'):
continue
build_file = self.build_file_class(self, self.domain, translatable)
try:
build_file.preprocess()
except UnicodeDecodeError as e:
self.stdout.write(
'UnicodeDecodeError: skipped file %s in %s (reason: %s)' % (
translatable.file, translatable.dirpath, e,
)
)
continue
build_files.append(build_file)
if self.domain == 'djangojs' and ('.vue' in self.extensions):
args = [
'yarn',
'--silent',
'xgettext-template',
'-L',
'Vue',
'--keyword=_',
'--keyword=gettext',
'--keyword=$gettext',
'--output=-'
]
elif self.domain == 'djangojs':
is_templatized = build_file.is_templatized
args = [
'xgettext',
'-d', self.domain,
'--language=%s' % ('C' if is_templatized else 'JavaScript',),
'--keyword=gettext_noop',
'--keyword=gettext_lazy',
'--keyword=ngettext_lazy:1,2',
'--keyword=pgettext:1c,2',
'--keyword=npgettext:1c,2,3',
'--output=-',
]
elif self.domain == 'django':
args = [
'xgettext',
'-d', self.domain,
'--language=Python',
'--keyword=gettext_noop',
'--keyword=gettext_lazy',
'--keyword=ngettext_lazy:1,2',
'--keyword=ugettext_noop',
'--keyword=ugettext_lazy',
'--keyword=ungettext_lazy:1,2',
'--keyword=pgettext:1c,2',
'--keyword=npgettext:1c,2,3',
'--keyword=pgettext_lazy:1c,2',
'--keyword=npgettext_lazy:1c,2,3',
'--output=-',
]
else:
return
input_files = [bf.work_path for bf in build_files]
with NamedTemporaryFile(mode='w+') as input_files_list:
input_files_list.write(('\n'.join(input_files)))
input_files_list.flush()
args.extend(['--files-from', input_files_list.name])
args.extend(self.xgettext_options)
msgs, errors, status = popen_wrapper(args)
if errors:
if status != STATUS_OK:
for build_file in build_files:
build_file.cleanup()
raise CommandError(
'errors happened while running xgettext on %s\n%s' %
('\n'.join(input_files), errors)
)
elif self.verbosity > 0:
# Print warnings
self.stdout.write(errors)
if msgs:
if locale_dir is NO_LOCALE_DIR:
file_path = os.path.normpath(build_files[0].path)
raise CommandError(
'Unable to find a locale path to store translations for '
'file %s' % file_path
)
for build_file in build_files:
msgs = build_file.postprocess_messages(msgs)
potfile = os.path.join(locale_dir, '%s.pot' % self.domain)
write_pot_file(potfile, msgs)
for build_file in build_files:
build_file.cleanup()
@pySilver
Copy link

Sadly it fails with:

# ./manage.py makemessages -d djangojs -e vue
CommandError: errors happened while running xgettext on ./assets/js/widgets/ProductModal.vue
./assets/js/widgets/ProductWidget.vue
xgettext-template: language 'Vue' is not supported

Try 'xgettext-template --help' for more information.

@pierremonico
Copy link
Author

Hi, thanks for reaching out.
Did you install gettext-vue like specified here?

@pySilver
Copy link

@pierremonico yeah but somehow it's not recognized. I've ended up with vue-i18n which is more active (and easy to integrate with django)

@pierremonico
Copy link
Author

@pierremonico yeah but somehow it's not recognized. I've ended up with vue-i18n which is more active (and easy to integrate with django)

That's your best option. To be honest I created this gist while still managing my Vue project in the same repo as my Django project, and serving the bundle entry point through a Django template, which is far from ideal :)
Will mark this as deprecated.

@risha700
Copy link

Nice lines, it might be a good fit for some small projects.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment