extract_i18n 0.1.0

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.
@@ -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: []