i18n_template 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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