i18n_template 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. data/.gitignore +4 -0
  2. data/Gemfile.rails23_r187 +8 -0
  3. data/Gemfile.rails30_r193 +8 -0
  4. data/Gemfile.rails31_r193 +8 -0
  5. data/README.md +250 -0
  6. data/Rakefile +1 -0
  7. data/bin/i18n_template +5 -0
  8. data/i18n_template.gemspec +26 -0
  9. data/lib/i18n_template.rb +28 -0
  10. data/lib/i18n_template/document.rb +460 -0
  11. data/lib/i18n_template/extractor.rb +4 -0
  12. data/lib/i18n_template/extractor/base.rb +44 -0
  13. data/lib/i18n_template/extractor/gettext.rb +127 -0
  14. data/lib/i18n_template/extractor/plain.rb +43 -0
  15. data/lib/i18n_template/extractor/yaml.rb +53 -0
  16. data/lib/i18n_template/handler.rb +61 -0
  17. data/lib/i18n_template/node.rb +74 -0
  18. data/lib/i18n_template/railtie.rb +7 -0
  19. data/lib/i18n_template/runner.rb +61 -0
  20. data/lib/i18n_template/runner/base.rb +11 -0
  21. data/lib/i18n_template/runner/extract_phrases.rb +70 -0
  22. data/lib/i18n_template/tasks.rb +2 -0
  23. data/lib/i18n_template/translation.rb +62 -0
  24. data/lib/i18n_template/translator.rb +5 -0
  25. data/lib/i18n_template/translator/i18n.rb +24 -0
  26. data/lib/i18n_template/version.rb +3 -0
  27. data/test/abstract_unit.rb +11 -0
  28. data/test/document_test.rb +316 -0
  29. data/test/fixtures/handling_if_blocks.yml +23 -0
  30. data/test/fixtures/ignored_markup.yml +15 -0
  31. data/test/fixtures/incorrect_node_markup.yml +17 -0
  32. data/test/fixtures/nested_nodes.yml +16 -0
  33. data/test/fixtures/nested_wrapped_text.yml +15 -0
  34. data/test/fixtures/phrase_fully_ignored.yml +14 -0
  35. data/test/fixtures/phrase_with_embed_words_and_scriptlet.yml +17 -0
  36. data/test/fixtures/phrase_with_single_char_to_ignore.yml +19 -0
  37. data/test/fixtures/replacing_br_with_newline.yml +15 -0
  38. data/test/fixtures/skipping_ignored_blocks.yml +15 -0
  39. data/test/fixtures/spans_as_phrases.yml +18 -0
  40. data/test/fixtures/table.yml +35 -0
  41. data/test/fixtures/text_with_braces.yml +17 -0
  42. data/test/fixtures/text_with_brackets.yml +17 -0
  43. data/test/fixtures/wrapped_key_propagation.yml +15 -0
  44. data/test/fixtures/wrapping_eval_blocks.yml +17 -0
  45. data/test/fixtures_rendering_test.rb +46 -0
  46. data/test/inline_rendering_test.rb +27 -0
  47. data/test/support/i18n_test_case_helper.rb +12 -0
  48. data/test/templates/_footer.html.erb +3 -0
  49. data/test/templates/greeting.html.erb +1 -0
  50. data/test/templates/layouts/application.html.erb +5 -0
  51. data/test/templates/users/_account.html.erb +3 -0
  52. data/test/templates/users/_profile.html.erb +6 -0
  53. data/test/templates/users/index.html.erb +5 -0
  54. data/test/templates/users/show.html.erb +4 -0
  55. data/test/templates_rendering_test.rb +81 -0
  56. data/test/translate_test.rb +72 -0
  57. metadata +156 -0
