Skip to content

Commit 47d24e8

Browse files
docs: add example translating Mustache templates
1 parent ff63e8f commit 47d24e8

6 files changed

Lines changed: 400 additions & 0 deletions

File tree

.gitlab-ci.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,18 @@ test:
119119
- test_report.xml
120120
when: always
121121

122+
mustache example:
123+
stage: test
124+
extends: .test
125+
script:
126+
- cd examples/mustache
127+
- pip install deepl
128+
- python . --help
129+
- python . --from en --to de "Hello {{user}}, your balance is {{{balance}}} dollars." > mustache_result.txt
130+
- cat mustache_result.txt
131+
- grep -q "{{user}}" mustache_result.txt
132+
- grep -q "{{{balance}}}" mustache_result.txt
133+
122134
# stage: publish -------------------------
123135

124136
pypi upload:

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99
### Added
10+
* Add [example script](examples/mustache) to translate Mustache templates.
1011
### Changed
1112
### Deprecated
1213
### Removed

examples/mustache/README.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Example: Translation of Mustache templates
2+
3+
An example showing how to translate [Mustache templates][mustache-docs] using
4+
XML tag-handling. Mustache templates embed tags in literal text and HTML tags;
5+
the tags refer to keys in hashes, so they should not be translated.
6+
7+
For example:
8+
9+
```
10+
Hello {{name}}. You have just won <b>{{value}} dollars</b>!
11+
```
12+
13+
could be translated into German as:
14+
15+
```
16+
Hallo {{name}}. Sie haben gerade <b>{{value}} Dollar</b> gewonnen!
17+
```
18+
19+
## Usage
20+
21+
Install the [`deepl` Python library](../../README.md).
22+
23+
Define your DeepL auth key as an environment variable `DEEPL_AUTH_KEY`.
24+
25+
```
26+
export DEEPL_AUTH_KEY=f63c02c5-f056-...
27+
```
28+
29+
Run the Mustache translator by running Python on this directory:
30+
31+
```
32+
python examples/mustache --to de "Hello {{name}}"
33+
```
34+
35+
For an explanation of the command line arguments, provide the `--help` option:
36+
37+
```
38+
python examples/mustache --help
39+
```
40+
41+
## How it works
42+
43+
This Mustache translator uses XML tag-handling to preserve the Mustache tags
44+
while using the DeepL API to translate. It also handles HTML tags embedded in
45+
the Mustache template.
46+
47+
1. The input Mustache template is parsed to separate the literal text from the
48+
Mustache tags.
49+
```
50+
Hello {{name}}. You have just won <b>{{value}} dollars</b>!
51+
^^^^^^^^ ^^^^^^^^^
52+
```
53+
2. The template is modified to replace all Mustache tags with placeholder XML
54+
tags. Unique IDs are attached to each placeholder tag to identify them in the
55+
translated XML.
56+
```
57+
Hello <m id=0 />. You have just won <b><m id=1 /> dollars</b>!
58+
^^^^^^^^^^ ^^^^^^^^^^
59+
```
60+
3. The XML template is translated using DeepL API.
61+
```
62+
Hallo <m id=0 />. Sie haben gerade <b><m id=1 /> Dollar</b> gewonnen!
63+
```
64+
4. The translated XML is parsed to identify placeholder tags and replace them
65+
with the original Mustache tags.
66+
```
67+
Hallo {{name}}. Sie haben gerade <b>{{value}} Dollar</b> gewonnen!
68+
^^^^^^^^ ^^^^^^^^^
69+
```
70+
71+
72+
[mustache-docs]: https://mustache.github.io/mustache.5.html

