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