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,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