blob: 3f25bfd0d3828524b9e56376a128daac95340729 [file] [log] [blame]
Yu-Ping Wud71b4452020-06-16 11:00:26 +08001#!/usr/bin/env python
Hung-Te Lin707e2ef2013-08-06 10:20:04 +08002# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5"""Build localized text resources by extracting firmware localization strings
6 and convert into TXT and PNG files into stage folder.
7
8Usage:
9 ./build.py <locale-list>
10"""
11
Yu-Ping Wuc90a22f2020-04-24 11:17:15 +080012import signal
Yu-Ping Wu6b282c52020-03-19 12:54:15 +080013import enum
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080014import glob
Jes Klinke1687a992020-06-16 13:47:17 -070015import json
Hung-Te Lin04addcc2015-03-23 18:43:30 +080016import multiprocessing
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080017import os
18import re
Jes Klinke1687a992020-06-16 13:47:17 -070019import shutil
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080020import subprocess
21import sys
Jes Klinke1687a992020-06-16 13:47:17 -070022import tempfile
Hung-Te Lin04addcc2015-03-23 18:43:30 +080023
Hung-Te Lindf738512018-09-14 08:39:27 +080024from PIL import Image
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080025import yaml
26
27SCRIPT_BASE = os.path.dirname(os.path.abspath(__file__))
Yu-Ping Wu10cf2892020-08-10 17:20:11 +080028DEFAULT_NAME = '_DEFAULT_'
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080029KEY_LOCALES = 'locales'
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080030KEY_FILES = 'files'
31KEY_FONTS = 'fonts'
32KEY_STYLES = 'styles'
Matt Delco4c5580d2019-03-07 14:00:28 -080033DIAGNOSTIC_FILES = 'diagnostic_files'
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080034
Jes Klinke1687a992020-06-16 13:47:17 -070035STRINGS_GRD_FILE = 'firmware_strings.grd'
36STRINGS_JSON_FILE_TMPL = '{}.json'
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080037FORMAT_FILE = 'format.yaml'
Yu-Ping Wu8f633b82020-09-22 14:27:57 +080038TXT_TO_PNG_SVG = os.path.join(SCRIPT_BASE, 'text_to_png_svg')
39LOCALE_DIR = os.path.join(SCRIPT_BASE, 'strings', 'locale')
Yu-Ping Wuae79af62020-09-23 16:48:06 +080040STAGE_DIR = os.path.join(os.getenv('OUTPUT',
41 os.path.join(SCRIPT_BASE, 'build')),
42 '.stage')
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080043
Jes Klinke1687a992020-06-16 13:47:17 -070044# Regular expressions used to eliminate spurious spaces and newlines in
45# translation strings.
46NEWLINE_PATTERN = re.compile(r'([^\n])\n([^\n])')
47NEWLINE_REPLACEMENT = r'\1 \2'
48CRLF_PATTERN = re.compile(r'\r\n')
49MULTIBLANK_PATTERN = re.compile(r' *')
50
Yu-Ping Wu6b282c52020-03-19 12:54:15 +080051
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080052class DataError(Exception):
53 pass
54
55
Yu-Ping Wuae79af62020-09-23 16:48:06 +080056def _load_locale_json_file(locale, json_dir):
Jes Klinke1687a992020-06-16 13:47:17 -070057 result = {}
Yu-Ping Wuae79af62020-09-23 16:48:06 +080058 filename = os.path.join(json_dir, STRINGS_JSON_FILE_TMPL.format(locale))
Yu-Ping Wud71b4452020-06-16 11:00:26 +080059 with open(filename, encoding='utf-8-sig') as input_file:
Jes Klinke1687a992020-06-16 13:47:17 -070060 for tag, msgdict in json.load(input_file).items():
61 msgtext = msgdict['message']
62 msgtext = re.sub(CRLF_PATTERN, '\n', msgtext)
63 msgtext = re.sub(NEWLINE_PATTERN, NEWLINE_REPLACEMENT, msgtext)
64 msgtext = re.sub(MULTIBLANK_PATTERN, ' ', msgtext)
65 # Strip any trailing whitespace. A trailing newline appears to make
66 # Pango report a larger layout size than what's actually visible.
67 msgtext = msgtext.strip()
68 result[tag] = msgtext
69 return result
70
Yu-Ping Wuae79af62020-09-23 16:48:06 +080071
72def parse_locale_json_file(locale, json_dir):
73 """Parses given firmware string json file for build_text_files.
Mathew King89d48c62019-02-15 10:08:39 -070074
75 Args:
Yu-Ping Wu8f633b82020-09-22 14:27:57 +080076 locale: The name of the locale, e.g. "da" or "pt-BR".
Jes Klinke1687a992020-06-16 13:47:17 -070077 json_dir: Directory containing json output from grit.
Mathew King89d48c62019-02-15 10:08:39 -070078
79 Returns:
80 A dictionary for mapping of "name to content" for files to be generated.
81 """
Yu-Ping Wuae79af62020-09-23 16:48:06 +080082 result = _load_locale_json_file(locale, json_dir)
83 original = _load_locale_json_file('en', json_dir)
84 for tag in original:
85 if tag not in result:
86 # Use original English text, in case translation is not yet available
87 print('WARNING: locale "%s", missing entry %s' % (locale, tag))
88 result[tag] = original[tag]
Mathew King89d48c62019-02-15 10:08:39 -070089
Yu-Ping Wuae79af62020-09-23 16:48:06 +080090 return result
Mathew King89d48c62019-02-15 10:08:39 -070091
Yu-Ping Wuae79af62020-09-23 16:48:06 +080092
93def parse_locale_input_files(locale, json_dir):
94 """Parses all firmware string files for the given locale.
95
96 Args:
97 locale: The name of the locale, e.g. "da" or "pt-BR".
98 json_dir: Directory containing json output from grit.
99
100 Returns:
101 A dictionary for mapping of "name to content" for files to be generated.
102 """
103 result = parse_locale_json_file(locale, json_dir)
104
105 # Walk locale directory to add pre-generated texts such as language names.
Yu-Ping Wu8f633b82020-09-22 14:27:57 +0800106 for input_file in glob.glob(os.path.join(LOCALE_DIR, locale, "*.txt")):
Mathew King89d48c62019-02-15 10:08:39 -0700107 name, _ = os.path.splitext(os.path.basename(input_file))
Yu-Ping Wud71b4452020-06-16 11:00:26 +0800108 with open(input_file, 'r', encoding='utf-8-sig') as f:
Mathew King89d48c62019-02-15 10:08:39 -0700109 result[name] = f.read().strip()
Shelley Chen2f616ac2017-05-22 13:19:40 -0700110
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800111 return result
112
113
Yu-Ping Wuae79af62020-09-23 16:48:06 +0800114def create_file(file_name, contents, output_dir):
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800115 """Creates a text file in output directory by given contents.
116
117 Args:
118 file_name: Output file name without extension.
119 contents: A list of strings for file content.
120 output_dir: The directory to store output file.
121 """
122 output_name = os.path.join(output_dir, file_name + '.txt')
Yu-Ping Wud71b4452020-06-16 11:00:26 +0800123 with open(output_name, 'w', encoding='utf-8-sig') as f:
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800124 f.write('\n'.join(contents) + '\n')
125
126
Yu-Ping Wuae79af62020-09-23 16:48:06 +0800127def build_text_files(inputs, files, output_dir):
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800128 """Builds text files from given input data.
129
130 Args:
131 inputs: Dictionary of contents for given file name.
132 files: List of file records: [name, content].
133 output_dir: Directory to generate text files.
134 """
Yu-Ping Wud71b4452020-06-16 11:00:26 +0800135 for file_name, file_content in files.items():
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800136 if file_content is None:
Yu-Ping Wuae79af62020-09-23 16:48:06 +0800137 create_file(file_name, [inputs[file_name]], output_dir)
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800138 else:
139 contents = []
140 for data in file_content:
Yu-Ping Wuae79af62020-09-23 16:48:06 +0800141 contents.append(inputs[data])
142 create_file(file_name, contents, output_dir)
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800143
144
Yu-Ping Wuae79af62020-09-23 16:48:06 +0800145def convert_text_to_png(locale, file_name, styles, fonts, output_dir):
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800146 """Converts text files into PNG image files.
147
148 Args:
149 locale: Locale (language) to select implicit rendering options.
Hung-Te Lin04addcc2015-03-23 18:43:30 +0800150 file_name: String of input file name to generate.
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800151 styles: Dictionary to get associated per-file style options.
Yu-Ping Wu10cf2892020-08-10 17:20:11 +0800152 fonts: Dictionary to get associated per-file font options. The value at
153 DEFAULT_NAME is used when |locale| is not in the dict, and the '--font'
154 option is omitted when neither exist.
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800155 output_dir: Directory to generate image files.
156 """
Hung-Te Lin04addcc2015-03-23 18:43:30 +0800157 input_file = os.path.join(output_dir, file_name + '.txt')
158 command = [TXT_TO_PNG_SVG, "--lan=%s" % locale, "--outdir=%s" % output_dir]
159 if file_name in styles:
160 command.append(styles[file_name])
Yu-Ping Wu10cf2892020-08-10 17:20:11 +0800161 default_font = fonts.get(DEFAULT_NAME)
162 font = fonts.get(locale, default_font)
163 if font:
164 command.append("--font='%s'" % font)
Hung-Te Lin04addcc2015-03-23 18:43:30 +0800165 font_size = os.getenv("FONTSIZE")
166 if font_size is not None:
167 command.append('--point=%r' % font_size)
Yu-Ping Wu51940352020-09-17 08:48:55 +0800168 command.append('--margin="0 0"')
169 # TODO(b/159399377): Set different widths for titles and descriptions.
170 # Currently only wrap lines for descriptions.
171 if '_desc' in file_name:
172 # Without the --width option set, the minimum height of the output SVG
173 # image is roughly 22px (for locale 'en'). With --width=WIDTH passed to
174 # pango-view, the width of the output seems to always be (WIDTH * 4 / 3),
175 # regardless of the font being used. Therefore, set the max_width in
176 # points as follows to prevent drawing from exceeding canvas boundary in
177 # depthcharge runtime.
178 # Some of the numbers below are from depthcharge:
179 # - 1000: UI_SCALE
180 # - 50: UI_MARGIN_H
181 # - 228: UI_REC_QR_SIZE
182 # - 24: UI_REC_QR_MARGIN_H
183 # - 24: UI_DESC_TEXT_HEIGHT
184 if file_name == 'rec_phone_step2_desc':
185 max_width = 1000 - 50 * 2 - 228 - 24 * 2
186 else:
187 max_width = 1000 - 50 * 2
188 max_width_pt = int(22 * max_width / 24 / (4 / 3))
189 command.append('--width=%d' % max_width_pt)
Hung-Te Lin04addcc2015-03-23 18:43:30 +0800190 command.append(input_file)
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800191
Hung-Te Lin04addcc2015-03-23 18:43:30 +0800192 if subprocess.call(' '.join(command), shell=True,
193 stdout=subprocess.PIPE) != 0:
194 return False
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800195
Hung-Te Lin04addcc2015-03-23 18:43:30 +0800196 # Check output file size
197 output_file = os.path.join(output_dir, file_name + '.png')
Hung-Te Lin04addcc2015-03-23 18:43:30 +0800198
199 return True
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800200
201def main(argv):
Yu-Ping Wu51940352020-09-17 08:48:55 +0800202 with open(FORMAT_FILE, encoding='utf-8') as f:
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800203 formats = yaml.load(f)
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800204
Yu-Ping Wu51940352020-09-17 08:48:55 +0800205 # Sources are one .grd file with identifiers chosen by engineers and
206 # corresponding English texts, as well as a set of .xlt files (one for each
207 # language other than US english) with a mapping from hash to translation.
208 # Because the keys in the xlt files are a hash of the English source text,
209 # rather than our identifiers, such as "btn_cancel", we use the "grit"
210 # command line tool to process the .grd and .xlt files, producing a set of
211 # .json files mapping our identifier to the translated string, one for every
212 # language including US English.
Jes Klinke1687a992020-06-16 13:47:17 -0700213
Yu-Ping Wu51940352020-09-17 08:48:55 +0800214 # Create a temporary directory to place the translation output from grit in.
215 json_dir = tempfile.mkdtemp()
216 # This invokes the grit build command to generate JSON files from the XTB
217 # files containing translations. The results are placed in `json_dir` as
218 # specified in firmware_strings.grd, i.e. one JSON file per locale.
219 subprocess.check_call([
220 'grit',
Yu-Ping Wu8f633b82020-09-22 14:27:57 +0800221 '-i', os.path.join(LOCALE_DIR, STRINGS_GRD_FILE),
Yu-Ping Wu51940352020-09-17 08:48:55 +0800222 'build',
223 '-o', os.path.join(json_dir)
224 ])
Jes Klinke1687a992020-06-16 13:47:17 -0700225
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800226 # Decide locales to build.
227 if len(argv) > 0:
228 locales = argv
229 else:
230 locales = os.getenv('LOCALES', '').split()
231 if not locales:
232 locales = formats[KEY_LOCALES]
233
Yu-Ping Wuc90a22f2020-04-24 11:17:15 +0800234 # Ignore SIGINT in child processes
235 sigint_handler = signal.signal(signal.SIGINT, signal.SIG_IGN)
Hung-Te Lin04addcc2015-03-23 18:43:30 +0800236 pool = multiprocessing.Pool(multiprocessing.cpu_count())
Yu-Ping Wuc90a22f2020-04-24 11:17:15 +0800237 signal.signal(signal.SIGINT, sigint_handler)
238
Hung-Te Lin04addcc2015-03-23 18:43:30 +0800239 results = []
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800240 for locale in locales:
Yu-Ping Wud71b4452020-06-16 11:00:26 +0800241 print(locale, end=' ', flush=True)
Yu-Ping Wuae79af62020-09-23 16:48:06 +0800242 inputs = parse_locale_input_files(locale, json_dir)
243 output_dir = os.path.normpath(os.path.join(STAGE_DIR, 'locale', locale))
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800244 if not os.path.exists(output_dir):
245 os.makedirs(output_dir)
Julius Wernera77900c2018-02-01 17:39:07 -0800246 files = formats[KEY_FILES]
247 styles = formats[KEY_STYLES]
Matt Delco4c5580d2019-03-07 14:00:28 -0800248
249 # Now parse strings for optional features
Joel Kitching2d6df162020-07-10 14:15:55 +0800250 if os.getenv("DIAGNOSTIC_UI") == "1" and DIAGNOSTIC_FILES in formats:
Matt Delco4c5580d2019-03-07 14:00:28 -0800251 files.update(formats[DIAGNOSTIC_FILES])
252
Yu-Ping Wuae79af62020-09-23 16:48:06 +0800253 build_text_files(inputs, files, output_dir)
Shelley Chen2f616ac2017-05-22 13:19:40 -0700254
Yu-Ping Wuae79af62020-09-23 16:48:06 +0800255 results += [pool.apply_async(convert_text_to_png,
Daisuke Nojiri9ae0ed82016-08-30 15:43:00 -0700256 (locale, file_name,
Shelley Chen2f616ac2017-05-22 13:19:40 -0700257 styles, formats[KEY_FONTS],
Hung-Te Lin04addcc2015-03-23 18:43:30 +0800258 output_dir))
259 for file_name in formats[KEY_FILES]]
Hung-Te Lin04addcc2015-03-23 18:43:30 +0800260 pool.close()
Jes Klinke1687a992020-06-16 13:47:17 -0700261 if json_dir is not None:
262 shutil.rmtree(json_dir)
Yu-Ping Wud71b4452020-06-16 11:00:26 +0800263 print()
Yu-Ping Wuc90a22f2020-04-24 11:17:15 +0800264
265 try:
266 success = [r.get() for r in results]
267 except KeyboardInterrupt:
268 pool.terminate()
269 pool.join()
270 exit('Aborted by user')
271 else:
272 pool.join()
273 if not all(success):
274 exit('Failed to render some locales')
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800275
276
277if __name__ == '__main__':
278 main(sys.argv[1:])