blob: d861759ed4d984259f0105103e25843a9594867c [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'
Mathew King89d48c62019-02-15 10:08:39 -070033VENDOR_INPUTS = 'vendor_inputs'
34VENDOR_FILES = 'vendor_files'
Matt Delco4c5580d2019-03-07 14:00:28 -080035DIAGNOSTIC_FILES = 'diagnostic_files'
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080036
Jes Klinke1687a992020-06-16 13:47:17 -070037STRINGS_GRD_FILE = 'firmware_strings.grd'
38STRINGS_JSON_FILE_TMPL = '{}.json'
Mathew King89d48c62019-02-15 10:08:39 -070039VENDOR_STRINGS_FILE = 'vendor_strings.txt'
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080040FORMAT_FILE = 'format.yaml'
Mathew King89d48c62019-02-15 10:08:39 -070041VENDOR_FORMAT_FILE = 'vendor_format.yaml'
Yu-Ping Wu8f633b82020-09-22 14:27:57 +080042TXT_TO_PNG_SVG = os.path.join(SCRIPT_BASE, 'text_to_png_svg')
43LOCALE_DIR = os.path.join(SCRIPT_BASE, 'strings', 'locale')
44OUTPUT_DIR = os.path.join(os.getenv('OUTPUT', os.path.join(SCRIPT_BASE,
45 'build')),
Hung-Te Lin6e381992015-03-18 20:09:30 +080046 '.stage', 'locale')
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080047
Mathew King89d48c62019-02-15 10:08:39 -070048VENDOR_STRINGS_DIR = os.getenv("VENDOR_STRINGS_DIR")
49VENDOR_STRINGS = VENDOR_STRINGS_DIR != None
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080050
Jes Klinke1687a992020-06-16 13:47:17 -070051# Regular expressions used to eliminate spurious spaces and newlines in
52# translation strings.
53NEWLINE_PATTERN = re.compile(r'([^\n])\n([^\n])')
54NEWLINE_REPLACEMENT = r'\1 \2'
55CRLF_PATTERN = re.compile(r'\r\n')
56MULTIBLANK_PATTERN = re.compile(r' *')
57
Yu-Ping Wu6b282c52020-03-19 12:54:15 +080058
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080059class DataError(Exception):
60 pass
61
62
63def GetImageWidth(filename):
64 """Returns the width of given image file."""
65 return Image.open(filename).size[0]
66
Mathew King89d48c62019-02-15 10:08:39 -070067def ParseLocaleInputFile(locale_dir, strings_file, input_format):
68 """Parses firmware string file in given locale directory for BuildTextFiles
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080069
70 Args:
Mathew King89d48c62019-02-15 10:08:39 -070071 locale: The locale folder with firmware string files.
72 strings_file: The name of the string txt file
73 input_format: Format description for each line in strings_file.
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080074
75 Returns:
76 A dictionary for mapping of "name to content" for files to be generated.
77 """
Mathew King89d48c62019-02-15 10:08:39 -070078 input_file = os.path.join(locale_dir, strings_file)
Yu-Ping Wud71b4452020-06-16 11:00:26 +080079 with open(input_file, 'r', encoding='utf-8-sig') as f:
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080080 input_data = f.readlines()
81 if len(input_data) != len(input_format):
Mathew King89d48c62019-02-15 10:08:39 -070082 raise DataError('Input file <%s> for locale <%s> '
83 'does not match input format.' %
84 (strings_file, locale_dir))
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080085 input_data = [s.strip() for s in input_data]
Mathew King89d48c62019-02-15 10:08:39 -070086 return dict(zip(input_format, input_data))
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080087
Jes Klinke1687a992020-06-16 13:47:17 -070088def ParseLocaleInputJsonFile(locale, strings_json_file_tmpl, json_dir):
89 """Parses given firmware string json file for BuildTextFiles
90
91 Args:
92 locale: The name of the locale, e.g. "da" or "pt-BR".
93 strings_json_file_tmpl: The template for the json input file name.
94 json_dir: Directory containing json output from grit.
95
96 Returns:
97 A dictionary for mapping of "name to content" for files to be generated.
98 """
99 result = LoadLocaleJsonFile(locale, strings_json_file_tmpl, json_dir)
100 original = LoadLocaleJsonFile("en", strings_json_file_tmpl, json_dir)
101 for tag in original:
102 if not tag in result:
103 # Use original English text, in case translation is not yet available
Yu-Ping Wud71b4452020-06-16 11:00:26 +0800104 print('WARNING: locale "%s", missing entry %s' % (locale, tag))
Jes Klinke1687a992020-06-16 13:47:17 -0700105 result[tag] = original[tag]
106
107 return result
108
109def LoadLocaleJsonFile(locale, strings_json_file_tmpl, json_dir):
110 result = {}
111 filename = os.path.join(json_dir, strings_json_file_tmpl.format(locale))
Yu-Ping Wud71b4452020-06-16 11:00:26 +0800112 with open(filename, encoding='utf-8-sig') as input_file:
Jes Klinke1687a992020-06-16 13:47:17 -0700113 for tag, msgdict in json.load(input_file).items():
114 msgtext = msgdict['message']
115 msgtext = re.sub(CRLF_PATTERN, '\n', msgtext)
116 msgtext = re.sub(NEWLINE_PATTERN, NEWLINE_REPLACEMENT, msgtext)
117 msgtext = re.sub(MULTIBLANK_PATTERN, ' ', msgtext)
118 # Strip any trailing whitespace. A trailing newline appears to make
119 # Pango report a larger layout size than what's actually visible.
120 msgtext = msgtext.strip()
121 result[tag] = msgtext
122 return result
123
Yu-Ping Wu8f633b82020-09-22 14:27:57 +0800124def ParseLocaleInputFiles(locale, vendor_format, json_dir):
Mathew King89d48c62019-02-15 10:08:39 -0700125 """Parses all firmware string files in given locale directory for
126 BuildTextFiles
127
128 Args:
Yu-Ping Wu8f633b82020-09-22 14:27:57 +0800129 locale: The name of the locale, e.g. "da" or "pt-BR".
Mathew King89d48c62019-02-15 10:08:39 -0700130 vendor_format: Format description for each line in VENDOR_STRINGS_FILE.
Jes Klinke1687a992020-06-16 13:47:17 -0700131 json_dir: Directory containing json output from grit.
Mathew King89d48c62019-02-15 10:08:39 -0700132
133 Returns:
134 A dictionary for mapping of "name to content" for files to be generated.
135 """
136 result = dict()
Yu-Ping Wu8f633b82020-09-22 14:27:57 +0800137 result.update(ParseLocaleInputJsonFile(locale,
Yu-Ping Wu51940352020-09-17 08:48:55 +0800138 STRINGS_JSON_FILE_TMPL,
139 json_dir))
Mathew King89d48c62019-02-15 10:08:39 -0700140
141 # Parse vendor files if enabled
142 if VENDOR_STRINGS:
Yu-Ping Wud71b4452020-06-16 11:00:26 +0800143 print(' (vendor specific strings)')
Mathew King89d48c62019-02-15 10:08:39 -0700144 result.update(
Yu-Ping Wu8f633b82020-09-22 14:27:57 +0800145 ParseLocaleInputFile(os.path.join(VENDOR_STRINGS_DIR, locale),
Mathew King89d48c62019-02-15 10:08:39 -0700146 VENDOR_STRINGS_FILE,
147 vendor_format))
148
149 # Walk locale directory to add pre-generated items.
Yu-Ping Wu8f633b82020-09-22 14:27:57 +0800150 for input_file in glob.glob(os.path.join(LOCALE_DIR, locale, "*.txt")):
Yu-Ping Wu51940352020-09-17 08:48:55 +0800151 if os.path.basename(input_file) == VENDOR_STRINGS_FILE:
Mathew King89d48c62019-02-15 10:08:39 -0700152 continue
153 name, _ = os.path.splitext(os.path.basename(input_file))
Yu-Ping Wud71b4452020-06-16 11:00:26 +0800154 with open(input_file, 'r', encoding='utf-8-sig') as f:
Mathew King89d48c62019-02-15 10:08:39 -0700155 result[name] = f.read().strip()
Shelley Chen2f616ac2017-05-22 13:19:40 -0700156
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800157 return result
158
159
160def CreateFile(file_name, contents, output_dir):
161 """Creates a text file in output directory by given contents.
162
163 Args:
164 file_name: Output file name without extension.
165 contents: A list of strings for file content.
166 output_dir: The directory to store output file.
167 """
168 output_name = os.path.join(output_dir, file_name + '.txt')
Yu-Ping Wud71b4452020-06-16 11:00:26 +0800169 with open(output_name, 'w', encoding='utf-8-sig') as f:
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800170 f.write('\n'.join(contents) + '\n')
171
172
173def ModifyContent(input_data, command):
174 """Modifies some input content with given Regex commands.
175
176 Args:
177 input_data: Input string to be modified.
178 command: Regex commands to execute.
179
180 Returns:
181 Processed output string.
182 """
183 if not command.startswith('s/'):
184 raise DataError('Unknown command: %s' % command)
185 _, pattern, repl, _ = command.split('/')
186 return re.sub(pattern, repl, input_data)
187
188
189def BuildTextFiles(inputs, files, output_dir):
190 """Builds text files from given input data.
191
192 Args:
193 inputs: Dictionary of contents for given file name.
194 files: List of file records: [name, content].
195 output_dir: Directory to generate text files.
196 """
Yu-Ping Wud71b4452020-06-16 11:00:26 +0800197 for file_name, file_content in files.items():
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800198 if file_content is None:
199 CreateFile(file_name, [inputs[file_name]], output_dir)
200 else:
201 contents = []
202 for data in file_content:
203 if '@' in data:
204 name, _, command = data.partition('@')
205 contents.append(ModifyContent(inputs[name], command))
206 else:
207 contents.append('' if data == '' else inputs[data])
208 CreateFile(file_name, contents, output_dir)
209
210
Daisuke Nojiri9ae0ed82016-08-30 15:43:00 -0700211def ConvertPngFile(locale, file_name, styles, fonts, output_dir):
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800212 """Converts text files into PNG image files.
213
214 Args:
215 locale: Locale (language) to select implicit rendering options.
Hung-Te Lin04addcc2015-03-23 18:43:30 +0800216 file_name: String of input file name to generate.
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800217 styles: Dictionary to get associated per-file style options.
Yu-Ping Wu10cf2892020-08-10 17:20:11 +0800218 fonts: Dictionary to get associated per-file font options. The value at
219 DEFAULT_NAME is used when |locale| is not in the dict, and the '--font'
220 option is omitted when neither exist.
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800221 output_dir: Directory to generate image files.
222 """
Hung-Te Lin04addcc2015-03-23 18:43:30 +0800223 input_file = os.path.join(output_dir, file_name + '.txt')
224 command = [TXT_TO_PNG_SVG, "--lan=%s" % locale, "--outdir=%s" % output_dir]
225 if file_name in styles:
226 command.append(styles[file_name])
Yu-Ping Wu10cf2892020-08-10 17:20:11 +0800227 default_font = fonts.get(DEFAULT_NAME)
228 font = fonts.get(locale, default_font)
229 if font:
230 command.append("--font='%s'" % font)
Hung-Te Lin04addcc2015-03-23 18:43:30 +0800231 font_size = os.getenv("FONTSIZE")
232 if font_size is not None:
233 command.append('--point=%r' % font_size)
Yu-Ping Wu51940352020-09-17 08:48:55 +0800234 command.append('--margin="0 0"')
235 # TODO(b/159399377): Set different widths for titles and descriptions.
236 # Currently only wrap lines for descriptions.
237 if '_desc' in file_name:
238 # Without the --width option set, the minimum height of the output SVG
239 # image is roughly 22px (for locale 'en'). With --width=WIDTH passed to
240 # pango-view, the width of the output seems to always be (WIDTH * 4 / 3),
241 # regardless of the font being used. Therefore, set the max_width in
242 # points as follows to prevent drawing from exceeding canvas boundary in
243 # depthcharge runtime.
244 # Some of the numbers below are from depthcharge:
245 # - 1000: UI_SCALE
246 # - 50: UI_MARGIN_H
247 # - 228: UI_REC_QR_SIZE
248 # - 24: UI_REC_QR_MARGIN_H
249 # - 24: UI_DESC_TEXT_HEIGHT
250 if file_name == 'rec_phone_step2_desc':
251 max_width = 1000 - 50 * 2 - 228 - 24 * 2
252 else:
253 max_width = 1000 - 50 * 2
254 max_width_pt = int(22 * max_width / 24 / (4 / 3))
255 command.append('--width=%d' % max_width_pt)
Hung-Te Lin04addcc2015-03-23 18:43:30 +0800256 command.append(input_file)
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800257
Hung-Te Lin04addcc2015-03-23 18:43:30 +0800258 if subprocess.call(' '.join(command), shell=True,
259 stdout=subprocess.PIPE) != 0:
260 return False
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800261
Hung-Te Lin04addcc2015-03-23 18:43:30 +0800262 # Check output file size
263 output_file = os.path.join(output_dir, file_name + '.png')
Hung-Te Lin04addcc2015-03-23 18:43:30 +0800264
265 return True
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800266
267def main(argv):
Yu-Ping Wu51940352020-09-17 08:48:55 +0800268 with open(FORMAT_FILE, encoding='utf-8') as f:
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800269 formats = yaml.load(f)
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800270
Mathew King89d48c62019-02-15 10:08:39 -0700271 if VENDOR_STRINGS:
Yu-Ping Wud71b4452020-06-16 11:00:26 +0800272 with open(os.path.join(VENDOR_STRINGS_DIR, VENDOR_FORMAT_FILE),
273 encoding='utf-8') as f:
Mathew King89d48c62019-02-15 10:08:39 -0700274 formats.update(yaml.load(f))
275
Jes Klinke1687a992020-06-16 13:47:17 -0700276 json_dir = None
Yu-Ping Wu51940352020-09-17 08:48:55 +0800277 # Sources are one .grd file with identifiers chosen by engineers and
278 # corresponding English texts, as well as a set of .xlt files (one for each
279 # language other than US english) with a mapping from hash to translation.
280 # Because the keys in the xlt files are a hash of the English source text,
281 # rather than our identifiers, such as "btn_cancel", we use the "grit"
282 # command line tool to process the .grd and .xlt files, producing a set of
283 # .json files mapping our identifier to the translated string, one for every
284 # language including US English.
Jes Klinke1687a992020-06-16 13:47:17 -0700285
Yu-Ping Wu51940352020-09-17 08:48:55 +0800286 # Create a temporary directory to place the translation output from grit in.
287 json_dir = tempfile.mkdtemp()
288 # This invokes the grit build command to generate JSON files from the XTB
289 # files containing translations. The results are placed in `json_dir` as
290 # specified in firmware_strings.grd, i.e. one JSON file per locale.
291 subprocess.check_call([
292 'grit',
Yu-Ping Wu8f633b82020-09-22 14:27:57 +0800293 '-i', os.path.join(LOCALE_DIR, STRINGS_GRD_FILE),
Yu-Ping Wu51940352020-09-17 08:48:55 +0800294 'build',
295 '-o', os.path.join(json_dir)
296 ])
Jes Klinke1687a992020-06-16 13:47:17 -0700297
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800298 # Decide locales to build.
299 if len(argv) > 0:
300 locales = argv
301 else:
302 locales = os.getenv('LOCALES', '').split()
303 if not locales:
304 locales = formats[KEY_LOCALES]
305
Yu-Ping Wuc90a22f2020-04-24 11:17:15 +0800306 # Ignore SIGINT in child processes
307 sigint_handler = signal.signal(signal.SIGINT, signal.SIG_IGN)
Hung-Te Lin04addcc2015-03-23 18:43:30 +0800308 pool = multiprocessing.Pool(multiprocessing.cpu_count())
Yu-Ping Wuc90a22f2020-04-24 11:17:15 +0800309 signal.signal(signal.SIGINT, sigint_handler)
310
Hung-Te Lin04addcc2015-03-23 18:43:30 +0800311 results = []
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800312 for locale in locales:
Yu-Ping Wud71b4452020-06-16 11:00:26 +0800313 print(locale, end=' ', flush=True)
Yu-Ping Wu51940352020-09-17 08:48:55 +0800314 inputs = ParseLocaleInputFiles(locale,
Mathew King89d48c62019-02-15 10:08:39 -0700315 formats[VENDOR_INPUTS] if VENDOR_STRINGS
Jes Klinke1687a992020-06-16 13:47:17 -0700316 else None,
317 json_dir)
Vadim Bendeburyc2dfa1d2014-02-12 10:32:27 -0800318 output_dir = os.path.normpath(os.path.join(OUTPUT_DIR, locale))
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800319 if not os.path.exists(output_dir):
320 os.makedirs(output_dir)
Julius Wernera77900c2018-02-01 17:39:07 -0800321 files = formats[KEY_FILES]
322 styles = formats[KEY_STYLES]
Matt Delco4c5580d2019-03-07 14:00:28 -0800323
324 # Now parse strings for optional features
Joel Kitching2d6df162020-07-10 14:15:55 +0800325 if os.getenv("DIAGNOSTIC_UI") == "1" and DIAGNOSTIC_FILES in formats:
Matt Delco4c5580d2019-03-07 14:00:28 -0800326 files.update(formats[DIAGNOSTIC_FILES])
327
Mathew King89d48c62019-02-15 10:08:39 -0700328 if VENDOR_STRINGS:
329 files.update(formats[VENDOR_FILES])
Shelley Chen2f616ac2017-05-22 13:19:40 -0700330 BuildTextFiles(inputs, files, output_dir)
331
Hung-Te Lin04addcc2015-03-23 18:43:30 +0800332 results += [pool.apply_async(ConvertPngFile,
Daisuke Nojiri9ae0ed82016-08-30 15:43:00 -0700333 (locale, file_name,
Shelley Chen2f616ac2017-05-22 13:19:40 -0700334 styles, formats[KEY_FONTS],
Hung-Te Lin04addcc2015-03-23 18:43:30 +0800335 output_dir))
336 for file_name in formats[KEY_FILES]]
Hung-Te Lin04addcc2015-03-23 18:43:30 +0800337 pool.close()
Jes Klinke1687a992020-06-16 13:47:17 -0700338 if json_dir is not None:
339 shutil.rmtree(json_dir)
Yu-Ping Wud71b4452020-06-16 11:00:26 +0800340 print()
Yu-Ping Wuc90a22f2020-04-24 11:17:15 +0800341
342 try:
343 success = [r.get() for r in results]
344 except KeyboardInterrupt:
345 pool.terminate()
346 pool.join()
347 exit('Aborted by user')
348 else:
349 pool.join()
350 if not all(success):
351 exit('Failed to render some locales')
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800352
353
354if __name__ == '__main__':
355 main(sys.argv[1:])