Hacking Django’s makemessages for better (translations) matching in React JSX components
Have you ever found a missing translation when running Django
makemessages on React JSX components? I did and it can happen as far as you don’t keep your
gettext tags in the beginning of JSX modules.
That’s fine if you are starting a new React project now — you keep all your
gettext() calls into variables in the beginning of the JSX components making the life easier for
makemessages script but maybe not as nice for other people to read the code — but hard to catch up if there are already hundreds of modules with
gettext() in place.
A solution would be writing a script to edit those JSX modules by picking all
gettext() calls, store them into a variable in the beginning of the module and invoke the variable where the
gettext() was being called before. But this is very prone to errors because people are not consistent regarding text formatting, variables interpolation, etc.
Facing this problem I decided to dig into Django’s
makemessages and understand how the script works. In the end,
makemessages is just a wrapper with some file handling making use of GNU’s library
Knowing that, I decided to play a bit with
xgettext myself passing different inputs. I ran
xgettext directly passing the problematic JSX module and
gettext() tags were picked up and shown in the output. In a desperate act I tried it with
--language=Python and… yeah, all
gettext() text was found.
Here is the output of passing
And here is the output for
Huge difference, isn’t it? The input file is exactly the same.
What do you do next after these findings? You go back to
makemessages script and try to figure out how to pass the language to the script. Slow down cowboy, you can’t do that.
The magic is all in
if domain == 'djangojs':
is_templatized = command.gettext_version < (0, 18, 3)
args = [
] + command.xgettext_options
elif domain == 'django':
args = [
] + command.xgettext_options
makemessages builds the argument list to be passed to
xgettext and in the end
xgettext_options is appended to the command. Also, as shown above, when
djangojs the language passed to
xgettext_options has inside is:
xgettext_options = ['--from-code=UTF-8', '--add-comments=Translators']
# and potentially
self.xgettext_options = self.xgettext_options[:] + ['--no-wrap']
self.xgettext_options = self.xgettext_options[:] + ['--no-location']
That’s it! I mean, since it is a class attribute and then a copy is done to build an instance attribute and add more options, we can hack it to allow the user to pass
--language=<whatever> to it.
I end up creating a new hacking Django’s management command in order to pass the language provided as input to
xgettext. It is not beauty because it takes advantage of arguments’ order passed to
xgettext but it does the job.
xgettext will be called with two
--language parameter and the last one will overrule the first one. A better way would be improving
makemessages directly but not now (maybe in the next steps).
Here it is:
from django.core.management.commands.makemessages import Command as MMCommand
This is a wrapper for the makemessages command and
it is used to force makemessages call xgettext with the language
provided as input
The solution is really hacky and takes advantage of the fact
that in makemessages TranslatableFile process()
the options in command.xgettext_options are appended to the end
of the xgettext command.
def add_arguments(self, parser):
help=’Language to be used by xgettext’
def handle(self, *args, **options):
language = options.get(‘language’)
super(Command, self).handle(*args, **options)
Now it is possible to pass the
makemessages which will pass it through to
xgettext. Just type (
--language Python is not really needed here because
Python` is the default value):
python manage.py makemessages_djangojs --domain djangojs --extension jsx --language Python