examples/mustache/__main__.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# Copyright 2023 DeepL SE (https://www.deepl.com)
2+
# Use of this source code is governed by an MIT
3+
# license that can be found in the LICENSE file.
4+
5+
import argparse
6+
import deepl
7+
import os
8+
9+
from mustache import translate_mustache
10+
11+
12+
env_auth_key = "DEEPL_AUTH_KEY"
13+
env_server_url = "DEEPL_SERVER_URL"
14+
15+
16+
def get_parser(prog_name):
17+
"""Constructs and returns the argument parser."""
18+
parser = argparse.ArgumentParser(
19+
prog=prog_name,
20+
description="Translate Mustache templates using the DeepL API "
21+
"(https://www.deepl.com/docs-api).",
22+
epilog="If you encounter issues while using this example, please "
23+
"report them at https://github.com/DeepLcom/deepl-python/issues",
24+
)
25+
26+
parser.add_argument(
27+
"--auth-key",
28+
default=None,
29+
help="authentication key as given in your DeepL account; the "
30+
f"{env_auth_key} environment variable is used as secondary fallback",
31+
)
32+
parser.add_argument(
33+
"--server-url",
34+
default=None,
35+
metavar="URL",
36+
help=f"alternative server URL for testing; the {env_server_url} "
37+
f"environment variable may be used as secondary fallback",
38+
)
39+
parser.add_argument(
40+
"--to",
41+
"--target-lang",
42+
dest="target_lang",
43+
required=True,
44+
help="language into which the template should be translated",
45+
)
46+
parser.add_argument(
47+
"--from",
48+
"--source-lang",
49+
dest="source_lang",
50+
help="language of the template to be translated",
51+
)
52+
parser.add_argument(
53+
"template",
54+
nargs="+",
55+
type=str,
56+
help="template to be translated. Wrap template in quotes to prevent "
57+
"the shell from splitting on whitespace.",
58+
)
59+
60+
return parser
61+
62+
63+
def main():
64+
# Create a parser, reusing most of the arguments from the main CLI
65+
parser = get_parser(prog_name=None)
66+
args = parser.parse_args()
67+
auth_key = args.auth_key or os.getenv(env_auth_key)
68+
server_url = args.server_url or os.getenv(env_server_url)
69+
if auth_key is None:
70+
raise Exception(
71+
f"Please provide authentication key via the {env_auth_key} "
72+
"environment variable or --auth_key argument"
73+
)
74+
75+
# Create a Translator object, and call get_usage() to validate connection
76+
translator = deepl.Translator(auth_key, server_url=server_url)
77+
translator.get_usage()
78+
79+
for template in args.template:
80+
# Call translate_mustache() to translate the Mustache template
81+
output = translate_mustache(
82+
template,
83+
translator=translator,
84+
source_lang=args.source_lang,
85+
target_lang=args.target_lang,
86+
)
87+
print(output)
88+
89+
90+
if __name__ == "__main__":
91+
main()

examples/mustache/html_parsing.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# Copyright 2023 DeepL SE (https://www.deepl.com)
2+
# Use of this source code is governed by an MIT
3+
# license that can be found in the LICENSE file.
4+
5+
from html.parser import HTMLParser
6+
import html
7+
from typing import List, Tuple, Dict
8+
9+
10+
class PassthroughHTMLParser(HTMLParser):
11+
"""
12+
An HTML parser that accumulates parsed HTML in the original HTML.
13+
The original HTML can be accessed using parsed().
14+
15+
Note: some HTML features are not correctly handled and reproduced: entity
16+
references, character references, declarations, processing instructions.
17+
"""
18+
19+
def __init__(self):
20+
super().__init__(convert_charrefs=False)
21+
self._result = ""
22+
23+
def parsed(self):
24+
return self._result
25+
26+
def _append(self, text):
27+
self._result += text
28+
29+
def handle_startendtag(self, tag, attrs):
30+
self._append(f"<{tag}{self._encode_attrs(attrs)} />")
31+
32+
def handle_starttag(self, tag, attrs):
33+
self._append(f"<{tag}{self._encode_attrs(attrs)}>")
34+
35+
def handle_endtag(self, tag):
36+
self._append(f"</{tag}>")
37+
38+
def handle_charref(self, name):
39+
self._append(f"&#{name};")
40+
41+
def handle_entityref(self, name):
42+
self._append(f"&{name};")
43+
44+
def handle_data(self, data):
45+
self._append(data)
46+
47+
def handle_comment(self, data):
48+
self._append(f"<!--{data}-->")
49+
50+
def handle_decl(self, decl):
51+
self._append(f"<!{decl}>")
52+
53+
def handle_pi(self, data):
54+
self._append(f"<?{data}>")
55+
56+
@staticmethod
57+
def _encode_attrs(attrs: List[Tuple[str, str]]) -> str:
58+
if attrs:
59+
return " " + " ".join(
60+
f'{key}="{html.escape(value)}"' for key, value in attrs
61+
)
62+
else:
63+
return ""
64+
65+
66+
class TagReplacerHTMLParser(PassthroughHTMLParser):
67+
"""
68+
An HTML parser that accumulates parsed HTML unmodified, except that
69+
matching start-end tags are replaced by prepared content.
70+
71+
:param replace_tags: A dictionary of tags to replace, with keys matching
72+
tag names ("tag") or tag names with IDs ("tag#id"), and values the text
73+
to replace matching tags with.
74+
"""
75+
76+
def __init__(self, replace_tags: Dict[str, str]):
77+
super().__init__()
78+
self._replace_tags = replace_tags
79+
80+
def handle_startendtag(self, tag, attrs):
81+
if self._replace_tag_if_matching(tag):
82+
return
83+
84+
id = next((value for key, value in attrs if key == "id"), None)
85+
if id is not None and self._replace_tag_if_matching(f"{tag}#{id}"):
86+
return
87+
88+
# If no replacement is found, fall back to passthrough parser
89+
super().handle_starttag(tag, attrs)
90+
91+
def _replace_tag_if_matching(self, tag_and_id):
92+
if tag_and_id in self._replace_tags:
93+
stored_tag = self._replace_tags[tag_and_id]
94+
self._append(stored_tag)
95+
return True
96+
return False

0 commit comments

Comments
 (0)