extract_i18n 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/verify.yml +23 -0
- data/.gitignore +9 -0
- data/.rspec +1 -0
- data/.rubocop.yml +35 -0
- data/Gemfile +14 -0
- data/LICENSE.txt +21 -0
- data/README.md +52 -0
- data/Rakefile +2 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/extract-i18n +7 -0
- data/extract_i18n.gemspec +34 -0
- data/lib/extract_i18n.rb +41 -0
- data/lib/extract_i18n/adapters/adapter.rb +35 -0
- data/lib/extract_i18n/adapters/ruby_adapter.rb +126 -0
- data/lib/extract_i18n/adapters/slim_adapter.rb +70 -0
- data/lib/extract_i18n/adapters/slim_adapter_wip.rb +130 -0
- data/lib/extract_i18n/adapters/vue_adapter.rb +31 -0
- data/lib/extract_i18n/cli.rb +76 -0
- data/lib/extract_i18n/file_processor.rb +99 -0
- data/lib/extract_i18n/slimkeyfy/slim_transformer.rb +151 -0
- data/lib/extract_i18n/slimkeyfy/vue_transformer.rb +84 -0
- data/lib/extract_i18n/slimkeyfy/whitespacer.rb +46 -0
- data/lib/extract_i18n/slimkeyfy/word.rb +88 -0
- data/lib/extract_i18n/source_change.rb +98 -0
- data/lib/extract_i18n/version.rb +3 -0
- metadata +156 -0
@@ -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(/^ /, '').sub(/ $/, '').gsub(" ", " "), tag.gsub("=><", "=<>")]
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.leading_nbsp(body)
|
34
|
+
return "<" if body.start_with?(" ")
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.trailing_nsbp(body, tag)
|
38
|
+
return '' if tag.start_with?("=>")
|
39
|
+
return ">" if body.end_with?(" ")
|
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
|
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: []
|