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