blob: b7b833706eae48ab63865970355d46ad51ba77c4 [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
Yu-Ping Wu7f6639a2020-09-28 15:31:35 +080023import copy
Hung-Te Lin04addcc2015-03-23 18:43:30 +080024
Hung-Te Lindf738512018-09-14 08:39:27 +080025from PIL import Image
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080026import yaml
27
28SCRIPT_BASE = os.path.dirname(os.path.abspath(__file__))
Yu-Ping Wu10cf2892020-08-10 17:20:11 +080029DEFAULT_NAME = '_DEFAULT_'
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080030KEY_LOCALES = 'locales'
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080031KEY_FILES = 'files'
32KEY_FONTS = 'fonts'
33KEY_STYLES = 'styles'
Matt Delco4c5580d2019-03-07 14:00:28 -080034DIAGNOSTIC_FILES = 'diagnostic_files'
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080035
Jes Klinke1687a992020-06-16 13:47:17 -070036STRINGS_GRD_FILE = 'firmware_strings.grd'
37STRINGS_JSON_FILE_TMPL = '{}.json'
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080038FORMAT_FILE = 'format.yaml'
Yu-Ping Wu8f633b82020-09-22 14:27:57 +080039TXT_TO_PNG_SVG = os.path.join(SCRIPT_BASE, 'text_to_png_svg')
Yu-Ping Wu7f6639a2020-09-28 15:31:35 +080040STRINGS_DIR = os.path.join(SCRIPT_BASE, 'strings')
41LOCALE_DIR = os.path.join(STRINGS_DIR, 'locale')
Yu-Ping Wuae79af62020-09-23 16:48:06 +080042STAGE_DIR = os.path.join(os.getenv('OUTPUT',
43 os.path.join(SCRIPT_BASE, 'build')),
44 '.stage')
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080045
Jes Klinke1687a992020-06-16 13:47:17 -070046# Regular expressions used to eliminate spurious spaces and newlines in
47# translation strings.
48NEWLINE_PATTERN = re.compile(r'([^\n])\n([^\n])')
49NEWLINE_REPLACEMENT = r'\1 \2'
50CRLF_PATTERN = re.compile(r'\r\n')
51MULTIBLANK_PATTERN = re.compile(r' *')
52
Yu-Ping Wu11027f02020-10-14 17:35:42 +080053GLYPH_FONT = 'Noto Sans Mono'
54
Yu-Ping Wu6b282c52020-03-19 12:54:15 +080055
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080056class DataError(Exception):
57 pass
58
59
Yu-Ping Wu11027f02020-10-14 17:35:42 +080060def convert_text_to_png(locale, input_file, font, margin, output_dir,
61 options=None):
62 """Converts text files into PNG image files.
63
64 Args:
65 locale: Locale (language) to select implicit rendering options. None for
66 locale-independent strings.
67 input_file: Path of input text file.
68 font: Font spec.
69 margin: CSS-style margin.
70 output_dir: Directory to generate image files.
71 options: List of other options to be added.
72 """
73 name, _ = os.path.splitext(os.path.basename(input_file))
74 command = [TXT_TO_PNG_SVG, '--outdir=%s' % output_dir]
75 if locale:
76 command.append('--lan=%s' % locale)
77 if font:
78 command.append("--font='%s'" % font)
79 font_size = os.getenv('FONTSIZE')
80 if font_size:
81 command.append('--point=%r' % font_size)
82 if margin:
83 command.append('--margin="%s"' % margin)
84 # TODO(b/159399377): Set different widths for titles and descriptions.
85 # Currently only wrap lines for descriptions.
86 if '_desc' in name:
87 # Without the --width option set, the minimum height of the output SVG
88 # image is roughly 22px (for locale 'en'). With --width=WIDTH passed to
89 # pango-view, the width of the output seems to always be (WIDTH * 4 / 3),
90 # regardless of the font being used. Therefore, set the max_width in
91 # points as follows to prevent drawing from exceeding canvas boundary in
92 # depthcharge runtime.
93 # Some of the numbers below are from depthcharge:
94 # - 1000: UI_SCALE
95 # - 50: UI_MARGIN_H
96 # - 228: UI_REC_QR_SIZE
97 # - 24: UI_REC_QR_MARGIN_H
98 # - 24: UI_DESC_TEXT_HEIGHT
99 if name == 'rec_phone_step2_desc':
100 max_width = 1000 - 50 * 2 - 228 - 24 * 2
101 else:
102 max_width = 1000 - 50 * 2
103 max_width_pt = int(22 * max_width / 24 / (4 / 3))
104 command.append('--width=%d' % max_width_pt)
105 if options:
106 command.extend(options)
107 command.append(input_file)
108
109 return subprocess.call(' '.join(command), shell=True,
110 stdout=subprocess.PIPE) == 0
111
112
113def convert_glyphs():
114 """Converts glyphs of ascii characters."""
115 font_dir = os.path.join(STAGE_DIR, 'font')
116 os.makedirs(font_dir, exist_ok=True)
117 # Remove the extra whitespace at the top/bottom within the glyphs
118 margin = '-3 0 -1 0'
119 options = ['--color="#ffffff"', '--bgcolor="#000000"']
120 for c in range(ord(' '), ord('~') + 1):
121 txt_file = os.path.join(font_dir, f'idx{c:03d}_{c:02x}.txt')
122 with open(txt_file, 'w', encoding='ascii') as f:
123 f.write(chr(c))
124 f.write('\n')
125 # TODO(b/163109632): Parallelize the conversion of glyphs
126 convert_text_to_png(None, txt_file, GLYPH_FONT, margin, font_dir, options)
127
128
Yu-Ping Wuae79af62020-09-23 16:48:06 +0800129def _load_locale_json_file(locale, json_dir):
Jes Klinke1687a992020-06-16 13:47:17 -0700130 result = {}
Yu-Ping Wuae79af62020-09-23 16:48:06 +0800131 filename = os.path.join(json_dir, STRINGS_JSON_FILE_TMPL.format(locale))
Yu-Ping Wud71b4452020-06-16 11:00:26 +0800132 with open(filename, encoding='utf-8-sig') as input_file:
Jes Klinke1687a992020-06-16 13:47:17 -0700133 for tag, msgdict in json.load(input_file).items():
134 msgtext = msgdict['message']
135 msgtext = re.sub(CRLF_PATTERN, '\n', msgtext)
136 msgtext = re.sub(NEWLINE_PATTERN, NEWLINE_REPLACEMENT, msgtext)
137 msgtext = re.sub(MULTIBLANK_PATTERN, ' ', msgtext)
138 # Strip any trailing whitespace. A trailing newline appears to make
139 # Pango report a larger layout size than what's actually visible.
140 msgtext = msgtext.strip()
141 result[tag] = msgtext
142 return result
143
Yu-Ping Wuae79af62020-09-23 16:48:06 +0800144
145def parse_locale_json_file(locale, json_dir):
146 """Parses given firmware string json file for build_text_files.
Mathew King89d48c62019-02-15 10:08:39 -0700147
148 Args:
Yu-Ping Wu8f633b82020-09-22 14:27:57 +0800149 locale: The name of the locale, e.g. "da" or "pt-BR".
Jes Klinke1687a992020-06-16 13:47:17 -0700150 json_dir: Directory containing json output from grit.
Mathew King89d48c62019-02-15 10:08:39 -0700151
152 Returns:
153 A dictionary for mapping of "name to content" for files to be generated.
154 """
Yu-Ping Wuae79af62020-09-23 16:48:06 +0800155 result = _load_locale_json_file(locale, json_dir)
156 original = _load_locale_json_file('en', json_dir)
157 for tag in original:
158 if tag not in result:
159 # Use original English text, in case translation is not yet available
160 print('WARNING: locale "%s", missing entry %s' % (locale, tag))
161 result[tag] = original[tag]
Mathew King89d48c62019-02-15 10:08:39 -0700162
Yu-Ping Wuae79af62020-09-23 16:48:06 +0800163 return result
Mathew King89d48c62019-02-15 10:08:39 -0700164
Yu-Ping Wuae79af62020-09-23 16:48:06 +0800165
166def parse_locale_input_files(locale, json_dir):
167 """Parses all firmware string files for the given locale.
168
169 Args:
170 locale: The name of the locale, e.g. "da" or "pt-BR".
171 json_dir: Directory containing json output from grit.
172
173 Returns:
174 A dictionary for mapping of "name to content" for files to be generated.
175 """
176 result = parse_locale_json_file(locale, json_dir)
177
178 # Walk locale directory to add pre-generated texts such as language names.
Yu-Ping Wu8f633b82020-09-22 14:27:57 +0800179 for input_file in glob.glob(os.path.join(LOCALE_DIR, locale, "*.txt")):
Mathew King89d48c62019-02-15 10:08:39 -0700180 name, _ = os.path.splitext(os.path.basename(input_file))
Yu-Ping Wud71b4452020-06-16 11:00:26 +0800181 with open(input_file, 'r', encoding='utf-8-sig') as f:
Mathew King89d48c62019-02-15 10:08:39 -0700182 result[name] = f.read().strip()
Shelley Chen2f616ac2017-05-22 13:19:40 -0700183
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800184 return result
185
186
Yu-Ping Wuae79af62020-09-23 16:48:06 +0800187def create_file(file_name, contents, output_dir):
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800188 """Creates a text file in output directory by given contents.
189
190 Args:
191 file_name: Output file name without extension.
192 contents: A list of strings for file content.
193 output_dir: The directory to store output file.
194 """
195 output_name = os.path.join(output_dir, file_name + '.txt')
Yu-Ping Wud71b4452020-06-16 11:00:26 +0800196 with open(output_name, 'w', encoding='utf-8-sig') as f:
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800197 f.write('\n'.join(contents) + '\n')
198
199
Yu-Ping Wuae79af62020-09-23 16:48:06 +0800200def build_text_files(inputs, files, output_dir):
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800201 """Builds text files from given input data.
202
203 Args:
204 inputs: Dictionary of contents for given file name.
205 files: List of file records: [name, content].
206 output_dir: Directory to generate text files.
207 """
Yu-Ping Wud71b4452020-06-16 11:00:26 +0800208 for file_name, file_content in files.items():
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800209 if file_content is None:
Yu-Ping Wuae79af62020-09-23 16:48:06 +0800210 create_file(file_name, [inputs[file_name]], output_dir)
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800211 else:
212 contents = []
213 for data in file_content:
Yu-Ping Wuae79af62020-09-23 16:48:06 +0800214 contents.append(inputs[data])
215 create_file(file_name, contents, output_dir)
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800216
217
Yu-Ping Wu7f6639a2020-09-28 15:31:35 +0800218def convert_localized_strings(formats, locales):
219 """Converts localized strings for |locales|."""
220 # Make a copy of formats to avoid modifying it
221 formats = copy.deepcopy(formats)
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800222
Yu-Ping Wu7f6639a2020-09-28 15:31:35 +0800223 if not locales:
224 env_locales = os.getenv('LOCALES')
225 if env_locales:
226 locales = env_locales.split()
227 else:
228 locales = formats[KEY_LOCALES]
229
230 styles = formats[KEY_STYLES]
231 fonts = formats[KEY_FONTS]
232 default_font = fonts.get(DEFAULT_NAME)
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800233
Yu-Ping Wu51940352020-09-17 08:48:55 +0800234 # Sources are one .grd file with identifiers chosen by engineers and
235 # corresponding English texts, as well as a set of .xlt files (one for each
236 # language other than US english) with a mapping from hash to translation.
237 # Because the keys in the xlt files are a hash of the English source text,
238 # rather than our identifiers, such as "btn_cancel", we use the "grit"
239 # command line tool to process the .grd and .xlt files, producing a set of
240 # .json files mapping our identifier to the translated string, one for every
241 # language including US English.
Jes Klinke1687a992020-06-16 13:47:17 -0700242
Yu-Ping Wu51940352020-09-17 08:48:55 +0800243 # Create a temporary directory to place the translation output from grit in.
244 json_dir = tempfile.mkdtemp()
Yu-Ping Wu7f6639a2020-09-28 15:31:35 +0800245
Yu-Ping Wu51940352020-09-17 08:48:55 +0800246 # This invokes the grit build command to generate JSON files from the XTB
247 # files containing translations. The results are placed in `json_dir` as
248 # specified in firmware_strings.grd, i.e. one JSON file per locale.
249 subprocess.check_call([
250 'grit',
Yu-Ping Wu8f633b82020-09-22 14:27:57 +0800251 '-i', os.path.join(LOCALE_DIR, STRINGS_GRD_FILE),
Yu-Ping Wu51940352020-09-17 08:48:55 +0800252 'build',
253 '-o', os.path.join(json_dir)
254 ])
Jes Klinke1687a992020-06-16 13:47:17 -0700255
Yu-Ping Wuc90a22f2020-04-24 11:17:15 +0800256 # Ignore SIGINT in child processes
257 sigint_handler = signal.signal(signal.SIGINT, signal.SIG_IGN)
Hung-Te Lin04addcc2015-03-23 18:43:30 +0800258 pool = multiprocessing.Pool(multiprocessing.cpu_count())
Yu-Ping Wuc90a22f2020-04-24 11:17:15 +0800259 signal.signal(signal.SIGINT, sigint_handler)
260
Hung-Te Lin04addcc2015-03-23 18:43:30 +0800261 results = []
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800262 for locale in locales:
Yu-Ping Wud71b4452020-06-16 11:00:26 +0800263 print(locale, end=' ', flush=True)
Yu-Ping Wuae79af62020-09-23 16:48:06 +0800264 inputs = parse_locale_input_files(locale, json_dir)
265 output_dir = os.path.normpath(os.path.join(STAGE_DIR, 'locale', locale))
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800266 if not os.path.exists(output_dir):
267 os.makedirs(output_dir)
Julius Wernera77900c2018-02-01 17:39:07 -0800268 files = formats[KEY_FILES]
Matt Delco4c5580d2019-03-07 14:00:28 -0800269
270 # Now parse strings for optional features
Yu-Ping Wu7f6639a2020-09-28 15:31:35 +0800271 if os.getenv('DIAGNOSTIC_UI') == '1' and DIAGNOSTIC_FILES in formats:
Matt Delco4c5580d2019-03-07 14:00:28 -0800272 files.update(formats[DIAGNOSTIC_FILES])
273
Yu-Ping Wuae79af62020-09-23 16:48:06 +0800274 build_text_files(inputs, files, output_dir)
Shelley Chen2f616ac2017-05-22 13:19:40 -0700275
Yu-Ping Wuae79af62020-09-23 16:48:06 +0800276 results += [pool.apply_async(convert_text_to_png,
Yu-Ping Wu7f6639a2020-09-28 15:31:35 +0800277 (locale,
278 os.path.join(output_dir, '%s.txt' % name),
Yu-Ping Wu7f6639a2020-09-28 15:31:35 +0800279 fonts.get(locale, default_font),
Yu-Ping Wu11027f02020-10-14 17:35:42 +0800280 '0',
281 output_dir,
282 [styles.get(name)]))
Yu-Ping Wu7f6639a2020-09-28 15:31:35 +0800283 for name in formats[KEY_FILES]]
Hung-Te Lin04addcc2015-03-23 18:43:30 +0800284 pool.close()
Jes Klinke1687a992020-06-16 13:47:17 -0700285 if json_dir is not None:
286 shutil.rmtree(json_dir)
Yu-Ping Wud71b4452020-06-16 11:00:26 +0800287 print()
Yu-Ping Wuc90a22f2020-04-24 11:17:15 +0800288
289 try:
290 success = [r.get() for r in results]
291 except KeyboardInterrupt:
292 pool.terminate()
293 pool.join()
294 exit('Aborted by user')
295 else:
296 pool.join()
297 if not all(success):
298 exit('Failed to render some locales')
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800299
300
Yu-Ping Wu7f6639a2020-09-28 15:31:35 +0800301def main(argv):
302 with open(FORMAT_FILE, encoding='utf-8') as f:
303 formats = yaml.load(f)
304
Yu-Ping Wu11027f02020-10-14 17:35:42 +0800305 # Convert glyphs
306 print('Converting glyphs...')
307 convert_glyphs()
308
Yu-Ping Wu7f6639a2020-09-28 15:31:35 +0800309 # Convert generic (locale-independent) strings
310 styles = formats[KEY_STYLES]
311 fonts = formats[KEY_FONTS]
312 default_font = fonts.get(DEFAULT_NAME)
313
314 for input_file in glob.glob(os.path.join(STRINGS_DIR, '*.txt')):
315 name, _ = os.path.splitext(os.path.basename(input_file))
316 style = styles.get(name)
Yu-Ping Wu11027f02020-10-14 17:35:42 +0800317 if not convert_text_to_png(None, input_file, default_font, '0',
318 STAGE_DIR, options=[style]):
Yu-Ping Wu7f6639a2020-09-28 15:31:35 +0800319 exit('Failed to convert text %s' % input_file)
320
321 # Convert localized strings
322 convert_localized_strings(formats, argv)
323
324
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800325if __name__ == '__main__':
326 main(sys.argv[1:])