terrestrial-cli 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.
- 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
|