blob: 04d5cd3591f4289e9cb9ea1528d3dacb7060b5dc [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*/>')
484_RE_TRANSLATION_ELEMENT = re.compile(r'<file (.*\.xtb")\s*/>')
485_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
490assert _RE_TRANSLATION_ELEMENT.match('<file path="foo/bar.xtb" />')
491assert _RE_TRANSLATION_ELEMENT.match('<file path="foo/bar.xtb"/>')
492assert _RE_TRANSLATION_ELEMENT.match('<file path="foo/bar.xml" />') is None
493
494
495def _IsGritInputFile(input_file):
496 """Returns True iff this is a GRIT input file."""
497 return input_file.endswith('.grd')
498
499
500def _SortGrdElementsRanges(grd_lines, element_predicate):
501 """Sort all .grd elements of a given type by their lang attribute."""
502 return _SortElementsRanges(
503 grd_lines,
504 element_predicate,
505 lambda x: _RE_LANG_ATTRIBUTE.search(x).group(1))
506
507
508def _CheckGrdElementRangeLang(grd_lines, start, end, wanted_locales):
509 """Check the element 'lang' attributes in specific .grd lines range.
510
511 This really checks the following:
512 - Each item has a correct 'lang' attribute.
513 - There are no duplicated lines for the same 'lang' attribute.
514 - That there are no extra locales that Chromium doesn't want.
515 - That no wanted locale is missing.
516
517 Args:
518 grd_lines: Input .grd lines.
519 start: Sub-range start position in input line list.
520 end: Sub-range limit position in input line list.
521 wanted_locales: Set of wanted Chromium locale names.
522 Returns:
523 List of error message strings for this input. Empty on success.
524 """
525 errors = []
526 locales = set()
527 for pos in xrange(start, end):
528 line = grd_lines[pos]
529 m = _RE_LANG_ATTRIBUTE.search(line)
530 if not m:
531 errors.append('%d: Missing "lang" attribute in <output> element' % pos +
532 1)
533 continue
534 lang = m.group(1)
535 cr_locale = _FixChromiumLangAttribute(lang)
536 if cr_locale in locales:
537 errors.append(
538 '%d: Redefinition of <output> for "%s" locale' % (pos + 1, lang))
539 locales.add(cr_locale)
540
541 extra_locales = locales.difference(wanted_locales)
542 if extra_locales:
543 errors.append('%d-%d: Extra locales found: %s' % (start + 1, end + 1,
544 sorted(extra_locales)))
545
546 missing_locales = wanted_locales.difference(locales)
547 if missing_locales:
548 errors.append('%d-%d: Missing locales: %s' % (start + 1, end + 1,
549 sorted(missing_locales)))
550
551 return errors
552
553
554##########################################################################
555##########################################################################
556#####
557##### G R D A N D R O I D O U T P U T S
558#####
559##########################################################################
560##########################################################################
561
562def _IsGrdAndroidOutputLine(line):
563 """Returns True iff this is an Android-specific <output> line."""
564 m = _RE_OUTPUT_ELEMENT.search(line)
565 if m:
566 return 'type="android"' in m.group(1)
567 return False
568
569assert _IsGrdAndroidOutputLine(' <output type="android"/>')
570
571# Many of the functions below have unused arguments due to genericity.
572# pylint: disable=unused-argument
573
574def _CheckGrdElementRangeAndroidOutputFilename(grd_lines, start, end,
575 wanted_locales):
576 """Check all <output> elements in specific input .grd lines range.
577
578 This really checks the following:
579 - Filenames exist for each listed locale.
580 - Filenames are well-formed.
581
582 Args:
583 grd_lines: Input .grd lines.
584 start: Sub-range start position in input line list.
585 end: Sub-range limit position in input line list.
586 wanted_locales: Set of wanted Chromium locale names.
587 Returns:
588 List of error message strings for this input. Empty on success.
589 """
590 errors = []
591 for pos in xrange(start, end):
592 line = grd_lines[pos]
593 m = _RE_LANG_ATTRIBUTE.search(line)
594 if not m:
595 continue
596 lang = m.group(1)
597 cr_locale = _FixChromiumLangAttribute(lang)
598
599 m = _RE_FILENAME_ATTRIBUTE.search(line)
600 if not m:
601 errors.append('%d: Missing filename attribute in <output> element' % pos +
602 1)
603 else:
604 filename = m.group(1)
605 if not filename.endswith('.xml'):
606 errors.append(
607 '%d: Filename should end with ".xml": %s' % (pos + 1, filename))
608
609 dirname = os.path.basename(os.path.dirname(filename))
610 prefix = ('values-%s' % resource_utils.ToAndroidLocaleName(cr_locale)
611 if cr_locale != _DEFAULT_LOCALE else 'values')
612 if dirname != prefix:
613 errors.append(
614 '%s: Directory name should be %s: %s' % (pos + 1, prefix, filename))
615
616 return errors
617
618
619def _CheckGrdAndroidOutputElements(grd_file, grd_lines, wanted_locales):
620 """Check all <output> elements related to Android.
621
622 Args:
623 grd_file: Input .grd file path.
624 grd_lines: List of input .grd lines.
625 wanted_locales: set of wanted Chromium locale names.
626 Returns:
627 List of error message strings. Empty on success.
628 """
629 intervals = _BuildIntervalList(grd_lines, _IsGrdAndroidOutputLine)
630 errors = []
631 for start, end in intervals:
632 errors += _CheckGrdElementRangeLang(grd_lines, start, end, wanted_locales)
633 errors += _CheckGrdElementRangeAndroidOutputFilename(grd_lines, start, end,
634 wanted_locales)
635 return errors
636
637
638def _AddMissingLocalesInGrdAndroidOutputs(grd_file, grd_lines, wanted_locales):
639 """Fix an input .grd line by adding missing Android outputs.
640
641 Args:
642 grd_file: Input .grd file path.
643 grd_lines: Input .grd line list.
644 wanted_locales: set of Chromium locale names.
645 Returns:
646 A new list of .grd lines, containing new <output> elements when needed
647 for locales from |wanted_locales| that were not part of the input.
648 """
649 intervals = _BuildIntervalList(grd_lines, _IsGrdAndroidOutputLine)
650 for start, end in reversed(intervals):
651 locales = set()
652 for pos in xrange(start, end):
653 lang = _RE_LANG_ATTRIBUTE.search(grd_lines[pos]).group(1)
654 locale = _FixChromiumLangAttribute(lang)
655 locales.add(locale)
656
657 missing_locales = wanted_locales.difference(locales)
658 if not missing_locales:
659 continue
660
661 src_locale = 'bg'
662 src_lang_attribute = 'lang="%s"' % src_locale
663 src_line = None
664 for pos in xrange(start, end):
665 if src_lang_attribute in grd_lines[pos]:
666 src_line = grd_lines[pos]
667 break
668
669 if not src_line:
670 raise Exception(
671 'Cannot find <output> element with "%s" lang attribute' % src_locale)
672
673 line_count = end - 1
674 for locale in missing_locales:
675 android_locale = resource_utils.ToAndroidLocaleName(locale)
676 dst_line = src_line.replace(
677 'lang="%s"' % src_locale, 'lang="%s"' % locale).replace(
678 'values-%s/' % src_locale, 'values-%s/' % android_locale)
679 grd_lines.insert(line_count, dst_line)
680 line_count += 1
681
682 # Sort the new <output> elements.
683 return _SortGrdElementsRanges(grd_lines, _IsGrdAndroidOutputLine)
684
685
686##########################################################################
687##########################################################################
688#####
689##### G R D T R A N S L A T I O N S
690#####
691##########################################################################
692##########################################################################
693
694
695def _IsTranslationGrdOutputLine(line):
696 """Returns True iff this is an output .xtb <file> element."""
697 m = _RE_TRANSLATION_ELEMENT.search(line)
698 return m is not None
699
700
701def _CheckGrdTranslationElementRange(grd_lines, start, end,
702 wanted_locales):
703 """Check all <translations> sub-elements in specific input .grd lines range.
704
705 This really checks the following:
706 - Each item has a 'path' attribute.
707 - Each such path value ends up with '.xtb'.
708
709 Args:
710 grd_lines: Input .grd lines.
711 start: Sub-range start position in input line list.
712 end: Sub-range limit position in input line list.
713 wanted_locales: Set of wanted Chromium locale names.
714 Returns:
715 List of error message strings for this input. Empty on success.
716 """
717 errors = []
718 for pos in xrange(start, end):
719 line = grd_lines[pos]
720 m = _RE_LANG_ATTRIBUTE.search(line)
721 if not m:
722 continue
723 m = _RE_PATH_ATTRIBUTE.search(line)
724 if not m:
725 errors.append('%d: Missing path attribute in <file> element' % pos +
726 1)
727 else:
728 filename = m.group(1)
729 if not filename.endswith('.xtb'):
730 errors.append(
731 '%d: Path should end with ".xtb": %s' % (pos + 1, filename))
732
733 return errors
734
735
736def _CheckGrdTranslations(grd_file, grd_lines, wanted_locales):
737 """Check all <file> elements that correspond to an .xtb output file.
738
739 Args:
740 grd_file: Input .grd file path.
741 grd_lines: List of input .grd lines.
742 wanted_locales: set of wanted Chromium locale names.
743 Returns:
744 List of error message strings. Empty on success.
745 """
746 wanted_locales = wanted_locales - set([_DEFAULT_LOCALE])
747 intervals = _BuildIntervalList(grd_lines, _IsTranslationGrdOutputLine)
748 errors = []
749 for start, end in intervals:
750 errors += _CheckGrdElementRangeLang(grd_lines, start, end, wanted_locales)
751 errors += _CheckGrdTranslationElementRange(grd_lines, start, end,
752 wanted_locales)
753 return errors
754
755
David 'Digit' Turner5553d512019-04-15 08:54:05756# Regular expression used to replace the lang attribute inside .xtb files.
757_RE_TRANSLATIONBUNDLE = re.compile('<translationbundle lang="(.*)">')
758
759
760def _CreateFakeXtbFileFrom(src_xtb_path, dst_xtb_path, dst_locale):
761 """Create a fake .xtb file.
762
763 Args:
764 src_xtb_path: Path to source .xtb file to copy from.
765 dst_xtb_path: Path to destination .xtb file to write to.
766 dst_locale: Destination locale, the lang attribute in the source file
767 will be substituted with this value before its lines are written
768 to the destination file.
769 """
770 with open(src_xtb_path) as f:
771 src_xtb_lines = f.readlines()
772
773 def replace_xtb_lang_attribute(line):
774 m = _RE_TRANSLATIONBUNDLE.search(line)
775 if not m:
776 return line
777 return line[:m.start(1)] + dst_locale + line[m.end(1):]
778
779 dst_xtb_lines = [replace_xtb_lang_attribute(line) for line in src_xtb_lines]
780 with build_utils.AtomicOutput(dst_xtb_path) as tmp:
781 tmp.writelines(dst_xtb_lines)
782
783
David 'Digit' Turner00b5f272019-04-10 21:15:27784def _AddMissingLocalesInGrdTranslations(grd_file, grd_lines, wanted_locales):
785 """Fix an input .grd line by adding missing Android outputs.
786
David 'Digit' Turner5553d512019-04-15 08:54:05787 This also creates fake .xtb files from the one provided for 'en-GB'.
788
David 'Digit' Turner00b5f272019-04-10 21:15:27789 Args:
790 grd_file: Input .grd file path.
791 grd_lines: Input .grd line list.
792 wanted_locales: set of Chromium locale names.
793 Returns:
794 A new list of .grd lines, containing new <output> elements when needed
795 for locales from |wanted_locales| that were not part of the input.
796 """
797 wanted_locales = wanted_locales - set([_DEFAULT_LOCALE])
798 intervals = _BuildIntervalList(grd_lines, _IsTranslationGrdOutputLine)
799 for start, end in reversed(intervals):
800 locales = set()
801 for pos in xrange(start, end):
802 lang = _RE_LANG_ATTRIBUTE.search(grd_lines[pos]).group(1)
803 locale = _FixChromiumLangAttribute(lang)
804 locales.add(locale)
805
806 missing_locales = wanted_locales.difference(locales)
807 if not missing_locales:
808 continue
809
David 'Digit' Turner5553d512019-04-15 08:54:05810 src_locale = 'en-GB'
David 'Digit' Turner00b5f272019-04-10 21:15:27811 src_lang_attribute = 'lang="%s"' % src_locale
812 src_line = None
813 for pos in xrange(start, end):
814 if src_lang_attribute in grd_lines[pos]:
815 src_line = grd_lines[pos]
816 break
817
818 if not src_line:
819 raise Exception(
820 'Cannot find <file> element with "%s" lang attribute' % src_locale)
821
David 'Digit' Turner5553d512019-04-15 08:54:05822 src_path = os.path.join(
823 os.path.dirname(grd_file),
824 _RE_PATH_ATTRIBUTE.search(src_line).group(1))
825
David 'Digit' Turner00b5f272019-04-10 21:15:27826 line_count = end - 1
827 for locale in missing_locales:
828 dst_line = src_line.replace(
829 'lang="%s"' % src_locale, 'lang="%s"' % locale).replace(
830 '_%s.xtb' % src_locale, '_%s.xtb' % locale)
831 grd_lines.insert(line_count, dst_line)
832 line_count += 1
833
David 'Digit' Turner5553d512019-04-15 08:54:05834 dst_path = src_path.replace('_%s.xtb' % src_locale, '_%s.xtb' % locale)
835 _CreateFakeXtbFileFrom(src_path, dst_path, locale)
836
837
David 'Digit' Turner00b5f272019-04-10 21:15:27838 # Sort the new <output> elements.
839 return _SortGrdElementsRanges(grd_lines, _IsTranslationGrdOutputLine)
840
841
842##########################################################################
843##########################################################################
844#####
845##### G N A N D R O I D O U T P U T S
846#####
847##########################################################################
848##########################################################################
849
850_RE_GN_VALUES_LIST_LINE = re.compile(
851 r'^\s*".*values(\-([A-Za-z0-9-]+))?/.*\.xml",\s*$')
852
853def _IsBuildGnInputFile(input_file):
854 """Returns True iff this is a BUILD.gn file."""
855 return os.path.basename(input_file) == 'BUILD.gn'
856
857
858def _GetAndroidGnOutputLocale(line):
859 """Check a GN list, and return its Android locale if it is an output .xml"""
860 m = _RE_GN_VALUES_LIST_LINE.match(line)
861 if not m:
862 return None
863
864 if m.group(1): # First group is optional and contains group 2.
865 return m.group(2)
866
867 return resource_utils.ToAndroidLocaleName(_DEFAULT_LOCALE)
868
869
870def _IsAndroidGnOutputLine(line):
871 """Returns True iff this is an Android-specific localized .xml output."""
872 return _GetAndroidGnOutputLocale(line) != None
873
874
875def _CheckGnOutputsRangeForLocalizedStrings(gn_lines, start, end):
876 """Check that a range of GN lines corresponds to localized strings.
877
878 Special case: Some BUILD.gn files list several non-localized .xml files
879 that should be ignored by this function, e.g. in
880 components/cronet/android/BUILD.gn, the following appears:
881
882 inputs = [
883 ...
884 "sample/res/layout/activity_main.xml",
885 "sample/res/layout/dialog_url.xml",
886 "sample/res/values/dimens.xml",
887 "sample/res/values/strings.xml",
888 ...
889 ]
890
891 These are non-localized strings, and should be ignored. This function is
892 used to detect them quickly.
893 """
894 for pos in xrange(start, end):
895 if not 'values/' in gn_lines[pos]:
896 return True
897 return False
898
899
900def _CheckGnOutputsRange(gn_lines, start, end, wanted_locales):
901 if not _CheckGnOutputsRangeForLocalizedStrings(gn_lines, start, end):
902 return []
903
904 errors = []
905 locales = set()
906 for pos in xrange(start, end):
907 line = gn_lines[pos]
908 android_locale = _GetAndroidGnOutputLocale(line)
909 assert android_locale != None
910 cr_locale = resource_utils.ToChromiumLocaleName(android_locale)
911 if cr_locale in locales:
912 errors.append('%s: Redefinition of output for "%s" locale' %
913 (pos + 1, android_locale))
914 locales.add(cr_locale)
915
916 extra_locales = locales.difference(wanted_locales)
917 if extra_locales:
918 errors.append('%d-%d: Extra locales: %s' % (start + 1, end + 1,
919 sorted(extra_locales)))
920
921 missing_locales = wanted_locales.difference(locales)
922 if missing_locales:
923 errors.append('%d-%d: Missing locales: %s' % (start + 1, end + 1,
924 sorted(missing_locales)))
925
926 return errors
927
928
929def _CheckGnAndroidOutputs(gn_file, gn_lines, wanted_locales):
930 intervals = _BuildIntervalList(gn_lines, _IsAndroidGnOutputLine)
931 errors = []
932 for start, end in intervals:
933 errors += _CheckGnOutputsRange(gn_lines, start, end, wanted_locales)
934 return errors
935
936
937def _AddMissingLocalesInGnAndroidOutputs(gn_file, gn_lines, wanted_locales):
938 intervals = _BuildIntervalList(gn_lines, _IsAndroidGnOutputLine)
939 # NOTE: Since this may insert new lines to each interval, process the
940 # list in reverse order to maintain valid (start,end) positions during
941 # the iteration.
942 for start, end in reversed(intervals):
943 if not _CheckGnOutputsRangeForLocalizedStrings(gn_lines, start, end):
944 continue
945
946 locales = set()
947 for pos in xrange(start, end):
948 lang = _GetAndroidGnOutputLocale(gn_lines[pos])
949 locale = resource_utils.ToChromiumLocaleName(lang)
950 locales.add(locale)
951
952 missing_locales = wanted_locales.difference(locales)
953 if not missing_locales:
954 continue
955
956 src_locale = 'bg'
957 src_values = 'values-%s/' % resource_utils.ToAndroidLocaleName(src_locale)
958 src_line = None
959 for pos in xrange(start, end):
960 if src_values in gn_lines[pos]:
961 src_line = gn_lines[pos]
962 break
963
964 if not src_line:
965 raise Exception(
966 'Cannot find output list item with "%s" locale' % src_locale)
967
968 line_count = end - 1
969 for locale in missing_locales:
970 if locale == _DEFAULT_LOCALE:
971 dst_line = src_line.replace('values-%s/' % src_locale, 'values/')
972 else:
973 dst_line = src_line.replace(
974 'values-%s/' % src_locale,
975 'values-%s/' % resource_utils.ToAndroidLocaleName(locale))
976 gn_lines.insert(line_count, dst_line)
977 line_count += 1
978
979 gn_lines = _SortListSubRange(
980 gn_lines, start, line_count,
981 lambda line: _RE_GN_VALUES_LIST_LINE.match(line).group(1))
982
983 return gn_lines
984
985
David 'Digit' Turner6e6f63f42019-04-15 22:02:27986##########################################################################
987##########################################################################
988#####
989##### T R A N S L A T I O N E X P E C T A T I O N S
990#####
991##########################################################################
992##########################################################################
993
994_EXPECTATIONS_FILENAME = 'translation_expectations.pyl'
995
996# Technical note: the format of translation_expectations.pyl
997# is a 'Python literal', which defines a python dictionary, so should
998# be easy to parse. However, when modifying it, care should be taken
999# to respect the line comments and the order of keys within the text
1000# file.
1001
1002
1003def _ReadPythonLiteralFile(pyl_path):
1004 """Read a .pyl file into a Python data structure."""
1005 with open(pyl_path) as f:
1006 pyl_content = f.read()
1007 # Evaluate as a Python data structure, use an empty global
1008 # and local dictionary.
1009 return eval(pyl_content, dict(), dict())
1010
1011
1012def _UpdateLocalesInExpectationLines(pyl_lines,
1013 wanted_locales,
1014 available_width=79):
1015 """Update the locales list(s) found in an expectations file.
1016
1017 Args:
1018 pyl_lines: Iterable of input lines from the file.
1019 wanted_locales: Set or list of new locale names.
1020 available_width: Optional, number of character colums used
1021 to word-wrap the new list items.
1022 Returns:
1023 New list of updated lines.
1024 """
1025 locales_list = ['"%s"' % loc for loc in sorted(wanted_locales)]
1026 result = []
1027 line_count = len(pyl_lines)
1028 line_num = 0
1029 DICT_START = '"languages": ['
1030 while line_num < line_count:
1031 line = pyl_lines[line_num]
1032 line_num += 1
1033 result.append(line)
1034 # Look for start of "languages" dictionary.
1035 pos = line.find(DICT_START)
1036 if pos < 0:
1037 continue
1038
1039 start_margin = pos
1040 start_line = line_num
1041 # Skip over all lines from the list.
1042 while (line_num < line_count and
1043 not pyl_lines[line_num].rstrip().endswith('],')):
1044 line_num += 1
1045 continue
1046
1047 if line_num == line_count:
1048 raise Exception('%d: Missing list termination!' % start_line)
1049
1050 # Format the new list according to the new margin.
1051 locale_width = available_width - (start_margin + 2)
1052 locale_lines = _PrettyPrintListAsLines(
1053 locales_list, locale_width, trailing_comma=True)
1054 for locale_line in locale_lines:
1055 result.append(' ' * (start_margin + 2) + locale_line)
1056 result.append(' ' * start_margin + '],')
1057 line_num += 1
1058
1059 return result
1060
1061
1062class _UpdateLocalesInExpectationLinesTest(unittest.TestCase):
1063
1064 def test_simple(self):
1065 self.maxDiff = 1000
1066 input_text = r'''
1067# This comment should be preserved
1068# 23456789012345678901234567890123456789
1069{
1070 "android_grd": {
1071 "languages": [
1072 "aa", "bb", "cc", "dd", "ee",
1073 "ff", "gg", "hh", "ii", "jj",
1074 "kk"],
1075 },
1076 # Example with bad indentation in input.
1077 "another_grd": {
1078 "languages": [
1079 "aa", "bb", "cc", "dd", "ee", "ff", "gg", "hh", "ii", "jj", "kk",
1080 ],
1081 },
1082}
1083'''
1084 expected_text = r'''
1085# This comment should be preserved
1086# 23456789012345678901234567890123456789
1087{
1088 "android_grd": {
1089 "languages": [
1090 "A2", "AA", "BB", "CC", "DD",
1091 "E2", "EE", "FF", "GG", "HH",
1092 "I2", "II", "JJ", "KK",
1093 ],
1094 },
1095 # Example with bad indentation in input.
1096 "another_grd": {
1097 "languages": [
1098 "A2", "AA", "BB", "CC", "DD",
1099 "E2", "EE", "FF", "GG", "HH",
1100 "I2", "II", "JJ", "KK",
1101 ],
1102 },
1103}
1104'''
1105 input_lines = input_text.splitlines()
1106 test_locales = ([
1107 'AA', 'BB', 'CC', 'DD', 'EE', 'FF', 'GG', 'HH', 'II', 'JJ', 'KK', 'A2',
1108 'E2', 'I2'
1109 ])
1110 expected_lines = expected_text.splitlines()
1111 self.assertListEqual(
1112 _UpdateLocalesInExpectationLines(input_lines, test_locales, 40),
1113 expected_lines)
1114
1115 def test_missing_list_termination(self):
1116 input_lines = r'''
1117 "languages": ['
1118 "aa", "bb", "cc", "dd"
1119'''.splitlines()
1120 with self.assertRaises(Exception) as cm:
1121 _UpdateLocalesInExpectationLines(input_lines, ['a', 'b'], 40)
1122
1123 self.assertEqual(str(cm.exception), '2: Missing list termination!')
1124
1125
1126def _UpdateLocalesInExpectationFile(pyl_path, wanted_locales):
1127 """Update all locales listed in a given expectations file.
1128
1129 Args:
1130 pyl_path: Path to .pyl file to update.
1131 wanted_locales: List of locales that need to be written to
1132 the file.
1133 """
1134 tc_locales = {
1135 _FixTranslationConsoleLocaleName(locale)
1136 for locale in set(wanted_locales) - set([_DEFAULT_LOCALE])
1137 }
1138
1139 with open(pyl_path) as f:
1140 input_lines = [l.rstrip() for l in f.readlines()]
1141
1142 updated_lines = _UpdateLocalesInExpectationLines(input_lines, tc_locales)
1143 print repr(updated_lines)
1144 with build_utils.AtomicOutput(pyl_path) as f:
1145 f.writelines('\n'.join(updated_lines) + '\n')
1146
David 'Digit' Turner00b5f272019-04-10 21:15:271147
1148##########################################################################
1149##########################################################################
1150#####
1151##### C H E C K E V E R Y T H I N G
1152#####
1153##########################################################################
1154##########################################################################
1155
David 'Digit' Turner6e6f63f42019-04-15 22:02:271156# pylint: enable=unused-argument
1157
1158
David 'Digit' Turner00b5f272019-04-10 21:15:271159def _IsAllInputFile(input_file):
1160 return _IsGritInputFile(input_file) or _IsBuildGnInputFile(input_file)
1161
1162
1163def _CheckAllFiles(input_file, input_lines, wanted_locales):
1164 errors = []
1165 if _IsGritInputFile(input_file):
1166 errors += _CheckGrdTranslations(input_file, input_lines, wanted_locales)
1167 errors += _CheckGrdAndroidOutputElements(
1168 input_file, input_lines, wanted_locales)
1169 elif _IsBuildGnInputFile(input_file):
1170 errors += _CheckGnAndroidOutputs(input_file, input_lines, wanted_locales)
1171 return errors
1172
1173
1174def _AddMissingLocalesInAllFiles(input_file, input_lines, wanted_locales):
1175 if _IsGritInputFile(input_file):
1176 lines = _AddMissingLocalesInGrdTranslations(
1177 input_file, input_lines, wanted_locales)
1178 lines = _AddMissingLocalesInGrdAndroidOutputs(
1179 input_file, lines, wanted_locales)
1180 elif _IsBuildGnInputFile(input_file):
1181 lines = _AddMissingLocalesInGnAndroidOutputs(
1182 input_file, input_lines, wanted_locales)
1183 return lines
1184
David 'Digit' Turner5553d512019-04-15 08:54:051185
David 'Digit' Turner00b5f272019-04-10 21:15:271186##########################################################################
1187##########################################################################
1188#####
1189##### C O M M A N D H A N D L I N G
1190#####
1191##########################################################################
1192##########################################################################
1193
1194class _Command(object):
1195 """A base class for all commands recognized by this script.
1196
1197 Usage is the following:
1198 1) Derived classes must re-define the following class-based fields:
1199 - name: Command name (e.g. 'list-locales')
1200 - description: Command short description.
1201 - long_description: Optional. Command long description.
1202 NOTE: As a convenience, if the first character is a newline,
1203 it will be omitted in the help output.
1204
1205 2) Derived classes for commands that take arguments should override
1206 RegisterExtraArgs(), which receives a corresponding argparse
1207 sub-parser as argument.
1208
1209 3) Derived classes should implement a Run() command, which can read
1210 the current arguments from self.args.
1211 """
1212 name = None
1213 description = None
1214 long_description = None
1215
1216 def __init__(self):
1217 self._parser = None
1218 self.args = None
1219
1220 def RegisterExtraArgs(self, subparser):
1221 pass
1222
1223 def RegisterArgs(self, parser):
1224 subp = parser.add_parser(
1225 self.name, help=self.description,
1226 description=self.long_description or self.description,
1227 formatter_class=argparse.RawDescriptionHelpFormatter)
1228 self._parser = subp
1229 subp.set_defaults(command=self)
1230 group = subp.add_argument_group('%s arguments' % self.name)
1231 self.RegisterExtraArgs(group)
1232
1233 def ProcessArgs(self, args):
1234 self.args = args
1235
1236
1237class _ListLocalesCommand(_Command):
1238 """Implement the 'list-locales' command to list locale lists of interest."""
1239 name = 'list-locales'
1240 description = 'List supported Chrome locales'
1241 long_description = r'''
1242List locales of interest, by default this prints all locales supported by
1243Chrome, but `--type=android_omitted` can be used to print the list of locales
1244omitted from Android APKs (but not app bundles), and `--type=ios_unsupported`
1245for the list of locales unsupported on iOS.
1246
1247These values are extracted directly from build/config/locales.gni.
1248
1249Additionally, use the --as-json argument to print the list as a JSON list,
1250instead of the default format (which is a space-separated list of locale names).
1251'''
1252
1253 # Maps type argument to a function returning the corresponding locales list.
1254 TYPE_MAP = {
1255 'all': ChromeLocales,
1256 'android_omitted': AndroidOmittedLocales,
1257 'ios_unsupported': IosUnsupportedLocales,
1258 }
1259
1260 def RegisterExtraArgs(self, group):
1261 group.add_argument(
1262 '--as-json',
1263 action='store_true',
1264 help='Output as JSON list.')
1265 group.add_argument(
1266 '--type',
1267 choices=tuple(self.TYPE_MAP.viewkeys()),
1268 default='all',
1269 help='Select type of locale list to print.')
1270
1271 def Run(self):
1272 locale_list = self.TYPE_MAP[self.args.type]()
1273 if self.args.as_json:
1274 print '[%s]' % ", ".join("'%s'" % loc for loc in locale_list)
1275 else:
1276 print ' '.join(locale_list)
1277
1278
1279class _CheckInputFileBaseCommand(_Command):
1280 """Used as a base for other _Command subclasses that check input files.
1281
1282 Subclasses should also define the following class-level variables:
1283
1284 - select_file_func:
1285 A predicate that receives a file name (not path) and return True if it
1286 should be selected for inspection. Used when scanning directories with
1287 '--scan-dir <dir>'.
1288
1289 - check_func:
1290 - fix_func:
1291 Two functions passed as parameters to _ProcessFile(), see relevant
1292 documentation in this function's definition.
1293 """
1294 select_file_func = None
1295 check_func = None
1296 fix_func = None
1297
1298 def RegisterExtraArgs(self, group):
1299 group.add_argument(
1300 '--scan-dir',
1301 action='append',
1302 help='Optional directory to scan for input files recursively.')
1303 group.add_argument(
1304 'input',
1305 nargs='*',
1306 help='Input file(s) to check.')
1307 group.add_argument(
1308 '--fix-inplace',
1309 action='store_true',
1310 help='Try to fix the files in-place too.')
1311 group.add_argument(
1312 '--add-locales',
1313 help='Space-separated list of additional locales to use')
1314
1315 def Run(self):
1316 args = self.args
1317 input_files = []
1318 if args.input:
1319 input_files = args.input
1320 if args.scan_dir:
1321 input_files.extend(_ScanDirectoriesForFiles(
1322 args.scan_dir, self.select_file_func.__func__))
1323 locales = ChromeLocales()
1324 if args.add_locales:
1325 locales.extend(args.add_locales.split(' '))
1326
1327 locales = set(locales)
1328
1329 for input_file in input_files:
1330 _ProcessFile(input_file,
1331 locales,
1332 self.check_func.__func__,
1333 self.fix_func.__func__ if args.fix_inplace else None)
1334 print '%sDone.' % (_CONSOLE_START_LINE)
1335
1336
1337class _CheckGrdAndroidOutputsCommand(_CheckInputFileBaseCommand):
1338 name = 'check-grd-android-outputs'
1339 description = (
1340 'Check the Android resource (.xml) files outputs in GRIT input files.')
1341 long_description = r'''
1342Check the Android .xml files outputs in one or more input GRIT (.grd) files
1343for the following conditions:
1344
1345 - Each item has a correct 'lang' attribute.
1346 - There are no duplicated lines for the same 'lang' attribute.
1347 - That there are no extra locales that Chromium doesn't want.
1348 - That no wanted locale is missing.
1349 - Filenames exist for each listed locale.
1350 - Filenames are well-formed.
1351'''
1352 select_file_func = _IsGritInputFile
1353 check_func = _CheckGrdAndroidOutputElements
1354 fix_func = _AddMissingLocalesInGrdAndroidOutputs
1355
1356
1357class _CheckGrdTranslationsCommand(_CheckInputFileBaseCommand):
1358 name = 'check-grd-translations'
1359 description = (
1360 'Check the translation (.xtb) files outputted by .grd input files.')
1361 long_description = r'''
1362Check the translation (.xtb) file outputs in one or more input GRIT (.grd) files
1363for the following conditions:
1364
1365 - Each item has a correct 'lang' attribute.
1366 - There are no duplicated lines for the same 'lang' attribute.
1367 - That there are no extra locales that Chromium doesn't want.
1368 - That no wanted locale is missing.
1369 - Each item has a 'path' attribute.
1370 - Each such path value ends up with '.xtb'.
1371'''
1372 select_file_func = _IsGritInputFile
1373 check_func = _CheckGrdTranslations
1374 fix_func = _AddMissingLocalesInGrdTranslations
1375
1376
1377class _CheckGnAndroidOutputsCommand(_CheckInputFileBaseCommand):
1378 name = 'check-gn-android-outputs'
1379 description = 'Check the Android .xml file lists in GN build files.'
1380 long_description = r'''
1381Check one or more BUILD.gn file, looking for lists of Android resource .xml
1382files, and checking that:
1383
1384 - There are no duplicated output files in the list.
1385 - Each output file belongs to a wanted Chromium locale.
1386 - There are no output files for unwanted Chromium locales.
1387'''
1388 select_file_func = _IsBuildGnInputFile
1389 check_func = _CheckGnAndroidOutputs
1390 fix_func = _AddMissingLocalesInGnAndroidOutputs
1391
1392
1393class _CheckAllCommand(_CheckInputFileBaseCommand):
1394 name = 'check-all'
1395 description = 'Check everything.'
1396 long_description = 'Equivalent to calling all other check-xxx commands.'
1397 select_file_func = _IsAllInputFile
1398 check_func = _CheckAllFiles
1399 fix_func = _AddMissingLocalesInAllFiles
1400
1401
David 'Digit' Turner6e6f63f42019-04-15 22:02:271402class _UpdateExpectationsCommand(_Command):
1403 name = 'update-expectations'
1404 description = 'Update translation expectations file.'
1405 long_description = r'''
1406Update %s files to match the current list of locales supported by Chromium.
1407This is especially useful to add new locales before updating any GRIT or GN
1408input file with the --add-locales option.
1409''' % _EXPECTATIONS_FILENAME
1410
1411 def RegisterExtraArgs(self, group):
1412 group.add_argument(
1413 '--add-locales',
1414 help='Space-separated list of additional locales to use.')
1415
1416 def Run(self):
1417 locales = ChromeLocales()
1418 add_locales = self.args.add_locales
1419 if add_locales:
1420 locales.extend(add_locales.split(' '))
1421
1422 expectation_paths = [
1423 'tools/gritsettings/translation_expectations.pyl',
1424 'clank/tools/translation_expectations.pyl',
1425 ]
1426 missing_expectation_files = []
1427 for path in enumerate(expectation_paths):
1428 file_path = os.path.join(_TOP_SRC_DIR, path)
1429 if not os.path.exists(file_path):
1430 missing_expectation_files.append(file_path)
1431 continue
1432 _UpdateLocalesInExpectationFile(file_path, locales)
1433
1434 if missing_expectation_files:
1435 sys.stderr.write('WARNING: Missing file(s): %s\n' %
1436 (', '.join(missing_expectation_files)))
1437
1438
1439class _UnitTestsCommand(_Command):
1440 name = 'unit-tests'
1441 description = 'Run internal unit-tests for this script'
1442
1443 def RegisterExtraArgs(self, group):
1444 group.add_argument(
1445 '-v', '--verbose', action='count', help='Increase test verbosity.')
1446 group.add_argument('args', nargs=argparse.REMAINDER)
1447
1448 def Run(self):
1449 argv = [_SCRIPT_NAME] + self.args.args
1450 unittest.main(argv=argv, verbosity=self.args.verbose)
1451
1452
David 'Digit' Turner00b5f272019-04-10 21:15:271453# List of all commands supported by this script.
1454_COMMANDS = [
David 'Digit' Turner6e6f63f42019-04-15 22:02:271455 _ListLocalesCommand,
1456 _CheckGrdAndroidOutputsCommand,
1457 _CheckGrdTranslationsCommand,
1458 _CheckGnAndroidOutputsCommand,
1459 _CheckAllCommand,
1460 _UpdateExpectationsCommand,
1461 _UnitTestsCommand,
David 'Digit' Turner00b5f272019-04-10 21:15:271462]
1463
1464
1465def main(argv):
1466 parser = argparse.ArgumentParser(
1467 description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
1468
1469 subparsers = parser.add_subparsers()
1470 commands = [clazz() for clazz in _COMMANDS]
1471 for command in commands:
1472 command.RegisterArgs(subparsers)
1473
1474 if not argv:
1475 argv = ['--help']
1476
1477 args = parser.parse_args(argv)
1478 args.command.ProcessArgs(args)
1479 args.command.Run()
1480
1481
1482if __name__ == "__main__":
1483 main(sys.argv[1:])