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,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ExtractI18n::Adapters
4
+ class SlimAdapter < Adapter
5
+ def self.supports_relative_keys?
6
+ true
7
+ end
8
+
9
+ def run(original_content)
10
+ @content = self.class.join_multiline(original_content.split("\n"))
11
+ @content << "" # new line at the end
12
+ @transformer = ExtractI18n::Slimkeyfy::SlimTransformer
13
+
14
+ @new_content =
15
+ @content.map do |old_line|
16
+ process_line(old_line)
17
+ end
18
+ @new_content.join("\n")
19
+ end
20
+
21
+ def process_line(old_line)
22
+ word = ExtractI18n::Slimkeyfy::Word.for('.slim').new(old_line)
23
+ ExtractI18n::Slimkeyfy::SlimTransformer.new(word, @file_key).transform do |change|
24
+ if change.nil? # nothing to do
25
+ return old_line
26
+ end
27
+
28
+ if @on_ask.call(change)
29
+ change.i18n_t(relative: @options[:relative])
30
+ else
31
+ old_line
32
+ end
33
+ end
34
+ end
35
+
36
+ def self.join_multiline(strings_array)
37
+ result = []
38
+ joining_str = ''
39
+ indent_length = 0
40
+ long_str_start = /^[ ]+\| */
41
+ long_str_indent = /^[ ]+/
42
+ long_str_indent_with_vertical_bar = /^[ ]+\| */
43
+ strings_array.each do |str|
44
+ if joining_str.empty?
45
+ if str[long_str_start]
46
+ joining_str = str
47
+ indent_length = str[long_str_start].length
48
+ else
49
+ result << str
50
+ end
51
+ # multiline string continues with spaces
52
+ elsif str[long_str_indent] && str[long_str_indent].length.to_i >= indent_length
53
+ joining_str << str.gsub(long_str_indent, ' ')
54
+ # muliline string continues with spaces and vertical bar with same indentation
55
+ elsif str[long_str_indent_with_vertical_bar] && str[long_str_indent_with_vertical_bar].length.to_i == indent_length
56
+ joining_str << str.gsub(long_str_indent_with_vertical_bar, ' ')
57
+ # multiline string ends
58
+ else
59
+ result << joining_str
60
+ joining_str = ''
61
+ indent_length = 0
62
+ result << str
63
+ end
64
+ end
65
+ result << joining_str unless joining_str.empty?
66
+
67
+ result
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'slim'
4
+
5
+ module ExtractI18n::Adapters
6
+ class SlimAdapterWip < Adapter
7
+ def run(original_content)
8
+ parser = SlimRewriterParser.new(file_key: file_key, on_ask: on_ask)
9
+ parser.call(original_content)
10
+ end
11
+ end
12
+
13
+ class SlimRewriterParser < Slim::Engine
14
+ class CustomFilter < Slim::Filter
15
+ define_options file_key: :dynamic, on_ask: :dynamic
16
+ def call(exp)
17
+ @indent = 0
18
+ super.to_s
19
+ end
20
+
21
+ def on_multi(*exps)
22
+ super[1..-1].join
23
+ end
24
+
25
+ def on_newline
26
+ "\n"
27
+ end
28
+
29
+ def on_static(*args)
30
+ args[0].include?('"') ? "'#{args.join(' ')}'" : %["#{args.join(' ')}"]
31
+ end
32
+
33
+ def on_html_doctype(name)
34
+ "#{indent}doctype #{name}"
35
+ end
36
+
37
+ def on_html_tag(name, attrs, content = nil)
38
+ ret = "#{indent}#{name}#{compile attrs}"
39
+ if content
40
+ ret << ' ' << block(content)
41
+ end
42
+ ret
43
+ end
44
+
45
+ def block(content)
46
+ @indent += 1
47
+ result = compile(content).to_s # todo remove to_s
48
+ if result.is_a?(String)
49
+ extract_from_string(result)
50
+ else
51
+ result
52
+ end
53
+ ensure
54
+ @indent -= 1
55
+ end
56
+
57
+ def extract_from_string(string)
58
+ i18n_key = ExtractI18n.key(string)
59
+ change = ExtractI18n::SourceChange.new(
60
+ i18n_key: "#{@options[:file_key]}.#{i18n_key}",
61
+ i18n_string: string,
62
+ interpolate_arguments: {},
63
+ source_line: string,
64
+ remove: string
65
+ )
66
+ puts change.format
67
+ if @options[:on_ask].call(change)
68
+ change.i18n_t
69
+ else
70
+ string
71
+ end
72
+ end
73
+
74
+ def on_html_attrs(*attrs)
75
+ attrs.empty? ? '' : "(#{super[2..-1].join(' ')})"
76
+ end
77
+
78
+ def on_slim_interpolate(text)
79
+ compile(text)
80
+ end
81
+
82
+ def on_slim_text(type, text)
83
+ block(text)
84
+ end
85
+
86
+ def on_slim_embedded(type, text)
87
+ "#{indent}#{block text}"
88
+ end
89
+
90
+ def on_html_comment(exp)
91
+ "#{indent}/ #{block exp}"
92
+ end
93
+
94
+ def on_slim_control(line, block)
95
+ "#{indent}- #{line}#{block block}"
96
+ end
97
+
98
+ def on_html_attr(name, content)
99
+ "#{name}=#{compile content}"
100
+ end
101
+
102
+ def indent
103
+ ' ' * @indent
104
+ end
105
+
106
+ # def parse_text_block(first_line = nil, text_indent = nil)
107
+ # source_line = @_lines[@lineno - 1]
108
+
109
+ # binding.pry
110
+ # result = super
111
+
112
+ # change = ExtractI18n::SourceChange.new(
113
+ # i18n_key: "#{@file_key}.#{i18n_key}",
114
+ # i18n_string: i18n_string,
115
+ # interpolate_arguments: {},
116
+ # source_line: source_line,
117
+ # remove: node.loc.expression.source
118
+ # )
119
+ # binding.pry
120
+
121
+ # result
122
+ # end
123
+ end
124
+ remove //
125
+ filter :Encoding
126
+ filter :RemoveBOM
127
+ use Slim::Parser
128
+ use CustomFilter
129
+ end
130
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ExtractI18n::Adapters
4
+ class VueAdapter < SlimAdapter
5
+ def process_line(old_line)
6
+ @mode ||= :template
7
+ if old_line[/^<template/]
8
+ @mode = :template
9
+ elsif old_line[/^<script/]
10
+ @mode = :script
11
+ elsif old_line[/^<style/]
12
+ @mode = :style
13
+ end
14
+ if @mode != :template
15
+ return old_line
16
+ end
17
+ word = ExtractI18n::Slimkeyfy::Word.for('.vue').new(old_line)
18
+ ExtractI18n::Slimkeyfy::VueTransformer.new(word, @file_key).transform do |change|
19
+ if change.nil? # nothing to do
20
+ return old_line
21
+ end
22
+
23
+ if @on_ask.call(change)
24
+ change.i18n_t
25
+ else
26
+ old_line
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Layout/LineLength
4
+
5
+ require 'optparse'
6
+ require 'extract_i18n'
7
+ require 'extract_i18n/file_processor'
8
+ require 'extract_i18n/version'
9
+ require 'open-uri'
10
+
11
+ module ExtractI18n
12
+ # Cli Class
13
+ class CLI
14
+ def initialize
15
+ @options = {}
16
+ ARGV << '-h' if ARGV.empty?
17
+ OptionParser.new do |opts|
18
+ opts.banner = "Usage: extract-i18n -l <locale> -w <target-yml> [path*]"
19
+
20
+ opts.on('--version', 'Print version number') do
21
+ puts ExtractI18n::VERSION
22
+ exit 1
23
+ end
24
+
25
+ opts.on('-lLOCALE', '--locale=LOCALE', 'default locale for extraction (Default = en)') do |f|
26
+ @options[:locale] = f || 'en'
27
+ end
28
+
29
+ opts.on('-nNAMESPACE', '--namespace=NAMESPACE', 'Locale base key to wrap locations in') do |f|
30
+ @options[:base_key] = f
31
+ end
32
+
33
+ opts.on('-r', '--slim-relative', 'When activated, will use relative keys like t(".title")') do |f|
34
+ @options[:relative] = f
35
+ end
36
+
37
+ opts.on('-yYAML', '--yaml=YAML-FILE', 'Write extracted keys to YAML file (default = config/locales/unsorted.LOCALE.yml)') do |f|
38
+ @options[:write_to] = f || "config/locales/unsorted.#{@options[:locale]}"
39
+ end
40
+
41
+ opts.on('-h', '--help', 'Prints this help') do
42
+ puts opts
43
+ exit 1
44
+ end
45
+ end.parse!
46
+
47
+ @options[:write_to] ||= "config/locales/unsorted.#{@options[:locale]}.yml"
48
+ @options[:locale] ||= 'en'
49
+ @files = ARGV
50
+ end
51
+
52
+ def run
53
+ paths = @files.empty? ? [] : @files
54
+ paths.each do |path|
55
+ if File.directory?(path)
56
+ glob_path = File.join(path, '**', '*.rb')
57
+ Dir.glob(glob_path) do |file_path|
58
+ process_file file_path
59
+ end
60
+ else
61
+ process_file path
62
+ end
63
+ end
64
+ end
65
+
66
+ def process_file(file_path)
67
+ puts "Processing: #{file_path}"
68
+ ExtractI18n::FileProcessor.new(
69
+ file_path: file_path,
70
+ write_to: @options[:write_to],
71
+ locale: @options[:locale],
72
+ options: @options
73
+ ).run
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'parser/current'
4
+ require 'tty/prompt'
5
+ require 'diffy'
6
+ require 'yaml'
7
+
8
+ module ExtractI18n
9
+ class FileProcessor
10
+ PROMPT = TTY::Prompt.new
11
+ PASTEL = Pastel.new
12
+
13
+ def initialize(file_path:, write_to:, locale:, options: {})
14
+ @file_path = file_path
15
+ @file_key = ExtractI18n.file_key(@file_path)
16
+
17
+ @locale = locale
18
+ @write_to = write_to
19
+ @options = options
20
+ @i18n_changes = {}
21
+ end
22
+
23
+ def run
24
+ puts " reading #{@file_path}"
25
+ read_and_transform do |result|
26
+ puts Diffy::Diff.new(original_content, result, context: 1).to_s(:color)
27
+ if PROMPT.yes?("Save changes?")
28
+ File.write(@file_path, result)
29
+ update_i18n_yml_file
30
+ puts PASTEL.green("Saved #{@file_path}")
31
+ end
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def read_and_transform(&_block)
38
+ key = if @options[:namespace]
39
+ "#{@options[:namespace]}.#{@file_key}"
40
+ else
41
+ @file_key
42
+ end
43
+ adapter_class = ExtractI18n::Adapters::Adapter.for(@file_path)
44
+ if adapter_class
45
+ adapter = adapter_class.new(
46
+ file_key: key,
47
+ on_ask: ->(change) { ask_one_change?(change) },
48
+ options: @options,
49
+ )
50
+ output = adapter.run(original_content)
51
+ if output != original_content
52
+ yield(output)
53
+ end
54
+ end
55
+ end
56
+
57
+ def ask_one_change?(change)
58
+ check_for_unique!(change)
59
+ puts change.format
60
+ if PROMPT.no?("replace line ?")
61
+ false
62
+ else
63
+ @i18n_changes[change.key] = change.i18n_string
64
+ true
65
+ end
66
+ end
67
+
68
+ def check_for_unique!(change)
69
+ if @i18n_changes[change.key] && @i18n_changes[change.key] != change.i18n_string
70
+ change.increment_key!
71
+ check_for_unique!(change)
72
+ end
73
+ end
74
+
75
+ def update_i18n_yml_file
76
+ base = if File.exist?(@write_to)
77
+ YAML.load_file(@write_to)
78
+ else
79
+ {}
80
+ end
81
+ @i18n_changes.each do |key, value|
82
+ tree = base
83
+ keys = key.split('.').unshift(@locale)
84
+ keys.each_with_index do |part, i|
85
+ if i == keys.length - 1
86
+ tree[part] = value
87
+ else
88
+ tree = tree[part] ||= {}
89
+ end
90
+ end
91
+ end
92
+ File.write(@write_to, base.to_yaml)
93
+ end
94
+
95
+ def original_content
96
+ @original_content ||= File.read(@file_path)
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ExtractI18n
4
+ class Slimkeyfy::SlimTransformer
5
+ TRANSLATED = /t\s*\(?\s*(".*?"|'.*?')\s*\)?/.freeze
6
+ STRING = /(\".*\"|\'.*\')/.freeze
7
+ STRING_WITHOUT_QUOTES = /("(?<double_quot>.*)"|'(?<single_quot>.*)')/.freeze
8
+
9
+ HTML_TAGS = /^(?<html_tag>'|\||([a-z\.]+[0-9\-]*)+)/.freeze
10
+ EQUALS = /#?([a-z0-9\.\-\s]+)?\=.*/.freeze
11
+
12
+ BEFORE = /(?<before>.*)/.freeze
13
+ TRANSLATION = /(?<translation>(".*?"|'.*?'))/.freeze
14
+ AFTER = /(?<after>,?.*)?/.freeze
15
+
16
+ HTML_ARGUMENTS = {
17
+ hint: /(?<html_tag>hint:\s*)/,
18
+ link_to: /(?<html_tag>link_to\s*\(?)/,
19
+ inconified: /(?<html_tag>(iconified\s*\(?))/,
20
+ placeholder: /(?<html_tag>placeholder:\s*)/,
21
+ title: /(?<html_tag>title:\s*)/,
22
+ prepend: /(?<html_tag>prepend:\s*)/,
23
+ append: /(?<html_tag>append:\s*)/,
24
+ label: /(?<html_tag>[a-z]*_?label:\s*)/,
25
+ optionals: /(?<html_tag>(default|include_blank|alt):\s*)/,
26
+ input: /(?<html_tag>[a-z]*\.?input:?\s*)/,
27
+ button: /(?<html_tag>[a-z]*\.?button:?\s*(\:[a-z]+\s*,\s*)?)/,
28
+ tag: /(?<html_tag>(submit|content)_tag[\:\(]?\s*)/,
29
+ data_naive: /(?<html_tag>data:\s*\{\s*(confirm|content):\s*)/
30
+ }.freeze
31
+
32
+ LINK_TO = /#{HTML_ARGUMENTS[:link_to]}#{TRANSLATION}/.freeze
33
+
34
+ def transform(&block)
35
+ return yield(nil) if should_not_be_processed?(@word.as_list)
36
+ unindented_line = @word.unindented_line
37
+
38
+ if unindented_line.match(EQUALS)
39
+ parse_html_arguments(unindented_line, &block)
40
+ elsif @word.head.match(HTML_TAGS)
41
+ parse_html(&block)
42
+ else
43
+ yield(nil)
44
+ end
45
+ end
46
+
47
+ def initialize(word, file_key)
48
+ @word = word
49
+ @file_key = file_key
50
+ end
51
+
52
+ private
53
+
54
+ def parse_html(&_block)
55
+ return @word.line if @word.line.match(TRANSLATED)
56
+
57
+ tagged_with_equals = Slimkeyfy::Whitespacer.convert_slim(@word.head)
58
+ body = @word.tail.join(" ")
59
+ body, tagged_with_equals = Slimkeyfy::Whitespacer.convert_nbsp(body, tagged_with_equals)
60
+
61
+ if body.match(LINK_TO) != nil
62
+ body = link_tos(body)
63
+ end
64
+
65
+ interpolate_arguments, body = extract_arguments(body)
66
+ change = ExtractI18n::SourceChange.new(
67
+ source_line: @word.line,
68
+ remove: body,
69
+ interpolate_arguments: interpolate_arguments,
70
+ i18n_key: "#{@file_key}.#{ExtractI18n.key(body)}",
71
+ i18n_string: body,
72
+ t_template: "#{@word.indentation}#{tagged_with_equals} t('%s'%s)"
73
+ )
74
+ yield(change)
75
+ end
76
+
77
+ def parse_html_arguments(line, token_skipped_before = nil, &block)
78
+ final_line = line
79
+ regex_list.each do |regex|
80
+ line.scan(regex) do |m_data|
81
+ next if m_data == token_skipped_before
82
+ before = m_data[0]
83
+ html_tag = m_data[1]
84
+ translation = match_string(m_data[2])
85
+ after = m_data[3]
86
+ interpolate_arguments, translation = extract_arguments(translation)
87
+ change = ExtractI18n::SourceChange.new(
88
+ source_line: final_line,
89
+ i18n_string: translation,
90
+ i18n_key: "#{@file_key}.#{ExtractI18n.key(translation)}",
91
+ remove: m_data[2],
92
+ interpolate_arguments: interpolate_arguments,
93
+ t_template: "#{before}#{html_tag}t('%s'%s)#{after}"
94
+ )
95
+ final_line = yield(change)
96
+ return parse_html_arguments(final_line, &block)
97
+ end
98
+ end
99
+ final_line
100
+ end
101
+
102
+ def link_tos(line)
103
+ m = line.match(LINK_TO)
104
+ if m != nil
105
+ _ = m[:html_tag]
106
+ translation = match_string(m[:translation])
107
+ translation_key = update_hashes(translation)
108
+ line = line.gsub(m[:translation], translation_key)
109
+ link_tos(line)
110
+ else
111
+ line
112
+ end
113
+ end
114
+
115
+ def should_not_be_processed?(tokens)
116
+ (tokens.nil? or tokens.size < 2)
117
+ end
118
+
119
+ def matches_string?(translation)
120
+ m = translation.match(STRING_WITHOUT_QUOTES)
121
+ return false if m.nil?
122
+ (m[:double_quot] != nil or m[:single_quot] != nil)
123
+ end
124
+
125
+ def match_string(translation)
126
+ m = translation.match(STRING_WITHOUT_QUOTES)
127
+ return translation if m.nil?
128
+ if m[:double_quot] != nil
129
+ m[:double_quot]
130
+ else
131
+ (m[:single_quot] != nil ? m[:single_quot] : translation)
132
+ end
133
+ end
134
+
135
+ def regex_list
136
+ HTML_ARGUMENTS.map { |_, regex| /#{BEFORE}#{regex}#{TRANSLATION}#{AFTER}/ }
137
+ end
138
+
139
+ def extract_arguments(translation)
140
+ args = {}
141
+ translation.scan(/\#{[^}]*}/).each_with_index do |arg, index|
142
+ stripped_arg = arg[2..-2]
143
+ key = ExtractI18n.key(arg)
144
+ key += index.to_s if index > 0
145
+ translation = translation.gsub(arg, "%{#{key}}")
146
+ args[key] = stripped_arg
147
+ end
148
+ [args, translation]
149
+ end
150
+ end
151
+ end