blob: 2c63915b83aeb55b381b03360af296de1bbc89aa [file] [log] [blame]
David 'Digit' Turner00b5f272019-04-10 21:15:271#!/usr/bin/env vpython
2# Copyright 2019 The Chromium 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
6"""Helper script used to manage locale-related files in Chromium.
7
8This script is used to check, and potentially fix, many locale-related files
9in your Chromium workspace, such as:
10
11 - GRIT input files (.grd) and the corresponding translations (.xtb).
12
13 - BUILD.gn files listing Android localized resource string resource .xml
14 generated by GRIT for all supported Chrome locales. These correspond to
15 <output> elements that use the type="android" attribute.
16
17The --scan-dir <dir> option can be used to check for all files under a specific
18directory, and the --fix-inplace option can be used to try fixing any file
19that doesn't pass the check.
20
21This can be very handy to avoid tedious and repetitive work when adding new
22translations / locales to the Chrome code base, since this script can update
23said input files for you.
24
25Important note: checks and fix may fail on some input files. For example
26remoting/resources/remoting_strings.grd contains an in-line comment element
27inside its <outputs> section that breaks the script. The check will fail, and
28trying to fix it too, but at least the file will not be modified.
29"""
Raul Tambre9e24293b2019-05-12 06:11:0730
31from __future__ import print_function
32
David 'Digit' Turner00b5f272019-04-10 21:15:2733import argparse
34import json
35import os
36import re
37import shutil
38import subprocess
39import sys
David 'Digit' Turner6e6f63f42019-04-15 22:02:2740import unittest
David 'Digit' Turner00b5f272019-04-10 21:15:2741
42# Assume this script is under build/
43_SCRIPT_DIR = os.path.dirname(__file__)
44_SCRIPT_NAME = os.path.join(_SCRIPT_DIR, os.path.basename(__file__))
45_TOP_SRC_DIR = os.path.join(_SCRIPT_DIR, '..')
46
47# Need to import android/gyp/util/resource_utils.py here.
48sys.path.insert(0, os.path.join(_SCRIPT_DIR, 'android/gyp'))
49
50from util import build_utils
51from util import resource_utils
52
53
54# This locale is the default and doesn't have translations.
55_DEFAULT_LOCALE = 'en-US'
56
57# Misc terminal codes to provide human friendly progress output.
58_CONSOLE_CODE_MOVE_CURSOR_TO_COLUMN_0 = '\x1b[0G'
59_CONSOLE_CODE_ERASE_LINE = '\x1b[K'
60_CONSOLE_START_LINE = (
61 _CONSOLE_CODE_MOVE_CURSOR_TO_COLUMN_0 + _CONSOLE_CODE_ERASE_LINE)
62
63##########################################################################
64##########################################################################
65#####
66##### G E N E R I C H E L P E R F U N C T I O N S
67#####
68##########################################################################
69##########################################################################
70
71def _FixChromiumLangAttribute(lang):
72 """Map XML "lang" attribute values to Chromium locale names."""
73 _CHROMIUM_LANG_FIXES = {
74 'en': 'en-US', # For now, Chromium doesn't have an 'en' locale.
75 'iw': 'he', # 'iw' is the obsolete form of ISO 639-1 for Hebrew
76 'no': 'nb', # 'no' is used by the Translation Console for Norwegian (nb).
77 }
78 return _CHROMIUM_LANG_FIXES.get(lang, lang)
79
80
David 'Digit' Turner6e6f63f42019-04-15 22:02:2781def _FixTranslationConsoleLocaleName(locale):
82 _FIXES = {
83 'nb': 'no', # Norwegian.
84 'he': 'iw', # Hebrew
85 }
86 return _FIXES.get(locale, locale)
87
88
David 'Digit' Turner00b5f272019-04-10 21:15:2789def _CompareLocaleLists(list_a, list_expected, list_name):
90 """Compare two lists of locale names. Print errors if they differ.
91
92 Args:
93 list_a: First list of locales.
94 list_expected: Second list of locales, as expected.
95 list_name: Name of list printed in error messages.
96 Returns:
97 On success, return False. On error, print error messages and return True.
98 """
99 errors = []
100 missing_locales = sorted(set(list_a) - set(list_expected))
101 if missing_locales:
102 errors.append('Missing locales: %s' % missing_locales)
103
104 extra_locales = sorted(set(list_expected) - set(list_a))
105 if extra_locales:
106 errors.append('Unexpected locales: %s' % extra_locales)
107
108 if errors:
Raul Tambre9e24293b2019-05-12 06:11:07109 print('Errors in %s definition:' % list_name)
David 'Digit' Turner00b5f272019-04-10 21:15:27110 for error in errors:
Raul Tambre9e24293b2019-05-12 06:11:07111 print(' %s\n' % error)
David 'Digit' Turner00b5f272019-04-10 21:15:27112 return True
113
114 return False
115
116
117def _BuildIntervalList(input_list, predicate):
118 """Find ranges of contiguous list items that pass a given predicate.
119
120 Args:
121 input_list: An input list of items of any type.
122 predicate: A function that takes a list item and return True if it
123 passes a given test.
124 Returns:
125 A list of (start_pos, end_pos) tuples, where all items in
126 [start_pos, end_pos) pass the predicate.
127 """
128 result = []
129 size = len(input_list)
130 start = 0
131 while True:
132 # Find first item in list that passes the predicate.
133 while start < size and not predicate(input_list[start]):
134 start += 1
135
136 if start >= size:
137 return result
138
139 # Find first item in the rest of the list that does not pass the
140 # predicate.
141 end = start + 1
142 while end < size and predicate(input_list[end]):
143 end += 1
144
145 result.append((start, end))
146 start = end + 1
147
148
149def _SortListSubRange(input_list, start, end, key_func):
150 """Sort an input list's sub-range according to a specific key function.
151
152 Args:
153 input_list: An input list.
154 start: Sub-range starting position in list.
155 end: Sub-range limit position in list.
156 key_func: A function that extracts a sort key from a line.
157 Returns:
158 A copy of |input_list|, with all items in [|start|, |end|) sorted
159 according to |key_func|.
160 """
161 result = input_list[:start]
162 inputs = []
163 for pos in xrange(start, end):
164 line = input_list[pos]
165 key = key_func(line)
166 inputs.append((key, line))
167
168 for _, line in sorted(inputs):
169 result.append(line)
170
171 result += input_list[end:]
172 return result
173
174
175def _SortElementsRanges(lines, element_predicate, element_key):
176 """Sort all elements of a given type in a list of lines by a given key.
177
178 Args:
179 lines: input lines.
180 element_predicate: predicate function to select elements to sort.
181 element_key: lambda returning a comparison key for each element that
182 passes the predicate.
183 Returns:
184 A new list of input lines, with lines [start..end) sorted.
185 """
186 intervals = _BuildIntervalList(lines, element_predicate)
187 for start, end in intervals:
188 lines = _SortListSubRange(lines, start, end, element_key)
189
190 return lines
191
192
193def _ProcessFile(input_file, locales, check_func, fix_func):
194 """Process a given input file, potentially fixing it.
195
196 Args:
197 input_file: Input file path.
198 locales: List of Chrome locales to consider / expect.
199 check_func: A lambda called to check the input file lines with
200 (input_lines, locales) argument. It must return an list of error
201 messages, or None on success.
202 fix_func: None, or a lambda called to fix the input file lines with
203 (input_lines, locales). It must return the new list of lines for
204 the input file, and may raise an Exception in case of error.
205 Returns:
206 True at the moment.
207 """
Raul Tambre9e24293b2019-05-12 06:11:07208 print('%sProcessing %s...' % (_CONSOLE_START_LINE, input_file), end=' ')
David 'Digit' Turner00b5f272019-04-10 21:15:27209 sys.stdout.flush()
210 with open(input_file) as f:
211 input_lines = f.readlines()
212 errors = check_func(input_file, input_lines, locales)
213 if errors:
Raul Tambre9e24293b2019-05-12 06:11:07214 print('\n%s%s' % (_CONSOLE_START_LINE, '\n'.join(errors)))
David 'Digit' Turner00b5f272019-04-10 21:15:27215 if fix_func:
216 try:
217 input_lines = fix_func(input_file, input_lines, locales)
218 output = ''.join(input_lines)
219 with open(input_file, 'wt') as f:
220 f.write(output)
Raul Tambre9e24293b2019-05-12 06:11:07221 print('Fixed %s.' % input_file)
David 'Digit' Turner00b5f272019-04-10 21:15:27222 except Exception as e: # pylint: disable=broad-except
Raul Tambre9e24293b2019-05-12 06:11:07223 print('Skipped %s: %s' % (input_file, e))
David 'Digit' Turner00b5f272019-04-10 21:15:27224
225 return True
226
227
228def _ScanDirectoriesForFiles(scan_dirs, file_predicate):
229 """Scan a directory for files that match a given predicate.
230
231 Args:
232 scan_dir: A list of top-level directories to start scan in.
233 file_predicate: lambda function which is passed the file's base name
234 and returns True if its full path, relative to |scan_dir|, should be
235 passed in the result.
236 Returns:
237 A list of file full paths.
238 """
239 result = []
240 for src_dir in scan_dirs:
241 for root, _, files in os.walk(src_dir):
242 result.extend(os.path.join(root, f) for f in files if file_predicate(f))
243 return result
244
245
246def _WriteFile(file_path, file_data):
247 """Write |file_data| to |file_path|."""
248 with open(file_path, 'w') as f:
249 f.write(file_data)
250
251
252def _FindGnExecutable():
253 """Locate the real GN executable used by this Chromium checkout.
254
255 This is needed because the depot_tools 'gn' wrapper script will look
256 for .gclient and other things we really don't need here.
257
258 Returns:
259 Path of real host GN executable from current Chromium src/ checkout.
260 """
261 # Simply scan buildtools/*/gn and return the first one found so we don't
262 # have to guess the platform-specific sub-directory name (e.g. 'linux64'
263 # for 64-bit Linux machines).
264 buildtools_dir = os.path.join(_TOP_SRC_DIR, 'buildtools')
265 for subdir in os.listdir(buildtools_dir):
266 subdir_path = os.path.join(buildtools_dir, subdir)
267 if not os.path.isdir(subdir_path):
268 continue
269 gn_path = os.path.join(subdir_path, 'gn')
270 if os.path.exists(gn_path):
271 return gn_path
272 return None
273
274
David 'Digit' Turner6e6f63f42019-04-15 22:02:27275def _PrettyPrintListAsLines(input_list, available_width, trailing_comma=False):
276 result = []
277 input_str = ', '.join(input_list)
278 while len(input_str) > available_width:
279 pos = input_str.rfind(',', 0, available_width)
280 result.append(input_str[:pos + 1])
281 input_str = input_str[pos + 1:].lstrip()
282 if trailing_comma and input_str:
283 input_str += ','
284 result.append(input_str)
285 return result
286
287
288class _PrettyPrintListAsLinesTest(unittest.TestCase):
289
290 def test_empty_list(self):
291 self.assertListEqual([''], _PrettyPrintListAsLines([], 10))
292
293 def test_wrapping(self):
294 input_list = ['foo', 'bar', 'zoo', 'tool']
295 self.assertListEqual(
296 _PrettyPrintListAsLines(input_list, 8),
297 ['foo,', 'bar,', 'zoo,', 'tool'])
298 self.assertListEqual(
299 _PrettyPrintListAsLines(input_list, 12), ['foo, bar,', 'zoo, tool'])
300 self.assertListEqual(
301 _PrettyPrintListAsLines(input_list, 79), ['foo, bar, zoo, tool'])
302
303 def test_trailing_comma(self):
304 input_list = ['foo', 'bar', 'zoo', 'tool']
305 self.assertListEqual(
306 _PrettyPrintListAsLines(input_list, 8, trailing_comma=True),
307 ['foo,', 'bar,', 'zoo,', 'tool,'])
308 self.assertListEqual(
309 _PrettyPrintListAsLines(input_list, 12, trailing_comma=True),
310 ['foo, bar,', 'zoo, tool,'])
311 self.assertListEqual(
312 _PrettyPrintListAsLines(input_list, 79, trailing_comma=True),
313 ['foo, bar, zoo, tool,'])
314
315
David 'Digit' Turner00b5f272019-04-10 21:15:27316##########################################################################
317##########################################################################
318#####
319##### L O C A L E S L I S T S
320#####
321##########################################################################
322##########################################################################
323
324# Various list of locales that will be extracted from build/config/locales.gni
325# Do not use these directly, use ChromeLocales(), AndroidOmittedLocales() and
326# IosUnsupportedLocales() instead to access these lists.
327_INTERNAL_CHROME_LOCALES = []
328_INTERNAL_ANDROID_OMITTED_LOCALES = []
329_INTERNAL_IOS_UNSUPPORTED_LOCALES = []
330
331
332def ChromeLocales():
333 """Return the list of all locales supported by Chrome."""
334 if not _INTERNAL_CHROME_LOCALES:
335 _ExtractAllChromeLocalesLists()
336 return _INTERNAL_CHROME_LOCALES
337
David 'Digit' Turner6e6f63f42019-04-15 22:02:27338
David 'Digit' Turner00b5f272019-04-10 21:15:27339def AndroidOmittedLocales():
340 """Reutrn the list of locales omitted from Android APKs."""
341 if not _INTERNAL_ANDROID_OMITTED_LOCALES:
342 _ExtractAllChromeLocalesLists()
343 return _INTERNAL_ANDROID_OMITTED_LOCALES
344
345
346def IosUnsupportedLocales():
347 """Return the list of locales that are unsupported on iOS."""
348 if not _INTERNAL_IOS_UNSUPPORTED_LOCALES:
349 _ExtractAllChromeLocalesLists()
350 return _INTERNAL_IOS_UNSUPPORTED_LOCALES
351
352
353def _PrepareTinyGnWorkspace(work_dir, out_subdir_name='out'):
354 """Populate an empty directory with a tiny set of working GN config files.
355
356 This allows us to run 'gn gen <out> --root <work_dir>' as fast as possible
357 to generate files containing the locales list. This takes about 300ms on
358 a decent machine, instead of more than 5 seconds when running the equivalent
359 commands from a real Chromium workspace, which requires regenerating more
360 than 23k targets.
361
362 Args:
363 work_dir: target working directory.
364 out_subdir_name: Name of output sub-directory.
365 Returns:
366 Full path of output directory created inside |work_dir|.
367 """
368 # Create top-level .gn file that must point to the BUILDCONFIG.gn.
369 _WriteFile(os.path.join(work_dir, '.gn'),
370 'buildconfig = "//BUILDCONFIG.gn"\n')
371 # Create BUILDCONFIG.gn which must set a default toolchain. Also add
372 # all variables that may be used in locales.gni in a declare_args() block.
373 _WriteFile(os.path.join(work_dir, 'BUILDCONFIG.gn'),
374 r'''set_default_toolchain("toolchain")
375declare_args () {
376 is_ios = false
377}
378''')
379
380 # Create fake toolchain required by BUILDCONFIG.gn.
381 os.mkdir(os.path.join(work_dir, 'toolchain'))
382 _WriteFile(os.path.join(work_dir, 'toolchain', 'BUILD.gn'),
383 r'''toolchain("toolchain") {
384 tool("stamp") {
385 command = "touch {{output}}" # Required by action()
386 }
387}
388''')
389
390 # Create top-level BUILD.gn, GN requires at least one target to build so do
391 # that with a fake action which will never be invoked. Also write the locales
392 # to misc files in the output directory.
393 _WriteFile(os.path.join(work_dir, 'BUILD.gn'),
394 r'''import("//locales.gni")
395
396action("create_foo") { # fake action to avoid GN complaints.
397 script = "//build/create_foo.py"
398 inputs = []
399 outputs = [ "$target_out_dir/$target_name" ]
400}
401
402# Write the locales lists to files in the output directory.
403_filename = root_build_dir + "/foo"
404write_file(_filename + ".locales", locales, "json")
405write_file(_filename + ".android_omitted_locales",
406 android_chrome_omitted_locales,
407 "json")
408write_file(_filename + ".ios_unsupported_locales",
409 ios_unsupported_locales,
410 "json")
411''')
412
413 # Copy build/config/locales.gni to the workspace, as required by BUILD.gn.
414 shutil.copyfile(os.path.join(_TOP_SRC_DIR, 'build', 'config', 'locales.gni'),
415 os.path.join(work_dir, 'locales.gni'))
416
417 # Create output directory.
418 out_path = os.path.join(work_dir, out_subdir_name)
419 os.mkdir(out_path)
420
421 # And ... we're good.
422 return out_path
423
424
425# Set this global variable to the path of a given temporary directory
426# before calling _ExtractAllChromeLocalesLists() if you want to debug
427# the locales list extraction process.
428_DEBUG_LOCALES_WORK_DIR = None
429
430
431def _ReadJsonList(file_path):
432 """Read a JSON file that must contain a list, and return it."""
433 with open(file_path) as f:
434 data = json.load(f)
435 assert isinstance(data, list), "JSON file %s is not a list!" % file_path
David 'Digit' Turner5553d512019-04-15 08:54:05436 return [item.encode('utf8') for item in data]
David 'Digit' Turner00b5f272019-04-10 21:15:27437
438
439def _ExtractAllChromeLocalesLists():
440 with build_utils.TempDir() as tmp_path:
441 if _DEBUG_LOCALES_WORK_DIR:
442 tmp_path = _DEBUG_LOCALES_WORK_DIR
443 build_utils.DeleteDirectory(tmp_path)
444 build_utils.MakeDirectory(tmp_path)
445
446 out_path = _PrepareTinyGnWorkspace(tmp_path, 'out')
447
448 # NOTE: The file suffixes used here should be kept in sync with
449 # build/config/locales.gni
450 gn_executable = _FindGnExecutable()
451 subprocess.check_output(
452 [gn_executable, 'gen', out_path, '--root=' + tmp_path])
453
454 global _INTERNAL_CHROME_LOCALES
455 _INTERNAL_CHROME_LOCALES = _ReadJsonList(
456 os.path.join(out_path, 'foo.locales'))
457
458 global _INTERNAL_ANDROID_OMITTED_LOCALES
459 _INTERNAL_ANDROID_OMITTED_LOCALES = _ReadJsonList(
460 os.path.join(out_path, 'foo.android_omitted_locales'))
461
462 global _INTERNAL_IOS_UNSUPPORTED_LOCALES
463 _INTERNAL_IOS_UNSUPPORTED_LOCALES = _ReadJsonList(
464 os.path.join(out_path, 'foo.ios_unsupported_locales'))
465
David 'Digit' Turner5553d512019-04-15 08:54:05466
David 'Digit' Turner00b5f272019-04-10 21:15:27467##########################################################################
468##########################################################################
469#####
470##### G R D H E L P E R F U N C T I O N S
471#####
472##########################################################################
473##########################################################################
474
475# Technical note:
476#
477# Even though .grd files are XML, an xml parser library is not used in order
478# to preserve the original file's structure after modification. ElementTree
479# tends to re-order attributes in each element when re-writing an XML
480# document tree, which is undesirable here.
481#
482# Thus simple line-based regular expression matching is used instead.
483#
484
485# Misc regular expressions used to match elements and their attributes.
486_RE_OUTPUT_ELEMENT = re.compile(r'<output (.*)\s*/>')
David 'Digit' Turner4d88f202019-04-18 22:11:28487_RE_TRANSLATION_ELEMENT = re.compile(r'<file( | .* )path="(.*\.xtb)".*/>')
David 'Digit' Turner00b5f272019-04-10 21:15:27488_RE_FILENAME_ATTRIBUTE = re.compile(r'filename="([^"]*)"')
489_RE_LANG_ATTRIBUTE = re.compile(r'lang="([^"]*)"')
490_RE_PATH_ATTRIBUTE = re.compile(r'path="([^"]*)"')
491_RE_TYPE_ANDROID_ATTRIBUTE = re.compile(r'type="android"')
492
David 'Digit' Turner00b5f272019-04-10 21:15:27493
494
495def _IsGritInputFile(input_file):
496 """Returns True iff this is a GRIT input file."""
497 return input_file.endswith('.grd')
498
499
David 'Digit' Turner4d88f202019-04-18 22:11:28500def _GetXmlLangAttribute(xml_line):
501 """Extract the lang attribute value from an XML input line."""
502 m = _RE_LANG_ATTRIBUTE.search(xml_line)
503 if not m:
504 return None
505 return m.group(1)
506
507
508class _GetXmlLangAttributeTest(unittest.TestCase):
509 TEST_DATA = {
510 '': None,
511 'foo': None,
512 'lang=foo': None,
513 'lang="foo"': 'foo',
514 '<something lang="foo bar" />': 'foo bar',
515 '<file lang="fr-CA" path="path/to/strings_fr-CA.xtb" />': 'fr-CA',
516 }
517
518 def test_GetXmlLangAttribute(self):
519 for test_line, expected in self.TEST_DATA.iteritems():
520 self.assertEquals(_GetXmlLangAttribute(test_line), expected)
521
522
David 'Digit' Turner00b5f272019-04-10 21:15:27523def _SortGrdElementsRanges(grd_lines, element_predicate):
524 """Sort all .grd elements of a given type by their lang attribute."""
David 'Digit' Turner4d88f202019-04-18 22:11:28525 return _SortElementsRanges(grd_lines, element_predicate, _GetXmlLangAttribute)
David 'Digit' Turner00b5f272019-04-10 21:15:27526
527
528def _CheckGrdElementRangeLang(grd_lines, start, end, wanted_locales):
529 """Check the element 'lang' attributes in specific .grd lines range.
530
531 This really checks the following:
532 - Each item has a correct 'lang' attribute.
533 - There are no duplicated lines for the same 'lang' attribute.
534 - That there are no extra locales that Chromium doesn't want.
535 - That no wanted locale is missing.
536
537 Args:
538 grd_lines: Input .grd lines.
539 start: Sub-range start position in input line list.
540 end: Sub-range limit position in input line list.
541 wanted_locales: Set of wanted Chromium locale names.
542 Returns:
543 List of error message strings for this input. Empty on success.
544 """
545 errors = []
546 locales = set()
547 for pos in xrange(start, end):
548 line = grd_lines[pos]
David 'Digit' Turner4d88f202019-04-18 22:11:28549 lang = _GetXmlLangAttribute(line)
550 if not lang:
David 'Digit' Turner00b5f272019-04-10 21:15:27551 errors.append('%d: Missing "lang" attribute in <output> element' % pos +
552 1)
553 continue
David 'Digit' Turner00b5f272019-04-10 21:15:27554 cr_locale = _FixChromiumLangAttribute(lang)
555 if cr_locale in locales:
556 errors.append(
557 '%d: Redefinition of <output> for "%s" locale' % (pos + 1, lang))
558 locales.add(cr_locale)
559
560 extra_locales = locales.difference(wanted_locales)
561 if extra_locales:
562 errors.append('%d-%d: Extra locales found: %s' % (start + 1, end + 1,
563 sorted(extra_locales)))
564
565 missing_locales = wanted_locales.difference(locales)
566 if missing_locales:
567 errors.append('%d-%d: Missing locales: %s' % (start + 1, end + 1,
568 sorted(missing_locales)))
569
570 return errors
571
572
573##########################################################################
574##########################################################################
575#####
576##### G R D A N D R O I D O U T P U T S
577#####
578##########################################################################
579##########################################################################
580
581def _IsGrdAndroidOutputLine(line):
582 """Returns True iff this is an Android-specific <output> line."""
583 m = _RE_OUTPUT_ELEMENT.search(line)
584 if m:
585 return 'type="android"' in m.group(1)
586 return False
587
588assert _IsGrdAndroidOutputLine(' <output type="android"/>')
589
590# Many of the functions below have unused arguments due to genericity.
591# pylint: disable=unused-argument
592
593def _CheckGrdElementRangeAndroidOutputFilename(grd_lines, start, end,
594 wanted_locales):
595 """Check all <output> elements in specific input .grd lines range.
596
597 This really checks the following:
598 - Filenames exist for each listed locale.
599 - Filenames are well-formed.
600
601 Args:
602 grd_lines: Input .grd lines.
603 start: Sub-range start position in input line list.
604 end: Sub-range limit position in input line list.
605 wanted_locales: Set of wanted Chromium locale names.
606 Returns:
607 List of error message strings for this input. Empty on success.
608 """
609 errors = []
610 for pos in xrange(start, end):
611 line = grd_lines[pos]
David 'Digit' Turner4d88f202019-04-18 22:11:28612 lang = _GetXmlLangAttribute(line)
613 if not lang:
David 'Digit' Turner00b5f272019-04-10 21:15:27614 continue
David 'Digit' Turner00b5f272019-04-10 21:15:27615 cr_locale = _FixChromiumLangAttribute(lang)
616
617 m = _RE_FILENAME_ATTRIBUTE.search(line)
618 if not m:
619 errors.append('%d: Missing filename attribute in <output> element' % pos +
620 1)
621 else:
622 filename = m.group(1)
623 if not filename.endswith('.xml'):
624 errors.append(
625 '%d: Filename should end with ".xml": %s' % (pos + 1, filename))
626
627 dirname = os.path.basename(os.path.dirname(filename))
628 prefix = ('values-%s' % resource_utils.ToAndroidLocaleName(cr_locale)
629 if cr_locale != _DEFAULT_LOCALE else 'values')
630 if dirname != prefix:
631 errors.append(
632 '%s: Directory name should be %s: %s' % (pos + 1, prefix, filename))
633
634 return errors
635
636
637def _CheckGrdAndroidOutputElements(grd_file, grd_lines, wanted_locales):
638 """Check all <output> elements related to Android.
639
640 Args:
641 grd_file: Input .grd file path.
642 grd_lines: List of input .grd lines.
643 wanted_locales: set of wanted Chromium locale names.
644 Returns:
645 List of error message strings. Empty on success.
646 """
647 intervals = _BuildIntervalList(grd_lines, _IsGrdAndroidOutputLine)
648 errors = []
649 for start, end in intervals:
650 errors += _CheckGrdElementRangeLang(grd_lines, start, end, wanted_locales)
651 errors += _CheckGrdElementRangeAndroidOutputFilename(grd_lines, start, end,
652 wanted_locales)
653 return errors
654
655
656def _AddMissingLocalesInGrdAndroidOutputs(grd_file, grd_lines, wanted_locales):
657 """Fix an input .grd line by adding missing Android outputs.
658
659 Args:
660 grd_file: Input .grd file path.
661 grd_lines: Input .grd line list.
662 wanted_locales: set of Chromium locale names.
663 Returns:
664 A new list of .grd lines, containing new <output> elements when needed
665 for locales from |wanted_locales| that were not part of the input.
666 """
667 intervals = _BuildIntervalList(grd_lines, _IsGrdAndroidOutputLine)
668 for start, end in reversed(intervals):
669 locales = set()
670 for pos in xrange(start, end):
David 'Digit' Turner4d88f202019-04-18 22:11:28671 lang = _GetXmlLangAttribute(grd_lines[pos])
David 'Digit' Turner00b5f272019-04-10 21:15:27672 locale = _FixChromiumLangAttribute(lang)
673 locales.add(locale)
674
675 missing_locales = wanted_locales.difference(locales)
676 if not missing_locales:
677 continue
678
679 src_locale = 'bg'
680 src_lang_attribute = 'lang="%s"' % src_locale
681 src_line = None
682 for pos in xrange(start, end):
683 if src_lang_attribute in grd_lines[pos]:
684 src_line = grd_lines[pos]
685 break
686
687 if not src_line:
688 raise Exception(
689 'Cannot find <output> element with "%s" lang attribute' % src_locale)
690
691 line_count = end - 1
692 for locale in missing_locales:
693 android_locale = resource_utils.ToAndroidLocaleName(locale)
694 dst_line = src_line.replace(
695 'lang="%s"' % src_locale, 'lang="%s"' % locale).replace(
696 'values-%s/' % src_locale, 'values-%s/' % android_locale)
697 grd_lines.insert(line_count, dst_line)
698 line_count += 1
699
700 # Sort the new <output> elements.
701 return _SortGrdElementsRanges(grd_lines, _IsGrdAndroidOutputLine)
702
703
704##########################################################################
705##########################################################################
706#####
707##### G R D T R A N S L A T I O N S
708#####
709##########################################################################
710##########################################################################
711
712
713def _IsTranslationGrdOutputLine(line):
714 """Returns True iff this is an output .xtb <file> element."""
715 m = _RE_TRANSLATION_ELEMENT.search(line)
716 return m is not None
717
718
David 'Digit' Turner4d88f202019-04-18 22:11:28719class _IsTranslationGrdOutputLineTest(unittest.TestCase):
720
721 def test_GrdTranslationOutputLines(self):
722 _VALID_INPUT_LINES = [
723 '<file path="foo/bar.xtb" />',
724 '<file path="foo/bar.xtb"/>',
725 '<file lang="fr-CA" path="translations/aw_strings_fr-CA.xtb"/>',
726 '<file lang="fr-CA" path="translations/aw_strings_fr-CA.xtb" />',
727 ' <file path="translations/aw_strings_ar.xtb" lang="ar" />',
728 ]
729 _INVALID_INPUT_LINES = ['<file path="foo/bar.xml" />']
730
731 for line in _VALID_INPUT_LINES:
732 self.assertTrue(
733 _IsTranslationGrdOutputLine(line),
734 '_IsTranslationGrdOutputLine() returned False for [%s]' % line)
735
736 for line in _INVALID_INPUT_LINES:
737 self.assertFalse(
738 _IsTranslationGrdOutputLine(line),
739 '_IsTranslationGrdOutputLine() returned True for [%s]' % line)
740
741
David 'Digit' Turner00b5f272019-04-10 21:15:27742def _CheckGrdTranslationElementRange(grd_lines, start, end,
743 wanted_locales):
744 """Check all <translations> sub-elements in specific input .grd lines range.
745
746 This really checks the following:
747 - Each item has a 'path' attribute.
748 - Each such path value ends up with '.xtb'.
749
750 Args:
751 grd_lines: Input .grd lines.
752 start: Sub-range start position in input line list.
753 end: Sub-range limit position in input line list.
754 wanted_locales: Set of wanted Chromium locale names.
755 Returns:
756 List of error message strings for this input. Empty on success.
757 """
758 errors = []
759 for pos in xrange(start, end):
760 line = grd_lines[pos]
David 'Digit' Turner4d88f202019-04-18 22:11:28761 lang = _GetXmlLangAttribute(line)
762 if not lang:
David 'Digit' Turner00b5f272019-04-10 21:15:27763 continue
764 m = _RE_PATH_ATTRIBUTE.search(line)
765 if not m:
766 errors.append('%d: Missing path attribute in <file> element' % pos +
767 1)
768 else:
769 filename = m.group(1)
770 if not filename.endswith('.xtb'):
771 errors.append(
772 '%d: Path should end with ".xtb": %s' % (pos + 1, filename))
773
774 return errors
775
776
777def _CheckGrdTranslations(grd_file, grd_lines, wanted_locales):
778 """Check all <file> elements that correspond to an .xtb output file.
779
780 Args:
781 grd_file: Input .grd file path.
782 grd_lines: List of input .grd lines.
783 wanted_locales: set of wanted Chromium locale names.
784 Returns:
785 List of error message strings. Empty on success.
786 """
787 wanted_locales = wanted_locales - set([_DEFAULT_LOCALE])
788 intervals = _BuildIntervalList(grd_lines, _IsTranslationGrdOutputLine)
789 errors = []
790 for start, end in intervals:
791 errors += _CheckGrdElementRangeLang(grd_lines, start, end, wanted_locales)
792 errors += _CheckGrdTranslationElementRange(grd_lines, start, end,
793 wanted_locales)
794 return errors
795
796
David 'Digit' Turner5553d512019-04-15 08:54:05797# Regular expression used to replace the lang attribute inside .xtb files.
798_RE_TRANSLATIONBUNDLE = re.compile('<translationbundle lang="(.*)">')
799
800
801def _CreateFakeXtbFileFrom(src_xtb_path, dst_xtb_path, dst_locale):
802 """Create a fake .xtb file.
803
804 Args:
805 src_xtb_path: Path to source .xtb file to copy from.
806 dst_xtb_path: Path to destination .xtb file to write to.
807 dst_locale: Destination locale, the lang attribute in the source file
808 will be substituted with this value before its lines are written
809 to the destination file.
810 """
811 with open(src_xtb_path) as f:
812 src_xtb_lines = f.readlines()
813
814 def replace_xtb_lang_attribute(line):
815 m = _RE_TRANSLATIONBUNDLE.search(line)
816 if not m:
817 return line
818 return line[:m.start(1)] + dst_locale + line[m.end(1):]
819
820 dst_xtb_lines = [replace_xtb_lang_attribute(line) for line in src_xtb_lines]
821 with build_utils.AtomicOutput(dst_xtb_path) as tmp:
822 tmp.writelines(dst_xtb_lines)
823
824
David 'Digit' Turner00b5f272019-04-10 21:15:27825def _AddMissingLocalesInGrdTranslations(grd_file, grd_lines, wanted_locales):
826 """Fix an input .grd line by adding missing Android outputs.
827
David 'Digit' Turner5553d512019-04-15 08:54:05828 This also creates fake .xtb files from the one provided for 'en-GB'.
829
David 'Digit' Turner00b5f272019-04-10 21:15:27830 Args:
831 grd_file: Input .grd file path.
832 grd_lines: Input .grd line list.
833 wanted_locales: set of Chromium locale names.
834 Returns:
835 A new list of .grd lines, containing new <output> elements when needed
836 for locales from |wanted_locales| that were not part of the input.
837 """
838 wanted_locales = wanted_locales - set([_DEFAULT_LOCALE])
839 intervals = _BuildIntervalList(grd_lines, _IsTranslationGrdOutputLine)
840 for start, end in reversed(intervals):
841 locales = set()
842 for pos in xrange(start, end):
David 'Digit' Turner4d88f202019-04-18 22:11:28843 lang = _GetXmlLangAttribute(grd_lines[pos])
David 'Digit' Turner00b5f272019-04-10 21:15:27844 locale = _FixChromiumLangAttribute(lang)
845 locales.add(locale)
846
847 missing_locales = wanted_locales.difference(locales)
848 if not missing_locales:
849 continue
850
David 'Digit' Turner5553d512019-04-15 08:54:05851 src_locale = 'en-GB'
David 'Digit' Turner00b5f272019-04-10 21:15:27852 src_lang_attribute = 'lang="%s"' % src_locale
853 src_line = None
854 for pos in xrange(start, end):
855 if src_lang_attribute in grd_lines[pos]:
856 src_line = grd_lines[pos]
857 break
858
859 if not src_line:
860 raise Exception(
861 'Cannot find <file> element with "%s" lang attribute' % src_locale)
862
David 'Digit' Turner5553d512019-04-15 08:54:05863 src_path = os.path.join(
864 os.path.dirname(grd_file),
865 _RE_PATH_ATTRIBUTE.search(src_line).group(1))
866
David 'Digit' Turner00b5f272019-04-10 21:15:27867 line_count = end - 1
868 for locale in missing_locales:
869 dst_line = src_line.replace(
870 'lang="%s"' % src_locale, 'lang="%s"' % locale).replace(
871 '_%s.xtb' % src_locale, '_%s.xtb' % locale)
872 grd_lines.insert(line_count, dst_line)
873 line_count += 1
874
David 'Digit' Turner5553d512019-04-15 08:54:05875 dst_path = src_path.replace('_%s.xtb' % src_locale, '_%s.xtb' % locale)
876 _CreateFakeXtbFileFrom(src_path, dst_path, locale)
877
878
David 'Digit' Turner00b5f272019-04-10 21:15:27879 # Sort the new <output> elements.
880 return _SortGrdElementsRanges(grd_lines, _IsTranslationGrdOutputLine)
881
882
883##########################################################################
884##########################################################################
885#####
886##### G N A N D R O I D O U T P U T S
887#####
888##########################################################################
889##########################################################################
890
891_RE_GN_VALUES_LIST_LINE = re.compile(
892 r'^\s*".*values(\-([A-Za-z0-9-]+))?/.*\.xml",\s*$')
893
894def _IsBuildGnInputFile(input_file):
895 """Returns True iff this is a BUILD.gn file."""
896 return os.path.basename(input_file) == 'BUILD.gn'
897
898
899def _GetAndroidGnOutputLocale(line):
900 """Check a GN list, and return its Android locale if it is an output .xml"""
901 m = _RE_GN_VALUES_LIST_LINE.match(line)
902 if not m:
903 return None
904
905 if m.group(1): # First group is optional and contains group 2.
906 return m.group(2)
907
908 return resource_utils.ToAndroidLocaleName(_DEFAULT_LOCALE)
909
910
911def _IsAndroidGnOutputLine(line):
912 """Returns True iff this is an Android-specific localized .xml output."""
913 return _GetAndroidGnOutputLocale(line) != None
914
915
916def _CheckGnOutputsRangeForLocalizedStrings(gn_lines, start, end):
917 """Check that a range of GN lines corresponds to localized strings.
918
919 Special case: Some BUILD.gn files list several non-localized .xml files
920 that should be ignored by this function, e.g. in
921 components/cronet/android/BUILD.gn, the following appears:
922
923 inputs = [
924 ...
925 "sample/res/layout/activity_main.xml",
926 "sample/res/layout/dialog_url.xml",
927 "sample/res/values/dimens.xml",
928 "sample/res/values/strings.xml",
929 ...
930 ]
931
932 These are non-localized strings, and should be ignored. This function is
933 used to detect them quickly.
934 """
935 for pos in xrange(start, end):
936 if not 'values/' in gn_lines[pos]:
937 return True
938 return False
939
940
941def _CheckGnOutputsRange(gn_lines, start, end, wanted_locales):
942 if not _CheckGnOutputsRangeForLocalizedStrings(gn_lines, start, end):
943 return []
944
945 errors = []
946 locales = set()
947 for pos in xrange(start, end):
948 line = gn_lines[pos]
949 android_locale = _GetAndroidGnOutputLocale(line)
950 assert android_locale != None
951 cr_locale = resource_utils.ToChromiumLocaleName(android_locale)
952 if cr_locale in locales:
953 errors.append('%s: Redefinition of output for "%s" locale' %
954 (pos + 1, android_locale))
955 locales.add(cr_locale)
956
957 extra_locales = locales.difference(wanted_locales)
958 if extra_locales:
959 errors.append('%d-%d: Extra locales: %s' % (start + 1, end + 1,
960 sorted(extra_locales)))
961
962 missing_locales = wanted_locales.difference(locales)
963 if missing_locales:
964 errors.append('%d-%d: Missing locales: %s' % (start + 1, end + 1,
965 sorted(missing_locales)))
966
967 return errors
968
969
970def _CheckGnAndroidOutputs(gn_file, gn_lines, wanted_locales):
971 intervals = _BuildIntervalList(gn_lines, _IsAndroidGnOutputLine)
972 errors = []
973 for start, end in intervals:
974 errors += _CheckGnOutputsRange(gn_lines, start, end, wanted_locales)
975 return errors
976
977
978def _AddMissingLocalesInGnAndroidOutputs(gn_file, gn_lines, wanted_locales):
979 intervals = _BuildIntervalList(gn_lines, _IsAndroidGnOutputLine)
980 # NOTE: Since this may insert new lines to each interval, process the
981 # list in reverse order to maintain valid (start,end) positions during
982 # the iteration.
983 for start, end in reversed(intervals):
984 if not _CheckGnOutputsRangeForLocalizedStrings(gn_lines, start, end):
985 continue
986
987 locales = set()
988 for pos in xrange(start, end):
989 lang = _GetAndroidGnOutputLocale(gn_lines[pos])
990 locale = resource_utils.ToChromiumLocaleName(lang)
991 locales.add(locale)
992
993 missing_locales = wanted_locales.difference(locales)
994 if not missing_locales:
995 continue
996
997 src_locale = 'bg'
998 src_values = 'values-%s/' % resource_utils.ToAndroidLocaleName(src_locale)
999 src_line = None
1000 for pos in xrange(start, end):
1001 if src_values in gn_lines[pos]:
1002 src_line = gn_lines[pos]
1003 break
1004
1005 if not src_line:
1006 raise Exception(
1007 'Cannot find output list item with "%s" locale' % src_locale)
1008
1009 line_count = end - 1
1010 for locale in missing_locales:
1011 if locale == _DEFAULT_LOCALE:
1012 dst_line = src_line.replace('values-%s/' % src_locale, 'values/')
1013 else:
1014 dst_line = src_line.replace(
1015 'values-%s/' % src_locale,
1016 'values-%s/' % resource_utils.ToAndroidLocaleName(locale))
1017 gn_lines.insert(line_count, dst_line)
1018 line_count += 1
1019
1020 gn_lines = _SortListSubRange(
1021 gn_lines, start, line_count,
1022 lambda line: _RE_GN_VALUES_LIST_LINE.match(line).group(1))
1023
1024 return gn_lines
1025
1026
David 'Digit' Turner6e6f63f42019-04-15 22:02:271027##########################################################################
1028##########################################################################
1029#####
1030##### T R A N S L A T I O N E X P E C T A T I O N S
1031#####
1032##########################################################################
1033##########################################################################
1034
1035_EXPECTATIONS_FILENAME = 'translation_expectations.pyl'
1036
1037# Technical note: the format of translation_expectations.pyl
1038# is a 'Python literal', which defines a python dictionary, so should
1039# be easy to parse. However, when modifying it, care should be taken
1040# to respect the line comments and the order of keys within the text
1041# file.
1042
1043
1044def _ReadPythonLiteralFile(pyl_path):
1045 """Read a .pyl file into a Python data structure."""
1046 with open(pyl_path) as f:
1047 pyl_content = f.read()
1048 # Evaluate as a Python data structure, use an empty global
1049 # and local dictionary.
1050 return eval(pyl_content, dict(), dict())
1051
1052
1053def _UpdateLocalesInExpectationLines(pyl_lines,
1054 wanted_locales,
1055 available_width=79):
1056 """Update the locales list(s) found in an expectations file.
1057
1058 Args:
1059 pyl_lines: Iterable of input lines from the file.
1060 wanted_locales: Set or list of new locale names.
1061 available_width: Optional, number of character colums used
1062 to word-wrap the new list items.
1063 Returns:
1064 New list of updated lines.
1065 """
1066 locales_list = ['"%s"' % loc for loc in sorted(wanted_locales)]
1067 result = []
1068 line_count = len(pyl_lines)
1069 line_num = 0
1070 DICT_START = '"languages": ['
1071 while line_num < line_count:
1072 line = pyl_lines[line_num]
1073 line_num += 1
1074 result.append(line)
1075 # Look for start of "languages" dictionary.
1076 pos = line.find(DICT_START)
1077 if pos < 0:
1078 continue
1079
1080 start_margin = pos
1081 start_line = line_num
1082 # Skip over all lines from the list.
1083 while (line_num < line_count and
1084 not pyl_lines[line_num].rstrip().endswith('],')):
1085 line_num += 1
1086 continue
1087
1088 if line_num == line_count:
1089 raise Exception('%d: Missing list termination!' % start_line)
1090
1091 # Format the new list according to the new margin.
1092 locale_width = available_width - (start_margin + 2)
1093 locale_lines = _PrettyPrintListAsLines(
1094 locales_list, locale_width, trailing_comma=True)
1095 for locale_line in locale_lines:
1096 result.append(' ' * (start_margin + 2) + locale_line)
1097 result.append(' ' * start_margin + '],')
1098 line_num += 1
1099
1100 return result
1101
1102
1103class _UpdateLocalesInExpectationLinesTest(unittest.TestCase):
1104
1105 def test_simple(self):
1106 self.maxDiff = 1000
1107 input_text = r'''
1108# This comment should be preserved
1109# 23456789012345678901234567890123456789
1110{
1111 "android_grd": {
1112 "languages": [
1113 "aa", "bb", "cc", "dd", "ee",
1114 "ff", "gg", "hh", "ii", "jj",
1115 "kk"],
1116 },
1117 # Example with bad indentation in input.
1118 "another_grd": {
1119 "languages": [
1120 "aa", "bb", "cc", "dd", "ee", "ff", "gg", "hh", "ii", "jj", "kk",
1121 ],
1122 },
1123}
1124'''
1125 expected_text = r'''
1126# This comment should be preserved
1127# 23456789012345678901234567890123456789
1128{
1129 "android_grd": {
1130 "languages": [
1131 "A2", "AA", "BB", "CC", "DD",
1132 "E2", "EE", "FF", "GG", "HH",
1133 "I2", "II", "JJ", "KK",
1134 ],
1135 },
1136 # Example with bad indentation in input.
1137 "another_grd": {
1138 "languages": [
1139 "A2", "AA", "BB", "CC", "DD",
1140 "E2", "EE", "FF", "GG", "HH",
1141 "I2", "II", "JJ", "KK",
1142 ],
1143 },
1144}
1145'''
1146 input_lines = input_text.splitlines()
1147 test_locales = ([
1148 'AA', 'BB', 'CC', 'DD', 'EE', 'FF', 'GG', 'HH', 'II', 'JJ', 'KK', 'A2',
1149 'E2', 'I2'
1150 ])
1151 expected_lines = expected_text.splitlines()
1152 self.assertListEqual(
1153 _UpdateLocalesInExpectationLines(input_lines, test_locales, 40),
1154 expected_lines)
1155
1156 def test_missing_list_termination(self):
1157 input_lines = r'''
1158 "languages": ['
1159 "aa", "bb", "cc", "dd"
1160'''.splitlines()
1161 with self.assertRaises(Exception) as cm:
1162 _UpdateLocalesInExpectationLines(input_lines, ['a', 'b'], 40)
1163
1164 self.assertEqual(str(cm.exception), '2: Missing list termination!')
1165
1166
1167def _UpdateLocalesInExpectationFile(pyl_path, wanted_locales):
1168 """Update all locales listed in a given expectations file.
1169
1170 Args:
1171 pyl_path: Path to .pyl file to update.
1172 wanted_locales: List of locales that need to be written to
1173 the file.
1174 """
1175 tc_locales = {
1176 _FixTranslationConsoleLocaleName(locale)
1177 for locale in set(wanted_locales) - set([_DEFAULT_LOCALE])
1178 }
1179
1180 with open(pyl_path) as f:
1181 input_lines = [l.rstrip() for l in f.readlines()]
1182
1183 updated_lines = _UpdateLocalesInExpectationLines(input_lines, tc_locales)
David 'Digit' Turner6e6f63f42019-04-15 22:02:271184 with build_utils.AtomicOutput(pyl_path) as f:
1185 f.writelines('\n'.join(updated_lines) + '\n')
1186
David 'Digit' Turner00b5f272019-04-10 21:15:271187
1188##########################################################################
1189##########################################################################
1190#####
1191##### C H E C K E V E R Y T H I N G
1192#####
1193##########################################################################
1194##########################################################################
1195
David 'Digit' Turner6e6f63f42019-04-15 22:02:271196# pylint: enable=unused-argument
1197
1198
David 'Digit' Turner00b5f272019-04-10 21:15:271199def _IsAllInputFile(input_file):
1200 return _IsGritInputFile(input_file) or _IsBuildGnInputFile(input_file)
1201
1202
1203def _CheckAllFiles(input_file, input_lines, wanted_locales):
1204 errors = []
1205 if _IsGritInputFile(input_file):
1206 errors += _CheckGrdTranslations(input_file, input_lines, wanted_locales)
1207 errors += _CheckGrdAndroidOutputElements(
1208 input_file, input_lines, wanted_locales)
1209 elif _IsBuildGnInputFile(input_file):
1210 errors += _CheckGnAndroidOutputs(input_file, input_lines, wanted_locales)
1211 return errors
1212
1213
1214def _AddMissingLocalesInAllFiles(input_file, input_lines, wanted_locales):
1215 if _IsGritInputFile(input_file):
1216 lines = _AddMissingLocalesInGrdTranslations(
1217 input_file, input_lines, wanted_locales)
1218 lines = _AddMissingLocalesInGrdAndroidOutputs(
1219 input_file, lines, wanted_locales)
1220 elif _IsBuildGnInputFile(input_file):
1221 lines = _AddMissingLocalesInGnAndroidOutputs(
1222 input_file, input_lines, wanted_locales)
1223 return lines
1224
David 'Digit' Turner5553d512019-04-15 08:54:051225
David 'Digit' Turner00b5f272019-04-10 21:15:271226##########################################################################
1227##########################################################################
1228#####
1229##### C O M M A N D H A N D L I N G
1230#####
1231##########################################################################
1232##########################################################################
1233
1234class _Command(object):
1235 """A base class for all commands recognized by this script.
1236
1237 Usage is the following:
1238 1) Derived classes must re-define the following class-based fields:
1239 - name: Command name (e.g. 'list-locales')
1240 - description: Command short description.
1241 - long_description: Optional. Command long description.
1242 NOTE: As a convenience, if the first character is a newline,
1243 it will be omitted in the help output.
1244
1245 2) Derived classes for commands that take arguments should override
1246 RegisterExtraArgs(), which receives a corresponding argparse
1247 sub-parser as argument.
1248
1249 3) Derived classes should implement a Run() command, which can read
1250 the current arguments from self.args.
1251 """
1252 name = None
1253 description = None
1254 long_description = None
1255
1256 def __init__(self):
1257 self._parser = None
1258 self.args = None
1259
1260 def RegisterExtraArgs(self, subparser):
1261 pass
1262
1263 def RegisterArgs(self, parser):
1264 subp = parser.add_parser(
1265 self.name, help=self.description,
1266 description=self.long_description or self.description,
1267 formatter_class=argparse.RawDescriptionHelpFormatter)
1268 self._parser = subp
1269 subp.set_defaults(command=self)
1270 group = subp.add_argument_group('%s arguments' % self.name)
1271 self.RegisterExtraArgs(group)
1272
1273 def ProcessArgs(self, args):
1274 self.args = args
1275
1276
1277class _ListLocalesCommand(_Command):
1278 """Implement the 'list-locales' command to list locale lists of interest."""
1279 name = 'list-locales'
1280 description = 'List supported Chrome locales'
1281 long_description = r'''
1282List locales of interest, by default this prints all locales supported by
1283Chrome, but `--type=android_omitted` can be used to print the list of locales
1284omitted from Android APKs (but not app bundles), and `--type=ios_unsupported`
1285for the list of locales unsupported on iOS.
1286
1287These values are extracted directly from build/config/locales.gni.
1288
1289Additionally, use the --as-json argument to print the list as a JSON list,
1290instead of the default format (which is a space-separated list of locale names).
1291'''
1292
1293 # Maps type argument to a function returning the corresponding locales list.
1294 TYPE_MAP = {
1295 'all': ChromeLocales,
1296 'android_omitted': AndroidOmittedLocales,
1297 'ios_unsupported': IosUnsupportedLocales,
1298 }
1299
1300 def RegisterExtraArgs(self, group):
1301 group.add_argument(
1302 '--as-json',
1303 action='store_true',
1304 help='Output as JSON list.')
1305 group.add_argument(
1306 '--type',
1307 choices=tuple(self.TYPE_MAP.viewkeys()),
1308 default='all',
1309 help='Select type of locale list to print.')
1310
1311 def Run(self):
1312 locale_list = self.TYPE_MAP[self.args.type]()
1313 if self.args.as_json:
Raul Tambre9e24293b2019-05-12 06:11:071314 print('[%s]' % ", ".join("'%s'" % loc for loc in locale_list))
David 'Digit' Turner00b5f272019-04-10 21:15:271315 else:
Raul Tambre9e24293b2019-05-12 06:11:071316 print(' '.join(locale_list))
David 'Digit' Turner00b5f272019-04-10 21:15:271317
1318
1319class _CheckInputFileBaseCommand(_Command):
1320 """Used as a base for other _Command subclasses that check input files.
1321
1322 Subclasses should also define the following class-level variables:
1323
1324 - select_file_func:
1325 A predicate that receives a file name (not path) and return True if it
1326 should be selected for inspection. Used when scanning directories with
1327 '--scan-dir <dir>'.
1328
1329 - check_func:
1330 - fix_func:
1331 Two functions passed as parameters to _ProcessFile(), see relevant
1332 documentation in this function's definition.
1333 """
1334 select_file_func = None
1335 check_func = None
1336 fix_func = None
1337
1338 def RegisterExtraArgs(self, group):
1339 group.add_argument(
1340 '--scan-dir',
1341 action='append',
1342 help='Optional directory to scan for input files recursively.')
1343 group.add_argument(
1344 'input',
1345 nargs='*',
1346 help='Input file(s) to check.')
1347 group.add_argument(
1348 '--fix-inplace',
1349 action='store_true',
1350 help='Try to fix the files in-place too.')
1351 group.add_argument(
1352 '--add-locales',
1353 help='Space-separated list of additional locales to use')
1354
1355 def Run(self):
1356 args = self.args
1357 input_files = []
1358 if args.input:
1359 input_files = args.input
1360 if args.scan_dir:
1361 input_files.extend(_ScanDirectoriesForFiles(
1362 args.scan_dir, self.select_file_func.__func__))
1363 locales = ChromeLocales()
1364 if args.add_locales:
1365 locales.extend(args.add_locales.split(' '))
1366
1367 locales = set(locales)
1368
1369 for input_file in input_files:
1370 _ProcessFile(input_file,
1371 locales,
1372 self.check_func.__func__,
1373 self.fix_func.__func__ if args.fix_inplace else None)
Raul Tambre9e24293b2019-05-12 06:11:071374 print('%sDone.' % (_CONSOLE_START_LINE))
David 'Digit' Turner00b5f272019-04-10 21:15:271375
1376
1377class _CheckGrdAndroidOutputsCommand(_CheckInputFileBaseCommand):
1378 name = 'check-grd-android-outputs'
1379 description = (
1380 'Check the Android resource (.xml) files outputs in GRIT input files.')
1381 long_description = r'''
1382Check the Android .xml files outputs in one or more input GRIT (.grd) files
1383for the following conditions:
1384
1385 - Each item has a correct 'lang' attribute.
1386 - There are no duplicated lines for the same 'lang' attribute.
1387 - That there are no extra locales that Chromium doesn't want.
1388 - That no wanted locale is missing.
1389 - Filenames exist for each listed locale.
1390 - Filenames are well-formed.
1391'''
1392 select_file_func = _IsGritInputFile
1393 check_func = _CheckGrdAndroidOutputElements
1394 fix_func = _AddMissingLocalesInGrdAndroidOutputs
1395
1396
1397class _CheckGrdTranslationsCommand(_CheckInputFileBaseCommand):
1398 name = 'check-grd-translations'
1399 description = (
1400 'Check the translation (.xtb) files outputted by .grd input files.')
1401 long_description = r'''
1402Check the translation (.xtb) file outputs in one or more input GRIT (.grd) files
1403for the following conditions:
1404
1405 - Each item has a correct 'lang' attribute.
1406 - There are no duplicated lines for the same 'lang' attribute.
1407 - That there are no extra locales that Chromium doesn't want.
1408 - That no wanted locale is missing.
1409 - Each item has a 'path' attribute.
1410 - Each such path value ends up with '.xtb'.
1411'''
1412 select_file_func = _IsGritInputFile
1413 check_func = _CheckGrdTranslations
1414 fix_func = _AddMissingLocalesInGrdTranslations
1415
1416
1417class _CheckGnAndroidOutputsCommand(_CheckInputFileBaseCommand):
1418 name = 'check-gn-android-outputs'
1419 description = 'Check the Android .xml file lists in GN build files.'
1420 long_description = r'''
1421Check one or more BUILD.gn file, looking for lists of Android resource .xml
1422files, and checking that:
1423
1424 - There are no duplicated output files in the list.
1425 - Each output file belongs to a wanted Chromium locale.
1426 - There are no output files for unwanted Chromium locales.
1427'''
1428 select_file_func = _IsBuildGnInputFile
1429 check_func = _CheckGnAndroidOutputs
1430 fix_func = _AddMissingLocalesInGnAndroidOutputs
1431
1432
1433class _CheckAllCommand(_CheckInputFileBaseCommand):
1434 name = 'check-all'
1435 description = 'Check everything.'
1436 long_description = 'Equivalent to calling all other check-xxx commands.'
1437 select_file_func = _IsAllInputFile
1438 check_func = _CheckAllFiles
1439 fix_func = _AddMissingLocalesInAllFiles
1440
1441
David 'Digit' Turner6e6f63f42019-04-15 22:02:271442class _UpdateExpectationsCommand(_Command):
1443 name = 'update-expectations'
1444 description = 'Update translation expectations file.'
1445 long_description = r'''
1446Update %s files to match the current list of locales supported by Chromium.
1447This is especially useful to add new locales before updating any GRIT or GN
1448input file with the --add-locales option.
1449''' % _EXPECTATIONS_FILENAME
1450
1451 def RegisterExtraArgs(self, group):
1452 group.add_argument(
1453 '--add-locales',
1454 help='Space-separated list of additional locales to use.')
1455
1456 def Run(self):
1457 locales = ChromeLocales()
1458 add_locales = self.args.add_locales
1459 if add_locales:
1460 locales.extend(add_locales.split(' '))
1461
1462 expectation_paths = [
1463 'tools/gritsettings/translation_expectations.pyl',
1464 'clank/tools/translation_expectations.pyl',
1465 ]
1466 missing_expectation_files = []
1467 for path in enumerate(expectation_paths):
1468 file_path = os.path.join(_TOP_SRC_DIR, path)
1469 if not os.path.exists(file_path):
1470 missing_expectation_files.append(file_path)
1471 continue
1472 _UpdateLocalesInExpectationFile(file_path, locales)
1473
1474 if missing_expectation_files:
1475 sys.stderr.write('WARNING: Missing file(s): %s\n' %
1476 (', '.join(missing_expectation_files)))
1477
1478
1479class _UnitTestsCommand(_Command):
1480 name = 'unit-tests'
1481 description = 'Run internal unit-tests for this script'
1482
1483 def RegisterExtraArgs(self, group):
1484 group.add_argument(
1485 '-v', '--verbose', action='count', help='Increase test verbosity.')
1486 group.add_argument('args', nargs=argparse.REMAINDER)
1487
1488 def Run(self):
1489 argv = [_SCRIPT_NAME] + self.args.args
1490 unittest.main(argv=argv, verbosity=self.args.verbose)
1491
1492
David 'Digit' Turner00b5f272019-04-10 21:15:271493# List of all commands supported by this script.
1494_COMMANDS = [
David 'Digit' Turner6e6f63f42019-04-15 22:02:271495 _ListLocalesCommand,
1496 _CheckGrdAndroidOutputsCommand,
1497 _CheckGrdTranslationsCommand,
1498 _CheckGnAndroidOutputsCommand,
1499 _CheckAllCommand,
1500 _UpdateExpectationsCommand,
1501 _UnitTestsCommand,
David 'Digit' Turner00b5f272019-04-10 21:15:271502]
1503
1504
1505def main(argv):
1506 parser = argparse.ArgumentParser(
1507 description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
1508
1509 subparsers = parser.add_subparsers()
1510 commands = [clazz() for clazz in _COMMANDS]
1511 for command in commands:
1512 command.RegisterArgs(subparsers)
1513
1514 if not argv:
1515 argv = ['--help']
1516
1517 args = parser.parse_args(argv)
1518 args.command.ProcessArgs(args)
1519 args.command.Run()
1520
1521
1522if __name__ == "__main__":
1523 main(sys.argv[1:])