@@ -0,0 +1,4 @@
1
+ require 'i18n_template/extractor/base'
2
+ require 'i18n_template/extractor/plain'
3
+ require 'i18n_template/extractor/gettext'
4
+ require 'i18n_template/extractor/yaml'
@@ -0,0 +1,44 @@
1
+ require "fileutils"
2
+
3
+ module I18nTemplate
4
+ module Extractor
5
+ class Base
6
+ class << self
7
+ def default_options
8
+ {
9
+ :glob => ['app/views/**/*.{erb,rhtml}'],
10
+ :format => 'gettext'
11
+ }
12
+ end
13
+ end
14
+
15
+ def initialize(options)
16
+ @options = self.class.default_options.dup
17
+ @options.merge!(options)
18
+ end
19
+
20
+ def call(source)
21
+ raise NotImplementedError, "'call' is not implemented by #{self.class.name}"
22
+ end
23
+
24
+ protected
25
+
26
+ def log(message)
27
+ puts message if @options[:verbose]
28
+ end
29
+
30
+ def extract_phrases(filename)
31
+ log "Processing #{filename}"
32
+ source = File.read(filename)
33
+ document = ::I18nTemplate::Document.new(source)
34
+ document.preprocess!
35
+
36
+ document.warnings.each { |warning| log(warning) } if @options[:verbose]
37
+
38
+ document.phrases
39
+ end
40
+
41
+ end
42
+ end
43
+ end
44
+
@@ -0,0 +1,127 @@
1
+ module I18nTemplate
2
+ module Extractor
3
+ #
4
+ # Extract phrases to gettext format. E.g. pot/po files
5
+ #
6
+ # Extractor creates and updates:
7
+ # * {po_root}/{textdomain}.pot
8
+ # * {pr_root}/{lang}/{textdomain}.po
9
+ class Gettext < Base
10
+
11
+ I18nTemplate.extractors << self
12
+
13
+ class << self
14
+ def format
15
+ 'gettext'
16
+ end
17
+
18
+ def default_options
19
+ super.merge({
20
+ :po_root => 'po',
21
+ :textdomain => 'phrases'
22
+ })
23
+ end
24
+ end
25
+
26
+ def initialize(options)
27
+ super(options)
28
+
29
+ @version = 'version 0.0.1'
30
+ @pot_file = File.join(@options[:po_root], "#{@options[:textdomain]}.pot")
31
+ @pot_file_tmp = "#{@pot_file}.tmp"
32
+ end
33
+
34
+ def call(paths)
35
+ # ensure root directory exists
36
+ FileUtils.mkdir_p(@options[:po_root])
37
+
38
+ # generate new temporary pot file
39
+ File.open(@pot_file_tmp, "w") do |f|
40
+ f.puts generate_pot_header
41
+ f.puts ""
42
+ f.puts generate_pot_body(paths)
43
+ end
44
+
45
+ # merge pot file
46
+ log("Merging #{@pot_file}")
47
+ if File.exist?(@pot_file)
48
+ merge_po_files(@pot_file, @pot_file_tmp)
49
+ else
50
+ FileUtils.cp(@pot_file_tmp, @pot_file)
51
+ end
52
+
53
+ # merge po files
54
+ Dir.glob("#{@options[:po_root]}/*/#{@options[:textdomain]}.po") do |po_file|
55
+ log("Merging #{po_file}")
56
+ concate_po_files(po_file, @pot_file_tmp)
57
+ end
58
+
59
+ # remove temporary pot file
60
+ FileUtils.rm(@pot_file_tmp)
61
+ end
62
+
63
+ protected
64
+
65
+ def generate_pot_header
66
+ time = Time.now.strftime("%Y-%m-%d %H:%M")
67
+ off = Time.now.utc_offset
68
+ sign = off <= 0 ? '-' : '+'
69
+ time += sprintf('%s%02d%02d', sign, *(off.abs / 60).divmod(60))
70
+
71
+ <<-TITLE
72
+ # SOME DESCRIPTIVE TITLE.
73
+ # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
74
+ # This file is distributed under the same license as the PACKAGE package.
75
+ # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
76
+ #
77
+ #, fuzzy
78
+ msgid ""
79
+ msgstr ""
80
+ "Project-Id-Version: PACKAGE VERSION\\n"
81
+ "POT-Creation-Date: #{time}\\n"
82
+ "PO-Revision-Date: #{time}\\n"
83
+ "Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n"
84
+ "Language-Team: LANGUAGE <LL@li.org>\\n"
85
+ "MIME-Version: 1.0\\n"
86
+ "Content-Type: text/plain; charset=UTF-8\\n"
87
+ "Content-Transfer-Encoding: 8bit\\n"
88
+ "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\\n"
89
+ TITLE
90
+ end
91
+
92
+ def generate_pot_body(paths)
93
+ sources = {}
94
+
95
+ paths.each do |path|
96
+ phrases = extract_phrases(path)
97
+ phrases.each do |phrase|
98
+ sources[phrase] ||= []
99
+ sources[phrase] << path
100
+ end
101
+ end
102
+
103
+ log("Extracted #{sources.keys.size} phrases")
104
+
105
+ data = ""
106
+ sources.sort.each do |phrase, paths|
107
+ data << "# #{paths.join(",")}\n"
108
+ data << "msgid #{phrase.inspect}\n"
109
+ data << "msgstr \"\"\n"
110
+ data << "\n"
111
+ end
112
+ data
113
+ end
114
+
115
+ def concate_po_files(def_po, ref_po)
116
+ command = "msgcat --output-file #{def_po} --use-first #{def_po} #{ref_po}"
117
+ system(command) or raise RuntimeError, "can't run #{command.inspect}"
118
+ end
119
+
120
+ def merge_po_files(def_po, ref_po)
121
+ command = "msgmerge --quiet --update #{def_po} #{ref_po}"
122
+ system(command) or raise RuntimeError, "can't run #{command.inspect}"
123
+ end
124
+
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,43 @@
1
+ module I18nTemplate
2
+ module Extractor
3
+ # Extract phrases to plain format:
4
+ # Each phrase per line
5
+ class Plain < Base
6
+
7
+ I18nTemplate.extractors << self
8
+
9
+ class << self
10
+ def format
11
+ 'plain'
12
+ end
13
+
14
+ def default_options
15
+ super.merge({
16
+ :output_file => 'phrases.txt'
17
+ })
18
+ end
19
+ end
20
+
21
+ def call(paths)
22
+ sources = {}
23
+
24
+ paths.each do |path|
25
+ phrases = extract_phrases(path)
26
+ phrases.each do |phrase|
27
+ sources[phrase] ||= []
28
+ sources[phrase] << path
29
+ end
30
+ end
31
+
32
+ log "Extracting #{sources.keys.size} phrases to #{@options[:output_file]}"
33
+ File.open(@options[:output_file], "w") do |f|
34
+ sources.sort.each do |phrase, paths|
35
+ f << "# #{paths.join(",")}\n"
36
+ f << phrase
37
+ f << "\n"
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,53 @@
1
+ require 'yaml'
2
+
3
+ module I18nTemplate
4
+ module Extractor
5
+ # Extract phrases to yaml format. E.g:
6
+ # config/locales/templates.yml
7
+ class Yaml < Base
8
+
9
+ I18nTemplate.extractors << self
10
+
11
+ class << self
12
+ def format
13
+ 'yaml'
14
+ end
15
+
16
+ def default_options
17
+ super.merge({
18
+ :locales_root => 'config/locales'
19
+ })
20
+ end
21
+ end
22
+
23
+ def call(paths)
24
+ # ensure root directory exists
25
+ FileUtils.mkdir_p(@options[:locales_root])
26
+ output_file = File.join(@options[:locales_root], 'phrases.yml')
27
+
28
+ # extract phrases
29
+ phrases = []
30
+ paths.each do |path|
31
+ phrases += extract_phrases(path)
32
+ end
33
+ phrases.uniq!
34
+
35
+ # update phrases
36
+ log "Extracting #{phrases.size} phrases to #{output_file}"
37
+ data = File.exists?(output_file) ? YAML.load_file(output_file) : {}
38
+ data['en'] ||= {}
39
+ data.keys.each do |locale|
40
+ phrases.each do |phrase|
41
+ data[locale][phrase] ||= nil
42
+ end
43
+ end
44
+
45
+ # store data
46
+ File.open(output_file, "w") do |f|
47
+ f << data.to_yaml
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+
@@ -0,0 +1,61 @@
1
+ module I18nTemplate
2
+ ##
3
+ # Handler is ActionView wrapper for erb handler.
4
+ # If internationalize? returns true
5
+ # it calls erb handler with internationalized template
6
+ # otherwise it calls handler with regular template
7
+ class Handler
8
+ def initialize(options = {})
9
+ @options = options
10
+ @default_format = ::Mime::HTML
11
+ init_erb_handler
12
+ end
13
+
14
+ # default format
15
+ attr_reader :default_format
16
+
17
+ # erb handler
18
+ attr_reader :erb_handler
19
+
20
+ # call method implements ActionView::Template handler interface
21
+ def call(template)
22
+ if internationalize?(template)
23
+ document = ::I18nTemplate::Document.new(template.source)
24
+ document.process!
25
+ document.warnings.each { |warning| $stderr.puts warning } if @options[:verbose]
26
+
27
+ erb_handler.call(document)
28
+ else
29
+ erb_handler.call(template)
30
+ end
31
+ end
32
+
33
+ protected
34
+
35
+ # check if template source should be internationalized
36
+ #
37
+ # @note
38
+ # if you need more control inherite and override this method
39
+ def internationalize?(template)
40
+ true
41
+ end
42
+
43
+ private
44
+
45
+ # init action_view erb handler
46
+ def init_erb_handler
47
+ require 'action_pack/version'
48
+
49
+ case ActionPack::VERSION::MAJOR
50
+ when 2
51
+ require 'action_view/template_handlers/erb'
52
+ @erb_handler = ::ActionView::TemplateHandlers::ERB
53
+ when 3
54
+ require 'action_view/template/handlers/erb'
55
+ @erb_handler = ::ActionView::Template::Handlers::ERB
56
+ else
57
+ raise NotImplementedError, "Can't init erb_handler for action_pack v#{ActionPack::VERSION::STRING}"
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,74 @@
1
+ module I18nTemplate
2
+ ##
3
+ # Document processing node
4
+ class Node
5
+ # The array of children of this node. Not all nodes have children.
6
+ attr_reader :children
7
+
8
+ # The parent node of this node. All nodes have a parent, except for the
9
+ # root node.
10
+ attr_reader :parent
11
+
12
+ # The line number of the input where this node was begun
13
+ attr_reader :line
14
+
15
+ # The byte position in the input where this node was begun
16
+ attr_reader :position
17
+
18
+ # Node token
19
+ attr_reader :token
20
+
21
+ attr_accessor :tag
22
+ attr_accessor :phrase
23
+
24
+ # Create a new node as a child of the given parent.
25
+ def initialize(parent, line = 0, position = 0, token = nil, tag = nil, &block)
26
+ @parent = parent
27
+ @children = []
28
+ @line, @position = line, position
29
+ @token = token
30
+ @tag = tag
31
+ instance_eval(&block) if block_given?
32
+ end
33
+
34
+ # tag node?
35
+ def tag?
36
+ !@tag.nil?
37
+ end
38
+
39
+ # text node?
40
+ def text?
41
+ @tag.nil?
42
+ end
43
+
44
+ # root node?
45
+ def root?
46
+ parent == self
47
+ end
48
+
49
+ # Return descendant text if tag node is wrapper node for some text node
50
+ # @return 'text' for
51
+ # <div><span><b>text</b></span><div>
52
+ # @return nil for
53
+ # <div><span><b>text</b></span><p>data</p><div>
54
+ def wrapped_node_text
55
+ node = self
56
+ while node.children.size == 1
57
+ node = node.children.first
58
+ return node.token if node.text?
59
+ end
60
+ return nil
61
+ end
62
+
63
+ def descendants_text
64
+ output = ""
65
+ children.each do |child|
66
+ output << child.token if child.text?
67
+ child.children.each do |node|
68
+ output << node.descendants_text
69
+ end
70
+ end
71
+ output
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,7 @@
1
+ module I18nTemplate
2
+ class Railtie < ::Rails::Railtie
3
+ rake_tasks do
4
+ require 'i18n_template/tasks'
5
+ end
6
+ end
7
+ end if defined?(Rails::Railtie)
@@ -0,0 +1,61 @@
1
+ require 'i18n_template/runner/base'
2
+ require 'i18n_template/runner/extract_phrases'
3
+
4
+ require 'optparse'
5
+ module I18nTemplate
6
+ module Runner
7
+ class << self
8
+
9
+ def run
10
+ options = {}
11
+
12
+ option_parser = OptionParser.new do |op|
13
+ op.banner = "Usage: #{File.basename($0)} COMMAND [OPTIONS]"
14
+
15
+ I18nTemplate.runners.each do |runner|
16
+ op.separator ""
17
+ op.separator "#{runner.command} - #{runner.description}"
18
+ runner.add_options!(op, options)
19
+ end
20
+
21
+ op.separator ""
22
+ op.on(
23
+ "--verbose",
24
+ "turn on verbosity"
25
+ ) { |v| options[:verbose] = true }
26
+
27
+ op.separator ""
28
+ op.on_tail("-h", "--help", "Show this message") { puts op; exit }
29
+ op.on_tail('-v', '--version', "Show version") { puts I18nTemplate::VERSION; exit }
30
+ end
31
+
32
+ begin
33
+ option_parser.parse!(ARGV)
34
+ rescue OptionParser::ParseError => e
35
+ warn e.message
36
+ puts option_parser
37
+ exit 1
38
+ end
39
+
40
+ I18nTemplate.runners.each do |runner|
41
+ runner.default_options.each do |key, value|
42
+ options[key] = value unless options.key?(key)
43
+ end
44
+ end
45
+
46
+ command = ARGV.first
47
+
48
+ runner = I18nTemplate.runners.detect { |klass| klass.command == command }
49
+
50
+ unless runner
51
+ warn "Unknown command '#{command}'"
52
+ puts option_parser
53
+ exit 1
54
+ end
55
+
56
+ runner.new(options).run
57
+ end
58
+ end
59
+
60
+ end
61
+ end