extract_i18n 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ExtractI18n::Slimkeyfy::VueTransformer < ExtractI18n::Slimkeyfy::SlimTransformer
4
+ BEFORE = /(?<before>.*[\( ^])/.freeze
5
+ HTML_ARGUMENTS = {
6
+ placeholder: /(?<html_tag>[a-z\-]*placeholder=\s*)/,
7
+ title: /(?<html_tag>title=\s*)/,
8
+ kind_of_title: /(?<html_tag>[a-z\-]+title=\s*)/,
9
+ label: /(?<html_tag>[a-z\-]*label=\s*)/,
10
+ description: /(?<html_tag>description=\s*)/,
11
+ alt: /(?<html_tag>alt=\s*)/,
12
+ prepend: /(?<html_tag>prepend=\s*)/,
13
+ append: /(?<html_tag>append=\s*)/,
14
+ }.freeze
15
+
16
+ def regex_list
17
+ HTML_ARGUMENTS.map { |_, regex| /#{BEFORE}#{regex}#{TRANSLATION}#{AFTER}/ }
18
+ end
19
+
20
+ def parse_html(&_block)
21
+ return @word.line if @word.line.match(TRANSLATED)
22
+ return @word.line if @word.tail.join(" ")[/^{{.*}}$/]
23
+
24
+ body = @word.tail.join(" ")
25
+ body, tagged_with_equals = ExtractI18n::Slimkeyfy::Whitespacer.convert_nbsp(body, @word.head)
26
+
27
+ tagged_with_equals = "|" if tagged_with_equals == "="
28
+
29
+ interpolate_arguments, body = extract_arguments(body)
30
+
31
+ change = ExtractI18n::SourceChange.new(
32
+ source_line: @word.line,
33
+ remove: body,
34
+ interpolate_arguments: interpolate_arguments,
35
+ interpolation_type: :vue,
36
+ i18n_key: "#{@file_key}.#{ExtractI18n.key(body)}",
37
+ i18n_string: body,
38
+ t_template: "#{@word.indentation}#{tagged_with_equals} {{ $t('%s'%s) }}"
39
+ )
40
+ yield(change)
41
+ end
42
+
43
+ def parse_html_arguments(line, token_skipped_before = nil, &block)
44
+ final_line = line
45
+ regex_list.each do |regex|
46
+ line.scan(regex) do |m_data|
47
+ next if m_data == token_skipped_before
48
+ before = m_data[0]
49
+ if before[-1] == ":" # already dynamic attribute
50
+ next
51
+ end
52
+ html_tag = m_data[1]
53
+ translation = match_string(m_data[2])
54
+ after = m_data[3]
55
+ interpolate_arguments, translation = extract_arguments(translation)
56
+
57
+ change = ExtractI18n::SourceChange.new(
58
+ source_line: final_line,
59
+ i18n_string: translation,
60
+ i18n_key: "#{@file_key}.#{ExtractI18n.key(translation)}",
61
+ remove: m_data[2],
62
+ interpolate_arguments: interpolate_arguments,
63
+ interpolation_type: :vue,
64
+ t_template: "#{before}:#{html_tag}\"$t('%s'%s)\"#{after}"
65
+ )
66
+ final_line = yield(change)
67
+ return parse_html_arguments(final_line, m_data, &block)
68
+ end
69
+ end
70
+ @word.indentation + final_line
71
+ end
72
+
73
+ def extract_arguments(translation)
74
+ args = {}
75
+ translation.scan(/\{\{([^}]*)\}\}/).each_with_index do |stripped_arg, index|
76
+ arg = Regexp.last_match[0]
77
+ key = ExtractI18n.key(arg)
78
+ key = key + index.to_s if index > 0
79
+ translation = translation.gsub(arg, "{#{key}}")
80
+ args[key] = stripped_arg[0]
81
+ end
82
+ [args, translation]
83
+ end
84
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ExtractI18n
4
+ class Slimkeyfy::Whitespacer
5
+ HTML = /([a-z\.]+[0-9\-]*)+/.freeze
6
+ HTML_TAGS = /^(?<html_tag>'|\||#{HTML})/.freeze
7
+
8
+ def self.convert_slim(string)
9
+ m = string.match(HTML_TAGS)
10
+
11
+ return string if m.nil? or m[:html_tag].nil?
12
+ return string if has_equals_tag?(string, m[:html_tag])
13
+ tag = m[:html_tag]
14
+
15
+ case tag
16
+ when /\|/
17
+ string.gsub("|", "=")
18
+ when /\'/
19
+ string.gsub("'", "=>")
20
+ when HTML
21
+ string.gsub(string, "#{string} =")
22
+ else string end
23
+ end
24
+
25
+ def self.convert_nbsp(body, tag)
26
+ lead = leading_nbsp(body)
27
+ trail = trailing_nsbp(body, tag)
28
+
29
+ tag = tag.gsub(tag, "#{tag}#{lead}#{trail}")
30
+ [body.sub(/^&nbsp;/, '').sub(/&nbsp;$/, '').gsub("&nbsp;", " "), tag.gsub("=><", "=<>")]
31
+ end
32
+
33
+ def self.leading_nbsp(body)
34
+ return "<" if body.start_with?("&nbsp;")
35
+ end
36
+
37
+ def self.trailing_nsbp(body, tag)
38
+ return '' if tag.start_with?("=>")
39
+ return ">" if body.end_with?("&nbsp;")
40
+ end
41
+
42
+ def self.has_equals_tag?(string, html_tag)
43
+ string.gsub(html_tag, "").strip.start_with?("=")
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ExtractI18n::Slimkeyfy
4
+ class Word
5
+ attr_reader :line, :tokens, :indentation
6
+
7
+ def self.for(extension)
8
+ if extension == 'vue'
9
+ JsWord
10
+ else
11
+ Word
12
+ end
13
+ end
14
+
15
+ def initialize(line)
16
+ @line = line
17
+ @indentation = " " * (@line.size - unindented_line.size)
18
+ end
19
+
20
+ def as_list
21
+ # check for div.foo-bar(foo=bar)
22
+ has_html_form = !!@line[/^ *[\.#]?[a-z\.#-]+\(/]
23
+ delimiter_items =
24
+ @line.
25
+ sub(/^(\s*\|)(\w)/, "\\1 \\2"). # add a whitespace to support "|string"
26
+ split(has_html_form ? /(?<=[\(])| +/ : ' '). # split by whitespace or ( but keep (
27
+ drop_while { |i| i == "" } # .. but that leaves leading ""
28
+ items = []
29
+ # div: div
30
+ delimiter_items.reverse_each do |item|
31
+ if item[/^([a-z]|\.[a-z]|#[a-z]).*:/] and items.length > 0
32
+ items[-1] = "#{item} #{items[-1]}"
33
+ else
34
+ items << item
35
+ end
36
+ end
37
+ items.reverse
38
+ end
39
+
40
+ def unindented_line
41
+ @line.sub(/^\s*/, "")
42
+ end
43
+
44
+ def head
45
+ as_list.first
46
+ end
47
+
48
+ def tail
49
+ as_list.drop(1)
50
+ end
51
+
52
+ def extract_arguments(translation)
53
+ args = {}
54
+ translation.scan(/\#{[^}]*}/).each_with_index do |arg, index|
55
+ stripped_arg = arg[2..-2]
56
+ key = arg[/\w+/]
57
+ key += index.to_s if index > 0
58
+ translation = translation.gsub(arg, "%{#{key}}")
59
+ args[key] = stripped_arg
60
+ end
61
+ [args, translation]
62
+ end
63
+
64
+ def extract_updated_key(translation_key_with_base)
65
+ return "" if translation_key_with_base.blank?
66
+ translation_key_with_base.split(".").last
67
+ end
68
+ end
69
+
70
+ class JsWord < Word
71
+ def initialize(*args)
72
+ super
73
+ @use_absolute_key = true
74
+ end
75
+
76
+ def extract_arguments(translation)
77
+ args = {}
78
+ translation.scan(/\{\{([^}]*)\}\}/).each_with_index do |stripped_arg, index|
79
+ arg = Regexp.last_match[0]
80
+ key = arg[/\w+/]
81
+ key += index.to_s if index > 0
82
+ translation = translation.gsub(arg, "{#{key}}")
83
+ args[key] = stripped_arg[0]
84
+ end
85
+ [args, translation]
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pastel'
4
+
5
+ module ExtractI18n
6
+ class SourceChange
7
+ # Data class for a proposed source change
8
+
9
+ PASTEL = Pastel.new
10
+
11
+ attr_reader :key, :i18n_string
12
+
13
+ # @param i18n_key [String]
14
+ # "models.foo.bar.button_text"
15
+ # @param interpolate_arguments [Hash]
16
+ # { "date" => "Date.new.to_s" }
17
+ # @param source_line [String]
18
+ # original souce line to modify for viewing purposes
19
+ # @param remove [String]
20
+ # what piece of source_line to replace
21
+ # @param t_template [String]
22
+ # how to format the replacement translation, use 2 placeholder %s for the string and for the optional arguments
23
+ # @param interpolation_type [Symbol]
24
+ # :ruby or :vue
25
+ def initialize(
26
+ i18n_key:,
27
+ i18n_string:,
28
+ interpolate_arguments:,
29
+ source_line:,
30
+ remove:,
31
+ t_template: %{I18n.t("%s"%s)},
32
+ interpolation_type: :ruby
33
+ )
34
+ @i18n_string = i18n_string
35
+ @key = i18n_key
36
+ @interpolate_arguments = interpolate_arguments
37
+ @source_line = source_line
38
+ @remove = remove
39
+ @t_template = t_template
40
+ @interpolation_type = interpolation_type
41
+ end
42
+
43
+ def format
44
+ s = ""
45
+ s += PASTEL.cyan("replace: ") + PASTEL.blue(@source_line).
46
+ gsub(@remove, PASTEL.red(@remove))
47
+ unless @source_line.include?("\n")
48
+ s += "\n"
49
+ end
50
+ s += PASTEL.cyan("with: ") + PASTEL.blue(@source_line).
51
+ gsub(@remove, PASTEL.green(i18n_t))
52
+ unless @source_line.include?("\n")
53
+ s += "\n"
54
+ end
55
+ s += PASTEL.cyan("add i18n: ") + PASTEL.blue("#{@key}: #{@i18n_string}")
56
+ s
57
+ end
58
+
59
+ def i18n_t(relative: false)
60
+ i18n_key = if relative
61
+ "." + key.split('.').last
62
+ else
63
+ key
64
+ end
65
+ sprintf(@t_template, i18n_key, i18n_arguments_string)
66
+ end
67
+
68
+ def increment_key!
69
+ if @key[-1][/(.*)([0-9]+)$/]
70
+ rest = $1
71
+ number = $2.to_i
72
+ else
73
+ number = 1
74
+ rest = @key
75
+ end
76
+ @key = "#{rest}#{number + 1}"
77
+ end
78
+
79
+ def i18n_arguments_string
80
+ case @interpolation_type
81
+ when :ruby
82
+ if @interpolate_arguments.keys.length > 0
83
+ ", " + @interpolate_arguments.map { |k, v| "#{k}: (#{v})" }.join(', ')
84
+ else
85
+ ""
86
+ end
87
+ when :vue
88
+ if @interpolate_arguments.keys.length > 0
89
+ ", { " + @interpolate_arguments.map { |k, v| "#{k}: (#{v.strip})" }.join(', ') + " }"
90
+ else
91
+ ""
92
+ end
93
+ else
94
+ raise NotImplementedError, @interpolation_type
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,3 @@
1
+ module ExtractI18n
2
+ VERSION = "0.1.0"
3
+ end
metadata ADDED
@@ -0,0 +1,156 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: extract_i18n
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Stefan Wienert
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2020-10-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: parser
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '2.6'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '2.6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: slim
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: tty-prompt
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: zeitwerk
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: diff-lcs
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: diffy
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description: Extact i18n from Ruby files using Ruby parser and slim files using regex
98
+ interactively
99
+ email:
100
+ - info@stefanwienert.de
101
+ executables:
102
+ - extract-i18n
103
+ extensions: []
104
+ extra_rdoc_files: []
105
+ files:
106
+ - ".github/workflows/verify.yml"
107
+ - ".gitignore"
108
+ - ".rspec"
109
+ - ".rubocop.yml"
110
+ - Gemfile
111
+ - LICENSE.txt
112
+ - README.md
113
+ - Rakefile
114
+ - bin/console
115
+ - bin/setup
116
+ - exe/extract-i18n
117
+ - extract_i18n.gemspec
118
+ - lib/extract_i18n.rb
119
+ - lib/extract_i18n/adapters/adapter.rb
120
+ - lib/extract_i18n/adapters/ruby_adapter.rb
121
+ - lib/extract_i18n/adapters/slim_adapter.rb
122
+ - lib/extract_i18n/adapters/slim_adapter_wip.rb
123
+ - lib/extract_i18n/adapters/vue_adapter.rb
124
+ - lib/extract_i18n/cli.rb
125
+ - lib/extract_i18n/file_processor.rb
126
+ - lib/extract_i18n/slimkeyfy/slim_transformer.rb
127
+ - lib/extract_i18n/slimkeyfy/vue_transformer.rb
128
+ - lib/extract_i18n/slimkeyfy/whitespacer.rb
129
+ - lib/extract_i18n/slimkeyfy/word.rb
130
+ - lib/extract_i18n/source_change.rb
131
+ - lib/extract_i18n/version.rb
132
+ homepage: https://github.com/pludoni/extract_i18n
133
+ licenses:
134
+ - MIT
135
+ metadata: {}
136
+ post_install_message:
137
+ rdoc_options: []
138
+ require_paths:
139
+ - lib
140
+ required_ruby_version: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
145
+ required_rubygems_version: !ruby/object:Gem::Requirement
146
+ requirements:
147
+ - - ">="
148
+ - !ruby/object:Gem::Version
149
+ version: '0'
150
+ requirements: []
151
+ rubyforge_project:
152
+ rubygems_version: 2.7.6
153
+ signing_key:
154
+ specification_version: 4
155
+ summary: Extact i18n from Ruby files using Ruby parser and slim files using regex
156
+ test_files: []