OLD | NEW |
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 # Copyright 2013 The Chromium Authors. All rights reserved. | 2 # Copyright 2013 The Chromium Authors. All rights reserved. |
3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
5 | 5 |
6 """ | 6 """ |
7 localize.py -- Generates an output file from the given template replacing | 7 localize.py -- Generates an output file from the given template replacing |
8 variables and localizing strings. | 8 variables and localizing strings. |
9 | 9 |
10 The script uses Jinja2 template processing library (src/third_party/jinja2). | 10 The script uses Jinja2 template processing library (src/third_party/jinja2). |
11 Variables available to the templates: | 11 Variables available to the templates: |
12 - |languages| - the list of languages passed on the command line. ('-l'). | 12 - |languages| - the list of languages passed on the command line. ('-l'). |
13 - Each KEY=VALUE define ('-d') can be accesses as |KEY|. | 13 - Each NAME=VALUE define ('-d') can be accesses as {{ NAME }}. |
14 - |official_build| is set to '1' when CHROME_BUILD_TYPE environment variable | 14 - |official_build| is set to '1' when CHROME_BUILD_TYPE environment variable |
15 is set to "_official". | 15 is set to "_official". |
16 | 16 |
17 Filters: | 17 Filters: |
18 - GetCodepage - returns the code page for the given language. | 18 - GetCodepage - returns the code page for the given language. |
19 - GetCodepageDecimal same as GetCodepage, but returns a decimal value. | 19 - GetCodepageDecimal same as GetCodepage, but returns a decimal value. |
20 - GetLangId - returns Win32 LANGID. | 20 - GetLangId - returns Win32 LANGID. |
21 - GetPrimaryLanguage - returns a named Win32 constant specifing the primary | 21 - GetPrimaryLanguage - returns a named Win32 constant specifing the primary |
22 language ID. | 22 language ID. |
23 - GetSublanguage - returns a named Win32 constant specifing the sublanguage | 23 - GetSublanguage - returns a named Win32 constant specifing the sublanguage |
24 ID. | 24 ID. |
25 | 25 |
26 Globals: | 26 Globals: |
| 27 - IsRtlLanguage(language) - returns True if the language is right-to-left. |
27 - SelectLanguage(language) - allows to select the language to the used by | 28 - SelectLanguage(language) - allows to select the language to the used by |
28 {% trans %}{% endtrans %} statements. | 29 {% trans %}{% endtrans %} statements. |
29 | 30 |
30 """ | 31 """ |
31 | 32 |
32 import io | 33 import io |
33 import json | 34 import json |
34 from optparse import OptionParser | 35 from optparse import OptionParser |
35 import os | 36 import os |
36 import sys | 37 import sys |
| 38 from string import Template |
| 39 |
37 | 40 |
38 # Win32 primary languages IDs. | 41 # Win32 primary languages IDs. |
39 _LANGUAGE_PRIMARY = { | 42 _LANGUAGE_PRIMARY = { |
40 'LANG_NEUTRAL' : 0x00, | 43 'LANG_NEUTRAL' : 0x00, |
41 'LANG_INVARIANT' : 0x7f, | 44 'LANG_INVARIANT' : 0x7f, |
42 'LANG_AFRIKAANS' : 0x36, | 45 'LANG_AFRIKAANS' : 0x36, |
43 'LANG_ALBANIAN' : 0x1c, | 46 'LANG_ALBANIAN' : 0x1c, |
44 'LANG_ALSATIAN' : 0x84, | 47 'LANG_ALSATIAN' : 0x84, |
45 'LANG_AMHARIC' : 0x5e, | 48 'LANG_AMHARIC' : 0x5e, |
46 'LANG_ARABIC' : 0x01, | 49 'LANG_ARABIC' : 0x01, |
(...skipping 488 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
535 | 538 |
536 | 539 |
537 def IsRtlLanguage(language): | 540 def IsRtlLanguage(language): |
538 return language in _RTL_LANGUAGES; | 541 return language in _RTL_LANGUAGES; |
539 | 542 |
540 | 543 |
541 def NormalizeLanguageCode(language): | 544 def NormalizeLanguageCode(language): |
542 return language.replace('_', '-', 1) | 545 return language.replace('_', '-', 1) |
543 | 546 |
544 | 547 |
| 548 def GetDataPackageSuffix(language): |
| 549 lang = NormalizeLanguageCode(language) |
| 550 if lang == 'en': |
| 551 lang = 'en-US' |
| 552 return lang |
| 553 |
| 554 |
| 555 def GetJsonSuffix(language): |
| 556 return language.replace('-', '_', 1) |
| 557 |
| 558 |
545 def ReadValuesFromFile(values_dict, file_name): | 559 def ReadValuesFromFile(values_dict, file_name): |
546 """ | 560 """ |
547 Reads KEYWORD=VALUE settings from the specified file. | 561 Reads NAME=VALUE settings from the specified file. |
548 | 562 |
549 Everything to the left of the first '=' is the keyword, | 563 Everything to the left of the first '=' is the keyword, |
550 everything to the right is the value. No stripping of | 564 everything to the right is the value. No stripping of |
551 white space, so beware. | 565 white space, so beware. |
552 | 566 |
553 The file must exist, otherwise you get the Python exception from open(). | 567 The file must exist, otherwise you get the Python exception from open(). |
554 """ | 568 """ |
555 for line in open(file_name, 'r').readlines(): | 569 for line in open(file_name, 'r').readlines(): |
556 key, val = line.rstrip('\r\n').split('=', 1) | 570 key, val = line.rstrip('\r\n').split('=', 1) |
557 values_dict[key] = val | 571 values_dict[key] = val |
(...skipping 31 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
589 else: | 603 else: |
590 if contents == old_contents: | 604 if contents == old_contents: |
591 return | 605 return |
592 target.close() | 606 target.close() |
593 os.unlink(file_name) | 607 os.unlink(file_name) |
594 io.open(file_name, 'w', encoding=encoding).write(contents) | 608 io.open(file_name, 'w', encoding=encoding).write(contents) |
595 | 609 |
596 | 610 |
597 class MessageMap: | 611 class MessageMap: |
598 """ Provides a dictionary of localized messages for each language.""" | 612 """ Provides a dictionary of localized messages for each language.""" |
599 def __init__(self, languages, messages_path): | 613 def __init__(self, languages, locale_dir): |
600 self.language = None | 614 self.language = None |
601 self.message_map = {} | 615 self.message_map = {} |
602 | 616 |
603 # Populate the message map | 617 # Populate the message map |
604 if messages_path: | 618 if locale_dir: |
605 for language in languages: | 619 for language in languages: |
606 file_name = os.path.join(messages_path, | 620 file_name = os.path.join(locale_dir, |
607 language.replace('-', '_', 1), | 621 GetJsonSuffix(language), |
608 'messages.json') | 622 'messages.json') |
609 self.message_map[language] = ReadMessagesFromFile(file_name) | 623 self.message_map[language] = ReadMessagesFromFile(file_name) |
610 | 624 |
611 def GetText(self, message): | 625 def GetText(self, message): |
612 """ Returns a localized message for the current language. """ | 626 """ Returns a localized message for the current language. """ |
613 return self.message_map[self.language][message] | 627 return self.message_map[self.language][message] |
614 | 628 |
615 def SelectLanguage(self, language): | 629 def SelectLanguage(self, language): |
616 """ Selects the language to be used when retrieving localized messages. """ | 630 """ Selects the language to be used when retrieving localized messages. """ |
617 self.language = language | 631 self.language = language |
618 | 632 |
619 def MakeSelectLanguage(self): | 633 def MakeSelectLanguage(self): |
620 """ Returns a function that can be used to select the current language. """ | 634 """ Returns a function that can be used to select the current language. """ |
621 return lambda language: self.SelectLanguage(language) | 635 return lambda language: self.SelectLanguage(language) |
622 | 636 |
623 def MakeGetText(self): | 637 def MakeGetText(self): |
624 """ Returns a function that can be used to retrieve a localized message. """ | 638 """ Returns a function that can be used to retrieve a localized message. """ |
625 return lambda message: self.GetText(message) | 639 return lambda message: self.GetText(message) |
626 | 640 |
627 | 641 |
628 def Localize(source, target, options): | 642 def Localize(source, locales, options): |
629 # Load jinja2 library. | 643 # Set the list of languages to use. |
630 if options.jinja2: | 644 languages = map(NormalizeLanguageCode, locales) |
631 jinja2_path = os.path.normpath(options.jinja2) | 645 context = { 'languages' : languages } |
632 else: | |
633 jinja2_path = os.path.normpath(os.path.join(os.path.abspath(__file__), | |
634 '../../../third_party/jinja2')) | |
635 sys.path.append(os.path.split(jinja2_path)[0]) | |
636 from jinja2 import Environment, FileSystemLoader | |
637 | 646 |
638 # Create jinja2 environment. | 647 # Load the localized messages. |
639 (template_path, template_name) = os.path.split(source) | 648 message_map = MessageMap(languages, options.locale_dir) |
640 env = Environment(loader=FileSystemLoader(template_path), | |
641 extensions=['jinja2.ext.do', 'jinja2.ext.i18n']) | |
642 | |
643 # Register custom filters. | |
644 env.filters['GetCodepage'] = GetCodepage | |
645 env.filters['GetCodepageDecimal'] = GetCodepageDecimal | |
646 env.filters['GetLangId'] = GetLangId | |
647 env.filters['GetPrimaryLanguage'] = GetPrimaryLanguage | |
648 env.filters['GetSublanguage'] = GetSublanguage | |
649 | |
650 # Set the list of languages to use | |
651 languages = map(NormalizeLanguageCode, options.languages) | |
652 context = { 'languages' : languages } | |
653 env.globals['IsRtlLanguage'] = IsRtlLanguage | |
654 | |
655 # Load the localized messages and register the message map with jinja2.i18n | |
656 # extension. | |
657 message_map = MessageMap(languages, options.messages_path) | |
658 env.globals['SelectLanguage'] = message_map.MakeSelectLanguage() | |
659 env.install_gettext_callables(message_map.MakeGetText(), | |
660 message_map.MakeGetText()); | |
661 | 649 |
662 # Add OFFICIAL_BUILD variable the same way chrome/tools/build/version.py | 650 # Add OFFICIAL_BUILD variable the same way chrome/tools/build/version.py |
663 # does. | 651 # does. |
664 if os.environ.get('CHROME_BUILD_TYPE') == '_official': | 652 if os.environ.get('CHROME_BUILD_TYPE') == '_official': |
665 context['official_build'] = '1' | 653 context['official_build'] = '1' |
666 else: | 654 else: |
667 context['official_build'] = '0' | 655 context['official_build'] = '0' |
668 | 656 |
669 # Add all variables defined in the command line. | 657 # Add all variables defined in the command line. |
670 if options.define: | 658 if options.define: |
671 for define in options.define: | 659 for define in options.define: |
672 context.update(dict([define.split('=', 1)])); | 660 context.update(dict([define.split('=', 1)])); |
673 | 661 |
674 # Read KEYWORD=VALUE variables from file. | 662 # Read NAME=VALUE variables from file. |
675 if options.input: | 663 if options.variables: |
676 for file_name in options.input: | 664 for file_name in options.variables: |
677 ReadValuesFromFile(context, file_name) | 665 ReadValuesFromFile(context, file_name) |
678 | 666 |
679 template = env.get_template(template_name) | 667 env = None |
680 WriteIfChanged(target, template.render(context), options.encoding); | 668 template = None |
681 return 0; | 669 |
| 670 if source: |
| 671 # Load jinja2 library. |
| 672 if options.jinja2: |
| 673 jinja2_path = os.path.normpath(options.jinja2) |
| 674 else: |
| 675 jinja2_path = os.path.normpath( |
| 676 os.path.join(os.path.abspath(__file__), |
| 677 '../../../../third_party/jinja2')) |
| 678 sys.path.append(os.path.split(jinja2_path)[0]) |
| 679 from jinja2 import Environment, FileSystemLoader |
| 680 |
| 681 # Create jinja2 environment. |
| 682 (template_path, template_name) = os.path.split(source) |
| 683 env = Environment(loader=FileSystemLoader(template_path), |
| 684 extensions=['jinja2.ext.do', 'jinja2.ext.i18n']) |
| 685 |
| 686 # Register custom filters. |
| 687 env.filters['GetCodepage'] = GetCodepage |
| 688 env.filters['GetCodepageDecimal'] = GetCodepageDecimal |
| 689 env.filters['GetLangId'] = GetLangId |
| 690 env.filters['GetPrimaryLanguage'] = GetPrimaryLanguage |
| 691 env.filters['GetSublanguage'] = GetSublanguage |
| 692 |
| 693 # Register the message map with jinja2.i18n extension. |
| 694 env.globals['IsRtlLanguage'] = IsRtlLanguage |
| 695 env.globals['SelectLanguage'] = message_map.MakeSelectLanguage() |
| 696 env.install_gettext_callables(message_map.MakeGetText(), |
| 697 message_map.MakeGetText()); |
| 698 |
| 699 template = env.get_template(template_name) |
| 700 |
| 701 # Generate a separate file per each locale if requested. |
| 702 outputs = [] |
| 703 if options.locale_output: |
| 704 target = Template(options.locale_output) |
| 705 for lang in languages: |
| 706 context['languages'] = [ lang ] |
| 707 context['language'] = lang |
| 708 context['pak_suffix'] = GetDataPackageSuffix(lang) |
| 709 context['json_suffix'] = GetJsonSuffix(lang) |
| 710 |
| 711 template_file_name = target.safe_substitute(context) |
| 712 outputs.append(template_file_name) |
| 713 if not options.print_only: |
| 714 WriteIfChanged(template_file_name, template.render(context), |
| 715 options.encoding) |
| 716 else: |
| 717 outputs.append(options.output) |
| 718 if not options.print_only: |
| 719 WriteIfChanged(options.output, template.render(context), options.encoding) |
| 720 |
| 721 if options.print_only: |
| 722 # Quote each element so filename spaces don't mess up gyp's attempt to parse |
| 723 # it into a list. |
| 724 return " ".join(['"%s"' % x for x in outputs]) |
| 725 |
| 726 return |
682 | 727 |
683 | 728 |
684 def main(): | 729 def DoMain(argv): |
685 usage = "Usage: localize [options] <input> <output>" | 730 usage = "Usage: localize [options] locales" |
686 parser = OptionParser(usage=usage) | 731 parser = OptionParser(usage=usage) |
687 parser.add_option( | 732 parser.add_option( |
688 '-d', '--define', dest='define', action='append', type='string', | 733 '-d', '--define', dest='define', action='append', type='string', |
689 help='define a variable (VAR=VALUE).') | 734 help='define a variable (NAME=VALUE).') |
690 parser.add_option( | |
691 '-i', '--input', dest='input', action='append', type='string', | |
692 help='read variables from INPUT.') | |
693 parser.add_option( | |
694 '-l', '--language', dest='languages', action='append', type='string', | |
695 help='add LANGUAGE to the list of languages to use.') | |
696 parser.add_option( | 735 parser.add_option( |
697 '--encoding', dest='encoding', type='string', default='utf-16', | 736 '--encoding', dest='encoding', type='string', default='utf-16', |
698 help="set the encoding of <output>. 'utf-16' is the default.") | 737 help="set the encoding of <output>. 'utf-16' is the default.") |
699 parser.add_option( | 738 parser.add_option( |
700 '--jinja2', dest='jinja2', type='string', | 739 '--jinja2', dest='jinja2', type='string', |
701 help="specifies path to the jinja2 library.") | 740 help="specifies path to the jinja2 library.") |
702 parser.add_option( | 741 parser.add_option( |
703 '--messages_path', dest='messages_path', type='string', | 742 '--locale_dir', dest='locale_dir', type='string', |
704 help="set path to localized messages.") | 743 help="set path to localized message files.") |
| 744 parser.add_option( |
| 745 '--locale_output', dest='locale_output', type='string', |
| 746 help='specify the per-locale output file name.') |
| 747 parser.add_option( |
| 748 '-o', '--output', dest='output', type='string', |
| 749 help="specify the output file name.") |
| 750 parser.add_option( |
| 751 '--print_only', dest='print_only', action='store_true', |
| 752 default=False, help='print the output file names only.') |
| 753 parser.add_option( |
| 754 '-t', '--template', dest='template', type='string', |
| 755 help="specify the template file name.") |
| 756 parser.add_option( |
| 757 '--variables', dest='variables', action='append', type='string', |
| 758 help='read variables (NAME=VALUE) from file.') |
705 | 759 |
706 options, args = parser.parse_args() | 760 options, locales = parser.parse_args(argv) |
707 if len(args) != 2: | 761 if not locales: |
708 parser.error('Two positional arguments (<input> and <output>) are expected') | 762 parser.error('At least one locale must be specified') |
709 if not options.languages: | 763 if bool(options.output) == bool(options.locale_output): |
710 parser.error('At least one language must be specified') | 764 parser.error( |
711 if not options.messages_path: | 765 'Either --output or --locale_output must be specified but not both') |
712 parser.error('--messages_path is required') | 766 if not options.template and not options.print_only: |
| 767 parser.error('The template name is required unless --print_only is used') |
713 | 768 |
714 return Localize(args[0], args[1], options) | 769 return Localize(options.template, locales, options) |
715 | 770 |
716 if __name__ == '__main__': | 771 if __name__ == '__main__': |
717 sys.exit(main()) | 772 sys.exit(DoMain(sys.argv[1:])) |
718 | 773 |
OLD | NEW |