blob: 552c06cebc90cbcaac0dee509f428354a58f6663 [file] [log] [blame]
#!/usr/bin/env python3
# Copyright 2018 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Minify the translation messages.
These files don't need to be readable when shipped as no one looks at them
directly, and there's a bunch of metadata that are only for translators.
"""
import glob
import json
import logging
import re
import sys
import libdot
def minify_placeholders(msg):
"""Expand placeholder values where possible.
An example input object:
{
"message": "Unable to parse destination: $DEST$",
"placeholders": {
"dest": {
"content": "$1",
"example": "invalid.destination"
}
}
}
We want to replace '$DEST$' in the message with '$1'.
For more complicated replacements, we'll leave it alone (for now).
{
"message": "Goodbye, $USER$. Come back to $OUR_SITE$ soon!",
"placeholders": {
"our_site": {
"content": "Example.com",
}
}
}
"""
assert "message" in msg
# Walk each of the placeholders.
for key, settings in list(msg.get("placeholders", {}).items()):
# Throw away the 'example' field which is meant for translators.
settings.pop("example", None)
# Require every replacement exist in the message. If it's not in there,
# then it's probably a typo, or a stale entry we want to remove.
assert (
f"${key.lower()}$" in msg["message"].lower()
), f"Missing ${key}$ (case insensitive) in: {msg['message']}"
# If the replacement is simple, inline it.
m = re.match(r"^[$][0-9]+$", settings["content"])
if m:
msg["message"] = re.sub(
rf"[$]{key}[$]", settings["content"], msg["message"], flags=re.I
)
msg["placeholders"].pop(key)
# Remove the placeholders setting if it's empty after we've processed it.
placeholders = msg.get("placeholders", {})
if not placeholders:
msg.pop("placeholders", None)
def minify(path, inplace=False):
"""Minify translation."""
with open(path, encoding="utf-8") as fp:
try:
data = json.loads(fp.read())
except ValueError as e:
logging.error("Processing %s: %s", path, e)
return False
# Strip out the metadata that only translators read.
for msg in data.values():
# Translator-aimed description of the message.
msg.pop("description", None)
# Expand the placeholders where possible.
minify_placeholders(msg)
# Throw away all whitespace.
formatted = json.dumps(
data,
ensure_ascii=False,
indent=None,
separators=(",", ":"),
sort_keys=True,
)
if inplace:
with open(path, "w", encoding="utf-8") as fp:
fp.write(formatted)
else:
sys.stdout.write(formatted + "\n")
return True
def minify_many(paths, inplace=False):
"""Minify translations."""
ret = True
for path in paths:
logging.debug("Processing %s", path)
if not minify(path, inplace):
ret = False
return ret
def get_parser():
"""Get a command line parser."""
parser = libdot.ArgumentParser(description=__doc__)
parser.add_argument(
"-i",
"--inplace",
default=False,
action="store_true",
help="Modify files inline rather than writing stdout.",
)
parser.add_argument(
"--glob",
action="store_true",
help="Whether to use glob() on input paths.",
)
parser.add_argument(
"files", nargs="+", metavar="files", help="The translations to format."
)
return parser
def main(argv):
"""The main func!"""
parser = get_parser()
opts = parser.parse_args(argv)
def expander(paths):
"""Handle recursive expansions of |paths|."""
for path in paths:
if opts.glob:
yield from glob.glob(path)
else:
yield path
return 0 if minify_many(expander(opts.files), opts.inplace) else 1
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))