terrestrial-cli 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/.gitignore +10 -0
- data/.rspec +1 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +134 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/bin/terrestrial +44 -0
- data/circle.yml +11 -0
- data/lib/terrestrial/cli/android_xml_formatter.rb +43 -0
- data/lib/terrestrial/cli/android_xml_parser.rb +49 -0
- data/lib/terrestrial/cli/bootstrapper.rb +184 -0
- data/lib/terrestrial/cli/command.rb +20 -0
- data/lib/terrestrial/cli/detects_project_type.rb +16 -0
- data/lib/terrestrial/cli/dot_strings_formatter.rb +53 -0
- data/lib/terrestrial/cli/dot_strings_parser.rb +139 -0
- data/lib/terrestrial/cli/editor/android_xml.rb +64 -0
- data/lib/terrestrial/cli/editor/base_editor.rb +36 -0
- data/lib/terrestrial/cli/editor/objc.rb +66 -0
- data/lib/terrestrial/cli/editor/printer.rb +47 -0
- data/lib/terrestrial/cli/editor/storyboard.rb +98 -0
- data/lib/terrestrial/cli/editor/swift.rb +92 -0
- data/lib/terrestrial/cli/editor.rb +42 -0
- data/lib/terrestrial/cli/engine_mapper.rb +30 -0
- data/lib/terrestrial/cli/entry_collection_differ.rb +22 -0
- data/lib/terrestrial/cli/file_finder.rb +65 -0
- data/lib/terrestrial/cli/file_picker.rb +58 -0
- data/lib/terrestrial/cli/flight/ios_workflow.rb +81 -0
- data/lib/terrestrial/cli/flight/table_workflow.rb +77 -0
- data/lib/terrestrial/cli/flight.rb +93 -0
- data/lib/terrestrial/cli/ignite.rb +73 -0
- data/lib/terrestrial/cli/init.rb +133 -0
- data/lib/terrestrial/cli/mixpanel_client.rb +56 -0
- data/lib/terrestrial/cli/parser/android_xml.rb +82 -0
- data/lib/terrestrial/cli/parser/base_parser.rb +42 -0
- data/lib/terrestrial/cli/parser/objc.rb +127 -0
- data/lib/terrestrial/cli/parser/storyboard.rb +166 -0
- data/lib/terrestrial/cli/parser/string_analyser.rb +115 -0
- data/lib/terrestrial/cli/parser/swift.rb +102 -0
- data/lib/terrestrial/cli/parser.rb +25 -0
- data/lib/terrestrial/cli/photoshoot.rb +65 -0
- data/lib/terrestrial/cli/pull.rb +110 -0
- data/lib/terrestrial/cli/push.rb +40 -0
- data/lib/terrestrial/cli/scan.rb +72 -0
- data/lib/terrestrial/cli/string_registry.rb +30 -0
- data/lib/terrestrial/cli/terminal_ui.rb +25 -0
- data/lib/terrestrial/cli/variable_normalizer.rb +34 -0
- data/lib/terrestrial/cli/version.rb +5 -0
- data/lib/terrestrial/cli.rb +82 -0
- data/lib/terrestrial/config.rb +99 -0
- data/lib/terrestrial/creates_terrestrial_yml.rb +9 -0
- data/lib/terrestrial/web/response.rb +17 -0
- data/lib/terrestrial/web.rb +78 -0
- data/lib/terrestrial/yaml_helper.rb +48 -0
- data/lib/terrestrial.rb +7 -0
- data/terrestrial-cli.gemspec +29 -0
- metadata +188 -0
@@ -0,0 +1,139 @@
|
|
1
|
+
module Terrestrial
|
2
|
+
module Cli
|
3
|
+
module DotStringsParser
|
4
|
+
class << self
|
5
|
+
|
6
|
+
def parse_file(path)
|
7
|
+
entries = do_parse_file read_file_with_correct_encoding(path)
|
8
|
+
entries.each do |entry|
|
9
|
+
entry["type"] = "localizable.strings"
|
10
|
+
entry["file"] = path
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def do_parse_file(contents)
|
17
|
+
results = []
|
18
|
+
|
19
|
+
multiline_comment = false
|
20
|
+
expecting_string = false
|
21
|
+
multiline_string = false
|
22
|
+
current = {}
|
23
|
+
|
24
|
+
contents.split("\n").each do |line|
|
25
|
+
line = line.rstrip
|
26
|
+
line = remove_comments(line) unless multiline_string
|
27
|
+
|
28
|
+
if !multiline_string && !multiline_comment && line == ""
|
29
|
+
# Just empty line between entries
|
30
|
+
next
|
31
|
+
elsif line.start_with?("\"") && !line.end_with?(";")
|
32
|
+
# Start multiline string"
|
33
|
+
current_id = line.split("=").map(&:strip)[0][1..-1][0..-2]
|
34
|
+
current_string = line.split("=").map(&:strip)[1][1..-1]
|
35
|
+
|
36
|
+
current["identifier"] = current_id
|
37
|
+
current["string"] = current_string unless current_string.empty?
|
38
|
+
multiline_string = true
|
39
|
+
elsif multiline_string && !line.end_with?(";")
|
40
|
+
# Continuing multiline string
|
41
|
+
if current["string"].nil?
|
42
|
+
current["string"] = line.lstrip
|
43
|
+
else
|
44
|
+
current["string"] << "\n" + line
|
45
|
+
end
|
46
|
+
elsif multiline_string && line.end_with?(";")
|
47
|
+
# Ending multiline string
|
48
|
+
current["string"] << "\n#{line[0..-3]}"
|
49
|
+
multiline_string = false
|
50
|
+
results << current
|
51
|
+
current = {}
|
52
|
+
elsif !expecting_string && line.lstrip.start_with?("/*") && !line.end_with?("*/")
|
53
|
+
# Start multline comment
|
54
|
+
tmp_content = line[2..-1].strip
|
55
|
+
current["context"] = "\n" + tmp_content unless tmp_content.empty?
|
56
|
+
multiline_comment = true
|
57
|
+
elsif multiline_comment && !line.end_with?("*/")
|
58
|
+
# Continuing multline comment
|
59
|
+
if current["context"].nil?
|
60
|
+
current["context"] = line.lstrip
|
61
|
+
else
|
62
|
+
current["context"] << "\n" + line
|
63
|
+
end
|
64
|
+
elsif multiline_comment && line.end_with?("*/")
|
65
|
+
# Ending multline comment
|
66
|
+
tmp_content = line[0..-3].strip
|
67
|
+
current["context"] << (tmp_content.empty? ? "" : "\n#{tmp_content}")
|
68
|
+
multiline_comment = false
|
69
|
+
expecting_string = true
|
70
|
+
elsif !expecting_string && line.start_with?("/*") && line.end_with?("*/")
|
71
|
+
# Single line comment
|
72
|
+
current["context"] = line[2..-1][0..-3].strip
|
73
|
+
expecting_string = true
|
74
|
+
elsif expecting_string && line.end_with?(";")
|
75
|
+
# Single line id/string pair after a comment
|
76
|
+
current_id, current_string = get_string_and_id(line)
|
77
|
+
current["identifier"] = current_id
|
78
|
+
current["string"] = current_string
|
79
|
+
|
80
|
+
expecting_string = false
|
81
|
+
results << current
|
82
|
+
current = {}
|
83
|
+
elsif !expecting_string && line.end_with?(";")
|
84
|
+
# id/string without comment first
|
85
|
+
current_id, current_string = get_string_and_id(line)
|
86
|
+
current["identifier"] = current_id
|
87
|
+
current["string"] = current_string
|
88
|
+
|
89
|
+
results << current
|
90
|
+
current = {}
|
91
|
+
else
|
92
|
+
raise "Don't know what to do with '#{line.inspect}'"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
results
|
96
|
+
end
|
97
|
+
|
98
|
+
def get_string_and_id(line)
|
99
|
+
id = line.split("=").map(&:strip)[0].gsub("\"", "")
|
100
|
+
string = line.split("=").map(&:strip)[1].gsub("\"", "")[0..-2]
|
101
|
+
[id, string]
|
102
|
+
end
|
103
|
+
|
104
|
+
def read_file_with_correct_encoding(path)
|
105
|
+
# Genstrings creates files with BOM UTF-16LE encoding.
|
106
|
+
# If we realise that we cannot operate on the content
|
107
|
+
# of the file assumin UTF-8, we try UTF-16!
|
108
|
+
|
109
|
+
content = File.read path
|
110
|
+
begin
|
111
|
+
# Try performing an operation on the content
|
112
|
+
content.split("\n")
|
113
|
+
rescue ArgumentError
|
114
|
+
# Failure! We think this is a UTF-16 file
|
115
|
+
|
116
|
+
# Remove the byte order marker from the beginning
|
117
|
+
# of the file. We tried doing this with a simple
|
118
|
+
# sub! of \xFF\xFE, but we kept running into
|
119
|
+
# more issues. We instead do it manually.
|
120
|
+
content = content.bytes[2..-1].pack('c*')
|
121
|
+
|
122
|
+
# Force UTF-16LE encoding as a setting (not actually
|
123
|
+
# changing any representations yet!), then encode from
|
124
|
+
# that to UTF-8 again
|
125
|
+
content = content
|
126
|
+
.force_encoding(Encoding::UTF_16LE)
|
127
|
+
.encode!(Encoding::UTF_8)
|
128
|
+
end
|
129
|
+
content
|
130
|
+
end
|
131
|
+
|
132
|
+
def remove_comments(line)
|
133
|
+
line = line.split("//")[0] || ""
|
134
|
+
line.rstrip
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Terrestrial
|
2
|
+
module Cli
|
3
|
+
module Editor
|
4
|
+
class AndroidXML < BaseEditor
|
5
|
+
|
6
|
+
def self.find_and_edit_line(string_entry)
|
7
|
+
self.new(string_entry).add_attributes
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.add_import(file)
|
11
|
+
# Not needed
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(entry)
|
15
|
+
@path = entry.file
|
16
|
+
@type = entry.type
|
17
|
+
@string = entry.string
|
18
|
+
@identifier = entry.identifier
|
19
|
+
|
20
|
+
@document = REXML::Document.new(File.new(@path))
|
21
|
+
@document.context[:attribute_quote] = :quote
|
22
|
+
end
|
23
|
+
|
24
|
+
def add_attributes
|
25
|
+
node = find_node(@identifier)
|
26
|
+
node.add_attribute("terrestrial", true)
|
27
|
+
refresh_document(node)
|
28
|
+
save_document
|
29
|
+
end
|
30
|
+
|
31
|
+
def find_node(name)
|
32
|
+
REXML::XPath.first(@document, "//resources/string[@name=\"#{name}\"]")
|
33
|
+
end
|
34
|
+
|
35
|
+
def refresh_document(node)
|
36
|
+
# This is a bit ridiculous, but necessary because
|
37
|
+
# &*£(*$)£@*$!£ REXML
|
38
|
+
|
39
|
+
@document = REXML::Document.new(node.document.to_s)
|
40
|
+
end
|
41
|
+
|
42
|
+
def save_document
|
43
|
+
# AAAAAAAARARARARARARRARAAAGH REXML STAAAAHP
|
44
|
+
# You can't make REXML print attributes inside double
|
45
|
+
# quotes without monkey patching >.<
|
46
|
+
#
|
47
|
+
# ...seriously?
|
48
|
+
|
49
|
+
REXML::Attribute.class_eval( %q^
|
50
|
+
def to_string
|
51
|
+
%Q[#@expanded_name="#{to_s().gsub(/"/, '"')}"]
|
52
|
+
end
|
53
|
+
^)
|
54
|
+
File.open(@path, "w") do |f|
|
55
|
+
printer = Printer.new(2)
|
56
|
+
printer.compact = true
|
57
|
+
printer.width = 1000000
|
58
|
+
printer.write(@document, f)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
|
3
|
+
module Terrestrial
|
4
|
+
module Cli
|
5
|
+
module Editor
|
6
|
+
class BaseEditor
|
7
|
+
|
8
|
+
def self.find_and_edit_line(string_entry)
|
9
|
+
raise "Not implemented"
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.add_import(file)
|
13
|
+
raise "Not implemented"
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.edit_file(path)
|
17
|
+
temp_file = Tempfile.new(File.basename(path))
|
18
|
+
begin
|
19
|
+
line_number = 1
|
20
|
+
File.open(path, 'r') do |file|
|
21
|
+
file.each_line do |line|
|
22
|
+
yield line, line_number, temp_file
|
23
|
+
line_number += 1
|
24
|
+
end
|
25
|
+
end
|
26
|
+
temp_file.close
|
27
|
+
FileUtils.mv(temp_file.path, path)
|
28
|
+
ensure
|
29
|
+
temp_file.close
|
30
|
+
temp_file.unlink
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Terrestrial
|
2
|
+
module Cli
|
3
|
+
module Editor
|
4
|
+
class ObjC < BaseEditor
|
5
|
+
|
6
|
+
def self.find_and_edit_line(new_string)
|
7
|
+
edit_file(new_string.file) do |line, line_number, file|
|
8
|
+
if line_number == new_string.line_number
|
9
|
+
file.puts do_edit_string(line, new_string)
|
10
|
+
else
|
11
|
+
file.puts line
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.do_edit_string(line, entry)
|
17
|
+
line.gsub(a_string_not_followed_by_translated(entry.string), "@\"#{entry.identifier}\".translated")
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.add_import(file)
|
21
|
+
# Adds #import "Terrestrial.h" as the last import
|
22
|
+
# statement at the top of the file.
|
23
|
+
#
|
24
|
+
# It goes through the file from top to bottom looking for the first import
|
25
|
+
# statement. After it finds it, it will look for the first line without
|
26
|
+
# an import statement. When it finds it, it will write the import line,
|
27
|
+
# and all following lines are just copied over.
|
28
|
+
|
29
|
+
found_first_import = false
|
30
|
+
imported = false
|
31
|
+
|
32
|
+
edit_file(file) do |line, line_number, file|
|
33
|
+
if !found_first_import && line.start_with?("#import ")
|
34
|
+
found_first_import = true
|
35
|
+
file.puts line
|
36
|
+
elsif line.start_with?("#import <Terrestrial/Terrestrial.h>")
|
37
|
+
imported = true
|
38
|
+
file.puts line
|
39
|
+
elsif !imported && found_first_import && !line.start_with?("#import ")
|
40
|
+
file.puts "#import <Terrestrial/Terrestrial.h>"
|
41
|
+
file.puts ""
|
42
|
+
imported = true
|
43
|
+
else
|
44
|
+
file.puts line
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.a_string_not_followed_by_translated(string)
|
50
|
+
# Does not match:
|
51
|
+
#
|
52
|
+
# @"foo".translated
|
53
|
+
# or
|
54
|
+
# @"foo" translatedWithContext
|
55
|
+
#
|
56
|
+
# (?!(\.|\s)translated) means don't match either
|
57
|
+
# period or single whitepspace character,
|
58
|
+
# followed by translated
|
59
|
+
|
60
|
+
|
61
|
+
/@"#{string}"(?!(\.|\s)translated)/
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# Common fixes for editors that use
|
2
|
+
# REXML
|
3
|
+
class Printer < REXML::Formatters::Pretty
|
4
|
+
# The point of this little thing is to keep
|
5
|
+
# the order of attributes when we flush the
|
6
|
+
# storyboard back to disk.
|
7
|
+
# We do this to avoid massive git diffs.
|
8
|
+
# Turns out that xcode is quite good at updating
|
9
|
+
# and reverting our formating changes though.
|
10
|
+
#
|
11
|
+
# Source:
|
12
|
+
# http://stackoverflow.com/questions/574724/rexml-preserve-attributes-order
|
13
|
+
#
|
14
|
+
# fmt = OrderedAttributes.new
|
15
|
+
# fmt.write(xmldoc, $stdout)
|
16
|
+
#
|
17
|
+
def write_element(elm, out)
|
18
|
+
att = elm.attributes
|
19
|
+
class <<att
|
20
|
+
alias _each_attribute each_attribute
|
21
|
+
|
22
|
+
def each_attribute(&b)
|
23
|
+
to_enum(:_each_attribute).sort_by {|x| x.name}.each(&b)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
super(elm, out)
|
27
|
+
end
|
28
|
+
|
29
|
+
# This genious is here to stop the damn thing from wrapping
|
30
|
+
# lines over 80 chars long >.<
|
31
|
+
#
|
32
|
+
# Source:
|
33
|
+
# http://stackoverflow.com/questions/4203180/rexml-is-wrapping-long-lines-how-do-i-switch-that-off
|
34
|
+
#
|
35
|
+
def write_text( node, output )
|
36
|
+
s = node.to_s()
|
37
|
+
s.gsub!(/\s/,' ')
|
38
|
+
s.squeeze!(" ")
|
39
|
+
|
40
|
+
#The Pretty formatter code mistakenly used 80 instead of the @width variable
|
41
|
+
#s = wrap(s, 80-@level)
|
42
|
+
s = wrap(s, @width-@level)
|
43
|
+
|
44
|
+
s = indent_text(s, @level, " ", true)
|
45
|
+
output << (' '*@level + s)
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
module Terrestrial
|
2
|
+
module Cli
|
3
|
+
module Editor
|
4
|
+
class Storyboard < BaseEditor
|
5
|
+
|
6
|
+
QUERIES = {
|
7
|
+
"storyboard-label" => "//label",
|
8
|
+
"storyboard-text-field" => "//textField",
|
9
|
+
"storyboard-button" => "//button",
|
10
|
+
"storyboard-bar-button-item" => "//barButtonItem",
|
11
|
+
"storyboard-navbar-item" => "//navigationItem",
|
12
|
+
"storyboard-text-view" => "//textView"
|
13
|
+
}
|
14
|
+
|
15
|
+
def self.find_and_edit_line(approved_string)
|
16
|
+
insert_runtime_attribute(approved_string)
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
def self.add_import(file)
|
21
|
+
# Not needed
|
22
|
+
# Override parent class implementation
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize(entry)
|
26
|
+
@path = entry.file
|
27
|
+
@type = entry.type
|
28
|
+
@string = entry.string
|
29
|
+
@storyboard_id = entry.metadata["storyboard_element_id"]
|
30
|
+
@identifier = entry.identifier
|
31
|
+
|
32
|
+
@document = REXML::Document.new(File.new(@path))
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.insert_runtime_attribute(entry)
|
36
|
+
self.new(entry).insert_attribute
|
37
|
+
end
|
38
|
+
|
39
|
+
def insert_attribute
|
40
|
+
node = find_node
|
41
|
+
|
42
|
+
# TODO, There was a case when "node" was nil in this point, after
|
43
|
+
# trying to find it by type + ID.
|
44
|
+
#
|
45
|
+
# Keep an eye out for it to see if reproducible
|
46
|
+
|
47
|
+
node.add(create_element)
|
48
|
+
refresh_document(node)
|
49
|
+
save_document
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def find_node
|
55
|
+
REXML::XPath.first(@document, query_for(@type, storyboard_id: @storyboard_id))
|
56
|
+
end
|
57
|
+
|
58
|
+
def refresh_document(node)
|
59
|
+
# This is a bit ridiculous, but necessary because
|
60
|
+
# &*£(*$)£@*$!£ REXML
|
61
|
+
|
62
|
+
@document = REXML::Document.new(node.document.to_s)
|
63
|
+
end
|
64
|
+
|
65
|
+
def save_document
|
66
|
+
File.open(@path, "w") do |f|
|
67
|
+
printer = Printer.new(2)
|
68
|
+
printer.width = 1000
|
69
|
+
printer.write(@document, f)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def query_for(type, storyboard_id: "")
|
74
|
+
# Find element of said type, with the ID, that does not
|
75
|
+
# have a userDefinedRuntimeAttribute as a child
|
76
|
+
|
77
|
+
QUERIES[type] + "[@id=\"#{storyboard_id}\" and not(userDefinedRuntimeAttributes)]]"
|
78
|
+
end
|
79
|
+
|
80
|
+
def text_attribute(type)
|
81
|
+
Parser::Storyboard::Engine::TEXT_ATTRIBUTE[type]
|
82
|
+
end
|
83
|
+
|
84
|
+
def create_element
|
85
|
+
REXML::Element.new("userDefinedRuntimeAttributes")
|
86
|
+
.add_element("userDefinedRuntimeAttribute",
|
87
|
+
{"type" => "boolean",
|
88
|
+
"keyPath" => "Terrestrial",
|
89
|
+
"value" => "YES" }).parent
|
90
|
+
.add_element("userDefinedRuntimeAttribute",
|
91
|
+
{"type" => "string",
|
92
|
+
"keyPath" => "Identifier",
|
93
|
+
"value" => @identifier }).parent
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module Terrestrial
|
2
|
+
module Cli
|
3
|
+
module Editor
|
4
|
+
class Swift < BaseEditor
|
5
|
+
|
6
|
+
def self.find_and_edit_line(new_string)
|
7
|
+
edit_file(new_string.file) do |line, line_number, file|
|
8
|
+
if line_number == new_string.line_number
|
9
|
+
file.puts do_edit_string(line, new_string)
|
10
|
+
else
|
11
|
+
file.puts line
|
12
|
+
end
|
13
|
+
line_number += 1
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.do_edit_string(line, entry)
|
18
|
+
if has_swift_variables? entry.string
|
19
|
+
edit_with_variables(line, entry)
|
20
|
+
else
|
21
|
+
line.gsub(a_string_not_followed_by_translated(entry.string), "\"#{entry.identifier}\".translated")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.edit_with_variables(line, entry)
|
26
|
+
line.gsub(a_string_not_followed_by_translated(entry.string), build_string_with_variables(entry))
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.build_string_with_variables(entry)
|
30
|
+
"NSString(format: \"#{entry.identifier}\".translated, #{swift_variables(entry.string).join(", ")})"
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.add_import(file)
|
34
|
+
# Adds import Terrestrial as the last import
|
35
|
+
# statement at the top of the file.
|
36
|
+
#
|
37
|
+
# It goes through the file from top to bottom looking for the first import
|
38
|
+
# statement. After it finds it, it will look for the first line without
|
39
|
+
# an import statement. When it finds it, it will write the import line,
|
40
|
+
# and all following lines are just copied over.
|
41
|
+
|
42
|
+
found_first_import = false
|
43
|
+
imported = false
|
44
|
+
|
45
|
+
edit_file(file) do |line, line_number, file|
|
46
|
+
# Detect first import statement
|
47
|
+
if !found_first_import && line.start_with?("import ")
|
48
|
+
found_first_import = true
|
49
|
+
file.puts line
|
50
|
+
# Terrestrial had already been imported
|
51
|
+
elsif line.start_with?("import Terrestrial")
|
52
|
+
imported = true
|
53
|
+
file.puts line
|
54
|
+
# Not imported, had found first import, and doesn't start with "import"
|
55
|
+
# -> import
|
56
|
+
elsif !imported && found_first_import && !line.start_with?("import ")
|
57
|
+
file.puts "import Terrestrial"
|
58
|
+
file.puts ""
|
59
|
+
imported = true
|
60
|
+
# Copy over as normal
|
61
|
+
else
|
62
|
+
file.puts line
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.has_swift_variables?(string)
|
68
|
+
swift_variables(string).any?
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.swift_variables(string)
|
72
|
+
string.scan(Parser::Swift::VARIABLE_REGEX).map(&:first)
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.a_string_not_followed_by_translated(string)
|
76
|
+
# Does not match:
|
77
|
+
#
|
78
|
+
# @"foo".translated
|
79
|
+
# or
|
80
|
+
# @"foo" translatedWithContext
|
81
|
+
#
|
82
|
+
# (?!(\.|\s)translated) means don't match either
|
83
|
+
# period or single whitepspace character,
|
84
|
+
# followed by translated
|
85
|
+
|
86
|
+
|
87
|
+
/"#{Regexp.quote(string)}"(?!\.translated)/
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'terrestrial/cli/editor/printer'
|
2
|
+
require 'terrestrial/cli/editor/base_editor'
|
3
|
+
require 'terrestrial/cli/editor/objc'
|
4
|
+
require 'terrestrial/cli/editor/swift'
|
5
|
+
require 'terrestrial/cli/editor/storyboard'
|
6
|
+
require 'terrestrial/cli/editor/android_xml'
|
7
|
+
|
8
|
+
module Terrestrial
|
9
|
+
module Cli
|
10
|
+
module Editor
|
11
|
+
class << self
|
12
|
+
def prepare_files(new_strings)
|
13
|
+
@new_strings = new_strings
|
14
|
+
run
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def run
|
20
|
+
wrap_string_with_sdk_functions
|
21
|
+
add_imports
|
22
|
+
end
|
23
|
+
|
24
|
+
def add_imports
|
25
|
+
@new_strings
|
26
|
+
.uniq {|string| string.file}
|
27
|
+
.each {|string| editor_for_type(string.file).add_import(string.file)}
|
28
|
+
end
|
29
|
+
|
30
|
+
def wrap_string_with_sdk_functions
|
31
|
+
@new_strings.each do |string|
|
32
|
+
editor_for_type(string.file).find_and_edit_line(string)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def editor_for_type(file)
|
37
|
+
EngineMapper.editor_for(File.extname(file))
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Terrestrial
|
2
|
+
module Cli
|
3
|
+
class EngineMapper
|
4
|
+
|
5
|
+
PARSERS = {
|
6
|
+
".m" => Terrestrial::Cli::Parser::ObjC,
|
7
|
+
".h" => Terrestrial::Cli::Parser::ObjC,
|
8
|
+
".swift" => Terrestrial::Cli::Parser::Swift,
|
9
|
+
".storyboard" => Terrestrial::Cli::Parser::Storyboard,
|
10
|
+
".xml" => Terrestrial::Cli::Parser::AndroidXML
|
11
|
+
}
|
12
|
+
|
13
|
+
EDITORS = {
|
14
|
+
".m" => Terrestrial::Cli::Editor::ObjC,
|
15
|
+
".h" => Terrestrial::Cli::Editor::ObjC,
|
16
|
+
".swift" => Terrestrial::Cli::Editor::Swift,
|
17
|
+
".storyboard" => Terrestrial::Cli::Editor::Storyboard,
|
18
|
+
".xml" => Terrestrial::Cli::Editor::AndroidXML
|
19
|
+
}
|
20
|
+
|
21
|
+
def self.parser_for(extension)
|
22
|
+
PARSERS[extension]
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.editor_for(extension)
|
26
|
+
EDITORS[extension]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Terrestrial
|
2
|
+
module Cli
|
3
|
+
class EntryCollectionDiffer
|
4
|
+
|
5
|
+
def self.omissions(first, second)
|
6
|
+
first.select do |a|
|
7
|
+
!second.any? {|b| match?(a, b)}
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.additions(first, second)
|
12
|
+
second.reject do |b|
|
13
|
+
first.any? {|a| match?(a, b) }
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.match?(a, b)
|
18
|
+
a.fetch("identifier") == b.fetch("identifier")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|