Skip to content

Commit 95ec921

Browse files
committed
makemessages: work with xgettext 0.24
1 parent e450fc5 commit 95ec921

File tree

2 files changed

+98
-105
lines changed

2 files changed

+98
-105
lines changed

README.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,67 @@ const cssUrl = staticUrl('/css/document.css')
9898
```
9999

100100
Note that you will need to use absolute paths starting from the `STATIC_ROOT` for the `staticUrl()` function. Different from the default `ManifestStaticFilesStorage`, our version will generally interprete file urls starting with a slash as being relative to the `STATIC_ROOT`.
101+
102+
Translations
103+
------------
104+
105+
Commands such as `./manage.py makemessages` and `./manage.py compilemessages` will work as always in Django, with some slightly different defaults. Not specifying any language will default to running with `--all` (all languages). Not specifying any domain will default to running for both "django" and "djangojs" (Python and Javascript files). The `static-transpile` directory will also be ignored by default.
106+
107+
**NOTE: JavaScript files that contain template strings will require at least xgettext version 0.24 or higher. See below for installation instructions.**
108+
109+
110+
Install xgettext 0.24
111+
---------------------
112+
113+
First check which xgettext version your OS comes with:
114+
115+
```bash
116+
xgettext --version
117+
```
118+
119+
If it is below version 0.24, you will need to install a newer version. For example in your current virtual environment:
120+
121+
Step 1: Activate Your Virtual Environment
122+
```bash
123+
source /path/to/venv/bin/activate
124+
```
125+
126+
Step 2: Install Build Dependencies
127+
Install tools required to compile software:
128+
129+
```bash
130+
sudo apt-get update
131+
sudo apt-get install -y build-essential libtool automake autoconf
132+
```
133+
134+
Step 3: Download and Extract Gettext 0.24
135+
136+
```bash
137+
wget https://ftp.gnu.org/pub/gnu/gettext/gettext-0.24.tar.gz
138+
tar -xzf gettext-0.24.tar.gz
139+
cd gettext-0.24
140+
```
141+
142+
Step 4: Configure and Install into the Venv
143+
Install to your venv's directory using --prefix:
144+
145+
```bash
146+
./configure --prefix=$VIRTUAL_ENV
147+
make
148+
make install
149+
```
150+
151+
Step 5: Verify Installation
152+
Ensure the new xgettext is in your venv and check the version:
153+
154+
```bash
155+
which xgettext # Should output a path inside your venv
156+
xgettext --version # Should show 0.24
157+
```
158+
159+
Step 6: Cleanup
160+
161+
```bash
162+
cd ..
163+
rm -rf gettext-0.24 gettext-0.24.tar.gz
164+
```
Lines changed: 34 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -1,109 +1,34 @@
1-
import os
2-
import re
3-
import shutil
4-
import tempfile
5-
61
from base.management import BaseCommand
72
from django.core.management.commands import makemessages
8-
from django.utils.functional import cached_property
9-
from django.utils.jslex import JsLexer
10-
from django.utils.jslex import Tok
11-
from django.utils.translation import templatize
12-
13-
JsLexer.both_before.append(
14-
Tok("template", r"`([^\\]|(\\(.|\n)))*?`", next="div"),
15-
)
163

174
# This makes makemessages create both translations for Python and JavaScript
185
# code in one go.
19-
#
20-
# Additionally, it works around xgettext not being able to work with ES2015
21-
# template strings [1] by transpiling the JavaScript files in a special way to
22-
# replace all template strings.
23-
#
24-
# [1] https://savannah.gnu.org/bugs/?50920
25-
26-
27-
def prepare_js_for_gettext(js):
28-
"""
29-
Convert the JavaScript source `js` into something resembling C for
30-
xgettext.
31-
What actually happens is that all the regex literals are replaced with
32-
"REGEX".
33-
"""
34-
35-
def escape_quotes(m):
36-
"""Used in a regex to properly escape double quotes."""
37-
s = m[0]
38-
if s == '"':
39-
return r"\""
40-
else:
41-
return s
42-
43-
lexer = JsLexer()
44-
c = []
45-
for name, tok in lexer.lex(js):
46-
if name == "regex":
47-
# C doesn't grok regexes, and they aren't needed for gettext,
48-
# so just output a string instead.
49-
tok = '"REGEX"'
50-
elif name == "string":
51-
# C doesn't have single-quoted strings, so make all strings
52-
# double-quoted.
53-
if tok.startswith("'"):
54-
guts = re.sub(r"\\.|.", escape_quotes, tok[1:-1])
55-
tok = '"' + guts + '"'
56-
elif name == "template":
57-
# Split the template string into chunks using the ${...} pattern
58-
chunks = re.findall(r"\${(.*?)}", tok)
59-
# Concatenate the chunks with "+"
60-
for chunk in chunks:
61-
tok = tok.replace("${" + chunk + "}", '" + ' + chunk + ' + "', 1)
62-
# Replace backtick-quotes with double-quotes.
63-
tok = '"' + tok[1:-1] + '"'
64-
elif name == "id":
65-
# C can't deal with Unicode escapes in identifiers. We don't
66-
# need them for gettext anyway, so replace them with something
67-
# innocuous
68-
tok = tok.replace("\\", "U")
69-
c.append(tok)
70-
return "".join(c)
716

72-
73-
class BuildFile(makemessages.BuildFile):
74-
@cached_property
75-
def is_templatized(self):
76-
if self.domain == "djangojs":
77-
return True
78-
elif self.domain == "django":
79-
file_ext = os.path.splitext(self.translatable.file)[1]
80-
return file_ext != ".py"
81-
return False
82-
83-
def preprocess(self):
84-
"""
85-
Preprocess (if necessary) a translatable file before passing it to
86-
xgettext GNU gettext utility.
87-
"""
88-
if not self.is_templatized:
89-
return
90-
with open(self.path, encoding="utf-8") as fp:
91-
src_data = fp.read()
92-
93-
if self.domain == "djangojs":
94-
content = prepare_js_for_gettext(src_data)
95-
elif self.domain == "django":
96-
content = templatize(src_data, origin=self.path[2:])
97-
98-
with open(self.work_path, "w", encoding="utf-8") as fp:
99-
fp.write(content)
100-
101-
def cleanup(self):
102-
pass
7+
# Note that translating JavaScript files with template strings requires xgettext 0.24 (see README.md))
1038

1049

10510
class Command(makemessages.Command, BaseCommand):
106-
build_file_class = BuildFile
11+
12+
def add_arguments(self, parser):
13+
# Call the parent class's add_arguments first
14+
super().add_arguments(parser)
15+
16+
# Modify the domain argument's help text
17+
for action in parser._actions:
18+
if action.dest == "domain":
19+
action.help = (
20+
"Domain of the messages file ('django' or 'djangojs'). "
21+
"By default, both domains will be processed."
22+
)
23+
action.default = None # This indicates we'll handle both domains
24+
elif action.dest == "locale":
25+
action.help = "Locale(s) to process. If none are specified, all locales will be processed."
26+
elif action.dest == "all":
27+
action.help = (
28+
"Process all locales. "
29+
"If not specified, and no locales are provided, all locales will be processed."
30+
)
31+
action.default = False
10732

10833
def handle(self, *args, **options):
10934
options["ignore_patterns"] += [
@@ -112,11 +37,15 @@ def handle(self, *args, **options):
11237
"node_modules",
11338
"static-transpile",
11439
]
115-
options["domain"] = "django"
116-
super().handle(*args, **options)
117-
options["domain"] = "djangojs"
118-
self.temp_dir_out = tempfile.mkdtemp()
119-
self.temp_dir_in = tempfile.mkdtemp()
120-
super().handle(*args, **options)
121-
shutil.rmtree(self.temp_dir_in)
122-
shutil.rmtree(self.temp_dir_out)
40+
if len(options["locale"]) == 0 and not options["all"]:
41+
options["all"] = True
42+
if options["domain"]:
43+
self.stdout.write("Domain %s" % options["domain"])
44+
return super().handle(*args, **options)
45+
else:
46+
options["domain"] = "django"
47+
self.stdout.write("Domain %s" % options["domain"])
48+
super().handle(*args, **options)
49+
options["domain"] = "djangojs"
50+
self.stdout.write("Domain %s" % options["domain"])
51+
super().handle(*args, **options)

0 commit comments

Comments
 (0)