terrestrial-cli 0.6.2 → 0.6.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +4 -96
- data/bin/terrestrial +1 -1
- data/lib/terrestrial/cli/dot_strings_formatter.rb +11 -3
- data/lib/terrestrial/cli/dot_strings_parser.rb +147 -119
- data/lib/terrestrial/cli/editor/custom_printer.rb +120 -23
- data/lib/terrestrial/cli/editor/storyboard.rb +1 -0
- data/lib/terrestrial/cli/flight/ios_workflow.rb +8 -1
- data/lib/terrestrial/cli/push.rb +44 -2
- data/lib/terrestrial/cli/scan.rb +4 -3
- data/lib/terrestrial/cli/string_registry.rb +18 -7
- data/lib/terrestrial/cli/version.rb +1 -1
- data/lib/terrestrial/config.rb +10 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 022e7e24bdc8f7eac43ef37debe3a0a5c9adf049
|
4
|
+
data.tar.gz: 961b8861d2b68f04f6b53a854e45fef56faf0a0c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b28356338e252dc9de49880b725d20a65276cec919292ae7d639807beefb47fcab627eccd37c411807841684be5ae44a4a050e1f85292c11221dbb53c5e5ba08
|
7
|
+
data.tar.gz: b5740884faa0dd0f12ba8fe98ca1e8a69d155792afe10010b3286637cf6384362f923a981e44fb433c2750ca2411cf587f83e86a0b2b046540a2a795ca132b24
|
data/README.md
CHANGED
@@ -1,8 +1,10 @@
|
|
1
1
|
# Terrestrial::Cli
|
2
2
|
|
3
|
-
|
3
|
+
[![Build Status](https://circleci.com/gh/terrestrial-io/terrestrial-cli.svg?style=shield)](https://circleci.com/gh/terrestrial-io/terrestrial-cli)
|
4
4
|
|
5
|
-
|
5
|
+
Official Terrestrial command line tool. For documentation visit the [official docs](http://docs.terrestrial.io/).
|
6
|
+
|
7
|
+
You can also join us on [Slack](https://terrestrial-slack.herokuapp.com/) to chat with the team directly.
|
6
8
|
|
7
9
|
## Installation
|
8
10
|
|
@@ -16,100 +18,6 @@ To get started with your project, cd to your project directory and run
|
|
16
18
|
|
17
19
|
You can find your API key and the correct project ID by logging into [Terrestrial Mission Control](https://mission.terrestrial.io).
|
18
20
|
|
19
|
-
### First time localizing
|
20
|
-
|
21
|
-
If you have not internationalized your app before, Terrestrial provides a tool for quickly extracting user-facing strings from your source code:
|
22
|
-
|
23
|
-
$ terrestrial flight
|
24
|
-
|
25
|
-
##### iOS
|
26
|
-
|
27
|
-
Flight will scan your source code and, using some clever heuristics, determine which strings can be shown to users. Terrestrial will list all the strings in terminal, and you are able to exclude any strings you wish not to internationalize.
|
28
|
-
|
29
|
-
...
|
30
|
-
Page 5 of 5
|
31
|
-
+-------+------------------------------+------------------------------------------------+
|
32
|
-
| Index | String | File |
|
33
|
-
+-------+------------------------------+------------------------------------------------+
|
34
|
-
| 40 | Home | InspectionMadeEasy/LeftMenuViewController.m:96 |
|
35
|
-
+-------+------------------------------+------------------------------------------------+
|
36
|
-
| 41 | Home | InspectionMadeEasy/MainViewController.m:23 |
|
37
|
-
+-------+------------------------------+------------------------------------------------+
|
38
|
-
-- Instructions --
|
39
|
-
- To exclude any strings from translation, type the index of each string.
|
40
|
-
- e.g. 1,2,4
|
41
|
-
------------------
|
42
|
-
Any Exclusions? (press return to continue or 'q' to quit at any time)
|
43
|
-
|
44
|
-
$
|
45
|
-
|
46
|
-
After this, Terrestrial generates a **Base.lproj/Localizable.strings** file based on the selected strings, and updates your source code so that each occurence of each strings is properly referenced by ID:
|
47
|
-
|
48
|
-
# Source Code
|
49
|
-
label.text = @"This is my string" => label.text = @"THIS_IS_MY_STRING".translated
|
50
|
-
|
51
|
-
# The ID is generated based on the original string.
|
52
|
-
# The .translated method is simple syntactic sugar over NSLocalizedString, and you
|
53
|
-
# are able to fall back to native iOS localization APIs if needed.
|
54
|
-
|
55
|
-
**Note on Stroyboards:** Terrestrial allows you to easily use strings from your Localizable.strings files inside your Storyboards via IBInspectable properties. During the *flight* process, any strings in Storyboards will have the Terrestrial IBInspectable property turned on, and the string's ID included as a value in the properties. To see this in action, view the Attributed Inspector tab of a UI element in your Storyboards.
|
56
|
-
|
57
|
-
#### Android
|
58
|
-
|
59
|
-
** Documentation coming soon **
|
60
|
-
|
61
|
-
### Existing App
|
62
|
-
|
63
|
-
If you have already translated your application, Terrestrial needs to know where to find your translation files. This is done via the **terrestrial.yml** file created when your project is initialized:
|
64
|
-
|
65
|
-
---
|
66
|
-
app_id: <app ID>
|
67
|
-
project_id: <project ID>
|
68
|
-
platform: <platform>
|
69
|
-
translation_files:
|
70
|
-
- /path/to
|
71
|
-
- /any/localization/files
|
72
|
-
|
73
|
-
Terrestrial will keep of the strings listed in the listed files.
|
74
|
-
|
75
|
-
### Workflow
|
76
|
-
|
77
|
-
As you add strings to you app, either in iOS's Localizable.strings or Android's strings.xml, you can track your changes with:
|
78
|
-
|
79
|
-
$ terrestrial scan
|
80
|
-
New Strings: 0
|
81
|
-
Removed Strings: 0
|
82
|
-
|
83
|
-
This will diff your local strings with the current strings stored in Terrestrial. You can see a breakdown of changes by running:
|
84
|
-
|
85
|
-
$ terrestrial scan --verbose
|
86
|
-
|
87
|
-
When you are ready to upload your local changes with Terrestrial for your translators to get to work, push your latest strings to Terrestrial:
|
88
|
-
|
89
|
-
$ terrestrial push
|
90
|
-
|
91
|
-
We suggest running *push* as part of a standard build cycle.
|
92
|
-
|
93
|
-
To get the latest translations for your app, run:
|
94
|
-
|
95
|
-
$ terrestrial pull
|
96
|
-
|
97
|
-
This will update the necessary language files in your project automatically with updated translations.
|
98
|
-
|
99
|
-
### Testing
|
100
|
-
|
101
|
-
Terrestrial allows you to start your iOS simulator in a specified locale from the command line:
|
102
|
-
|
103
|
-
$ terrestrial ignite es # Starts the simulator in Spanish
|
104
|
-
|
105
|
-
|
106
|
-
To upload screenshots, along with metadata of string positions and styles, run the photoshoot command:
|
107
|
-
|
108
|
-
$ terrestrial photoshoot
|
109
|
-
|
110
|
-
This will start the simulator and initialize the Terrestrial SDK in photoshoot mode. To upload screenshots to your web dashboard, just tap the injected screenshot button for each screen you wish to upload.
|
111
|
-
|
112
|
-
|
113
21
|
## Contributing
|
114
22
|
|
115
23
|
Bug reports and pull requests are welcome on GitHub at https://github.com/terrestrial-io/terrestrial-cli.
|
data/bin/terrestrial
CHANGED
@@ -11,7 +11,7 @@ module Terrestrial
|
|
11
11
|
entries.reject(&:placeholder?).each do |entry|
|
12
12
|
# just id and string needed for translation
|
13
13
|
# files. extra metadata is found in base.lproj.
|
14
|
-
result <<
|
14
|
+
result << id_and_string(entry)
|
15
15
|
result << ""
|
16
16
|
end
|
17
17
|
|
@@ -20,7 +20,7 @@ module Terrestrial
|
|
20
20
|
entries.select(&:placeholder?).each do |entry|
|
21
21
|
# just id and string needed for translation
|
22
22
|
# files. extra metadata is found in base.lproj.
|
23
|
-
result <<
|
23
|
+
result << id_and_string(entry)
|
24
24
|
result << ""
|
25
25
|
end
|
26
26
|
result.join("\n")
|
@@ -55,7 +55,15 @@ module Terrestrial
|
|
55
55
|
end
|
56
56
|
|
57
57
|
def id_and_string(entry)
|
58
|
-
|
58
|
+
if entry.respond_to? :formatted_string
|
59
|
+
["\"#{entry.identifier}\"=\"#{escape_string(entry.formatted_string)}\";"]
|
60
|
+
else
|
61
|
+
["\"#{entry.identifier}\"=\"#{escape_string(entry.string)}\";"]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def escape_string(string)
|
66
|
+
string.gsub("\"", "\\\"")
|
59
67
|
end
|
60
68
|
|
61
69
|
def spacing
|
@@ -1,138 +1,166 @@
|
|
1
1
|
module Terrestrial
|
2
2
|
module Cli
|
3
|
-
|
3
|
+
class LocaliableStringsParserError < RuntimeError; end
|
4
|
+
|
5
|
+
class DotStringsParser
|
4
6
|
class << self
|
5
7
|
|
6
8
|
def parse_file(path)
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
new(path).parse
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(path)
|
14
|
+
@path = path
|
15
|
+
end
|
16
|
+
|
17
|
+
def parse
|
18
|
+
entries = do_parse_file read_file_with_correct_encoding(path)
|
19
|
+
entries.each do |entry|
|
20
|
+
entry["type"] = "localizable.strings"
|
21
|
+
entry["file"] = path
|
12
22
|
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def path
|
28
|
+
@path
|
29
|
+
end
|
30
|
+
|
31
|
+
def do_parse_file(contents)
|
32
|
+
results = []
|
33
|
+
|
34
|
+
multiline_comment = false
|
35
|
+
expecting_string = false
|
36
|
+
multiline_string = false
|
37
|
+
current = {}
|
38
|
+
|
39
|
+
contents.split("\n").each_with_index do |line, line_number|
|
40
|
+
@current_line = line
|
41
|
+
@current_line_number = line_number
|
42
|
+
line = line.rstrip
|
43
|
+
line = remove_comments(line) unless multiline_string
|
13
44
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
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 = {}
|
45
|
+
if !multiline_string && !multiline_comment && line == ""
|
46
|
+
# Just empty line between entries
|
47
|
+
next
|
48
|
+
elsif line.start_with?("\"") && !line.end_with?(";")
|
49
|
+
# Start multiline string"
|
50
|
+
current_id = line.split("=").map(&:strip)[0][1..-1][0..-2]
|
51
|
+
current_string = line.split("=").map(&:strip)[1][1..-1]
|
52
|
+
|
53
|
+
current["identifier"] = current_id
|
54
|
+
current["string"] = current_string unless current_string.empty?
|
55
|
+
multiline_string = true
|
56
|
+
elsif multiline_string && !line.end_with?(";")
|
57
|
+
# Continuing multiline string
|
58
|
+
if current["string"].nil?
|
59
|
+
current["string"] = line.lstrip
|
60
|
+
else
|
61
|
+
current["string"] << "\n" + line
|
62
|
+
end
|
63
|
+
elsif multiline_string && line.end_with?(";")
|
64
|
+
# Ending multiline string
|
65
|
+
current["string"] << "\n#{line[0..-3]}"
|
66
|
+
multiline_string = false
|
67
|
+
results << current
|
68
|
+
current = {}
|
69
|
+
elsif !expecting_string && line.lstrip.start_with?("/*") && !line.end_with?("*/")
|
70
|
+
# Start multline comment
|
71
|
+
tmp_content = line[2..-1].strip
|
72
|
+
current["context"] = "\n" + tmp_content unless tmp_content.empty?
|
73
|
+
multiline_comment = true
|
74
|
+
elsif multiline_comment && !line.end_with?("*/")
|
75
|
+
# Continuing multline comment
|
76
|
+
if current["context"].nil?
|
77
|
+
current["context"] = line.lstrip
|
91
78
|
else
|
92
|
-
|
79
|
+
current["context"] << "\n" + line
|
93
80
|
end
|
81
|
+
elsif multiline_comment && line.end_with?("*/")
|
82
|
+
# Ending multline comment
|
83
|
+
tmp_content = line[0..-3].strip
|
84
|
+
current["context"] << (tmp_content.empty? ? "" : "\n#{tmp_content}")
|
85
|
+
multiline_comment = false
|
86
|
+
expecting_string = true
|
87
|
+
elsif !expecting_string && line.start_with?("/*") && line.end_with?("*/")
|
88
|
+
# Single line comment
|
89
|
+
current["context"] = line[2..-1][0..-3].strip
|
90
|
+
expecting_string = true
|
91
|
+
elsif expecting_string && line.end_with?(";")
|
92
|
+
# Single line id/string pair after a comment
|
93
|
+
current_id, current_string = get_string_and_id(line)
|
94
|
+
current["identifier"] = current_id
|
95
|
+
current["string"] = current_string
|
96
|
+
|
97
|
+
expecting_string = false
|
98
|
+
results << current
|
99
|
+
current = {}
|
100
|
+
elsif !expecting_string && line.end_with?(";")
|
101
|
+
# id/string without comment first
|
102
|
+
current_id, current_string = get_string_and_id(line)
|
103
|
+
current["identifier"] = current_id
|
104
|
+
current["string"] = current_string
|
105
|
+
|
106
|
+
results << current
|
107
|
+
current = {}
|
108
|
+
else
|
109
|
+
raise LocaliableStringsParserError
|
94
110
|
end
|
95
|
-
results
|
96
111
|
end
|
112
|
+
results
|
113
|
+
rescue
|
114
|
+
puts "There was an error parsing #{path}:"
|
115
|
+
puts ""
|
116
|
+
puts " line #{@current_line_number}: "
|
117
|
+
puts " #{@current_line}"
|
118
|
+
Kernel.abort
|
119
|
+
end
|
97
120
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
121
|
+
def get_string_and_id(line)
|
122
|
+
cap = line.match(/"([^"]*)"\s*=\s*"([^"]*)";$/).captures
|
123
|
+
if cap[0].nil? || cap[1].nil?
|
124
|
+
raise LocaliableStringsParserError
|
125
|
+
else
|
126
|
+
cap
|
102
127
|
end
|
128
|
+
end
|
103
129
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
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
|
130
|
+
def read_file_with_correct_encoding(path)
|
131
|
+
# Genstrings creates files with BOM UTF-16LE encoding.
|
132
|
+
# If we realise that we cannot operate on the content
|
133
|
+
# of the file assumin UTF-8, we try UTF-16!
|
134
|
+
|
135
|
+
content = File.read path
|
136
|
+
begin
|
137
|
+
# Try performing an operation on the content
|
138
|
+
content.split("\n")
|
139
|
+
rescue ArgumentError
|
140
|
+
# Failure! We think this is a UTF-16 file
|
131
141
|
|
132
|
-
|
133
|
-
|
134
|
-
|
142
|
+
# Remove the byte order marker from the beginning
|
143
|
+
# of the file. We tried doing this with a simple
|
144
|
+
# sub! of \xFF\xFE, but we kept running into
|
145
|
+
# more issues. We instead do it manually.
|
146
|
+
content = content.bytes[2..-1].pack('c*')
|
147
|
+
|
148
|
+
# Force UTF-16LE encoding as a setting (not actually
|
149
|
+
# changing any representations yet!), then encode from
|
150
|
+
# that to UTF-8 again
|
151
|
+
content = content
|
152
|
+
.force_encoding(Encoding::UTF_16LE)
|
153
|
+
.encode!(Encoding::UTF_8)
|
135
154
|
end
|
155
|
+
content
|
156
|
+
end
|
157
|
+
|
158
|
+
def remove_comments(line)
|
159
|
+
# Regex delightfully borrowed from
|
160
|
+
# http://stackoverflow.com/questions/6462578/alternative-to-regex-match-all-instances-not-inside-quotes
|
161
|
+
double_slashes_not_inside_quotes = /\/\/(?=([^"\\]*(\\.|"([^"\\]*\\.)*[^"\\]*"))*[^"]*$)/
|
162
|
+
line = line.split(double_slashes_not_inside_quotes)[0] || ""
|
163
|
+
line.rstrip
|
136
164
|
end
|
137
165
|
end
|
138
166
|
end
|
@@ -1,30 +1,129 @@
|
|
1
1
|
# Common fixes for editors that use
|
2
2
|
# REXML
|
3
3
|
class CustomPrinter < REXML::Formatters::Pretty
|
4
|
-
#
|
5
|
-
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
# Turns out that xcode is quite good at updating
|
9
|
-
# and reverting our formating changes though.
|
4
|
+
# Custom sorting of Storyboard element attributes.
|
5
|
+
# In order to prevent massive diffs in Storyboard files
|
6
|
+
# after flight, we need to try to match the Xcode
|
7
|
+
# XML generator as closely as possible.
|
10
8
|
#
|
11
|
-
#
|
12
|
-
#
|
9
|
+
# One big issue is the ordering of XML element attributes.
|
10
|
+
# Here we attempt to match the attribute ordering for each type
|
11
|
+
# of Storyboard element.
|
13
12
|
#
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
13
|
+
def write_element(elm, out)
|
14
|
+
att = elm.attributes
|
15
|
+
|
16
|
+
class <<att
|
17
|
+
# Alias old method
|
18
|
+
alias _each_attribute each_attribute
|
19
|
+
|
20
|
+
# Redefine the each_attribute method to call our sorting
|
21
|
+
# method
|
22
|
+
def each_attribute(&b)
|
23
|
+
to_enum(:_each_attribute)
|
24
|
+
.sort_by {|x| xcode_index_for(x) }
|
25
|
+
.each(&b)
|
26
|
+
end
|
21
27
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
+
def _xcode_get_index(order, attr)
|
29
|
+
order.index(attr.name) || attr.name.length
|
30
|
+
end
|
31
|
+
|
32
|
+
# Define the order for each type of Xcode element
|
33
|
+
def xcode_index_for(attr)
|
34
|
+
element_type = attr.element.name
|
35
|
+
|
36
|
+
case element_type
|
37
|
+
when 'userDefinedRuntimeAttribute'
|
38
|
+
index = _xcode_get_index(['type','keyPath','value'], attr)
|
39
|
+
when 'color'
|
40
|
+
index = _xcode_get_index(['key', 'red', 'green', 'blue', 'white', 'alpha', 'colorSpace',
|
41
|
+
'customColorSpace'], attr)
|
42
|
+
when 'placeholder'
|
43
|
+
index = _xcode_get_index(['placeholderIdentifier', 'id', 'userLabel', 'sceneMemberID'], attr)
|
44
|
+
when 'fontDescription'
|
45
|
+
index = _xcode_get_index(['key', 'type', 'name', 'family', 'pointSize'], attr)
|
46
|
+
when 'rect'
|
47
|
+
index = _xcode_get_index(['key', 'x', 'y', 'width', 'height'], attr)
|
48
|
+
when 'label'
|
49
|
+
index = _xcode_get_index(['opaque', 'multipleTouchEnabled', 'userInteractionEnabled', 'contentMode',
|
50
|
+
'horizontalHuggingPriority', 'verticalHuggingPriority', 'text',
|
51
|
+
'misplaced', 'textAlignment', 'lineBreakMode', 'numberOfLines',
|
52
|
+
'baselineAdjustment', 'adjustsFontSizeToFit', 'minimumScaleFactor',
|
53
|
+
'translatesAutoresizingMaskIntoConstraints', 'id',
|
54
|
+
'customClass'], attr)
|
55
|
+
when 'button'
|
56
|
+
index = _xcode_get_index(['opaque', 'contentMode', 'contentHorizontalAlignment',
|
57
|
+
'contentVerticalAlignment', 'buttonType', 'lineBreakMode',
|
58
|
+
'id'], attr)
|
59
|
+
when 'viewController'
|
60
|
+
index = _xcode_get_index(['storyboardIdentifier', 'id', 'customClass', 'customModule',
|
61
|
+
'customModuleProvider', 'sceneMemberID'], attr)
|
62
|
+
when 'viewControllerLayoutGuide'
|
63
|
+
index = _xcode_get_index(['type', 'id'], attr)
|
64
|
+
when 'view'
|
65
|
+
index = _xcode_get_index(['autoresizesSubviews','clipsSubviews', 'alpha', 'key',
|
66
|
+
'contentMode', 'id'], attr)
|
67
|
+
when 'autoresizingMask'
|
68
|
+
index = _xcode_get_index(['key', 'flexibleMaxX', 'flexibleMaxY', 'widthSizable',
|
69
|
+
'heightSizable'], attr)
|
70
|
+
when 'document'
|
71
|
+
index = _xcode_get_index(['type', 'version', 'toolsVersion', 'systemVersion',
|
72
|
+
'targetRuntime', 'propertyAccessControl', 'useAutolayout',
|
73
|
+
'useTraitCollections', 'initialViewController'], attr)
|
74
|
+
when 'document'
|
75
|
+
index = _xcode_get_index(['type', 'version', 'toolsVersion', 'systemVersion',
|
76
|
+
'targetRuntime', 'propertyAccessControl', 'useAutolayout',
|
77
|
+
'useTraitCollections', 'initialViewController'], attr)
|
78
|
+
when 'image'
|
79
|
+
index = _xcode_get_index(['name', 'width', 'height'], attr)
|
80
|
+
when 'imageView'
|
81
|
+
index = _xcode_get_index(['clipsSubviews', 'userInteractionEnabled', 'alpha',
|
82
|
+
'contentMode', 'horizontalHuggingPriority',
|
83
|
+
'verticalHuggingPriority', 'image', 'id'], attr)
|
84
|
+
when 'segue'
|
85
|
+
index = _xcode_get_index(['destination', 'kind', 'relationship', 'id'], attr)
|
86
|
+
when 'navigationBar'
|
87
|
+
index = _xcode_get_index(['key', 'contentMode', 'id'], attr)
|
88
|
+
when 'navigationItem'
|
89
|
+
index = _xcode_get_index(['key', 'title', 'id'], attr)
|
90
|
+
when 'navigationController'
|
91
|
+
index = _xcode_get_index(['storyboardIdentifier', 'automaticallyAdjustsScrollViewInsets',
|
92
|
+
'id', 'sceneMemberID'], attr)
|
93
|
+
when 'outlet'
|
94
|
+
index = _xcode_get_index(['property', 'destination', 'id'], attr)
|
95
|
+
when 'action'
|
96
|
+
index = _xcode_get_index(['selector', 'destination', 'id'], attr)
|
97
|
+
when 'barButtonItem'
|
98
|
+
index = _xcode_get_index(['key', 'title', 'id'], attr)
|
99
|
+
when 'tableViewController'
|
100
|
+
index = _xcode_get_index(['restorationIdentifier', 'storyboardIdentifier', 'id',
|
101
|
+
'customClass', 'sceneMemberID'], attr)
|
102
|
+
when 'tableView'
|
103
|
+
index = _xcode_get_index(['key', 'clipsSubviews', 'contentMode', 'alwaysBounceVertical',
|
104
|
+
'dataMode', 'style', 'separatorStyle', 'rowHeight',
|
105
|
+
'sectionHeaderHeight', 'sectionFooterHeight', 'id'], attr)
|
106
|
+
when 'tableViewCellContentView'
|
107
|
+
index = _xcode_get_index(['key', 'opaque', 'clipsSubviews', 'multipleTouchEnabled',
|
108
|
+
'contentMode', 'tableViewCell', 'id'], attr)
|
109
|
+
when 'tableViewCell'
|
110
|
+
index = _xcode_get_index(['key', 'opaque', 'clipsSubviews', 'multipleTouchEnabled',
|
111
|
+
'contentMode', 'selectionStyle', 'accessoryType', 'indentationWidth',
|
112
|
+
'textLabel', 'detailTextLabel', 'rowHeight', 'style', 'id'], attr)
|
113
|
+
when 'size'
|
114
|
+
index = _xcode_get_index(['key', 'width', 'height'], attr)
|
115
|
+
when 'textField'
|
116
|
+
index = _xcode_get_index(['opaque', 'clipsSubviews', 'contentMode', 'contentHorizontalAlignment',
|
117
|
+
'contentVerticalAlignment', 'text', 'borderStyle', 'placeholder',
|
118
|
+
'textAlignment', 'minimumFontSize', 'id'], attr)
|
119
|
+
else
|
120
|
+
index = attr.name
|
121
|
+
end
|
122
|
+
index
|
123
|
+
end
|
124
|
+
end
|
125
|
+
super(elm, out)
|
126
|
+
end
|
28
127
|
|
29
128
|
# This genious is here to stop the damn thing from wrapping
|
30
129
|
# lines over 80 chars long >.<
|
@@ -34,8 +133,6 @@ class CustomPrinter < REXML::Formatters::Pretty
|
|
34
133
|
#
|
35
134
|
def write_text( node, output )
|
36
135
|
s = node.to_s()
|
37
|
-
s.gsub!(/\s/,' ')
|
38
|
-
s.squeeze!(" ")
|
39
136
|
|
40
137
|
#The Pretty formatter code mistakenly used 80 instead of the @width variable
|
41
138
|
#s = wrap(s, 80-@level)
|
@@ -56,7 +56,14 @@ module Terrestrial
|
|
56
56
|
|
57
57
|
def detect_project_language(folder)
|
58
58
|
info_plist = Dir[Config[:directory] + "/#{folder}/Info.plist"].first
|
59
|
-
`defaults read '#{info_plist}' CFBundleDevelopmentRegion`.gsub("\n", "").squeeze(" ")
|
59
|
+
lang = `defaults read '#{info_plist}' CFBundleDevelopmentRegion 2> /dev/null`.gsub("\n", "").squeeze(" ")
|
60
|
+
|
61
|
+
if lang.empty?
|
62
|
+
puts "Unable to detect project language. Defaulting to 'en'."
|
63
|
+
'en'
|
64
|
+
else
|
65
|
+
lang
|
66
|
+
end
|
60
67
|
end
|
61
68
|
|
62
69
|
def print_done_message(lproj_folder)
|
data/lib/terrestrial/cli/push.rb
CHANGED
@@ -7,6 +7,20 @@ module Terrestrial
|
|
7
7
|
MixpanelClient.track("cli-push-command")
|
8
8
|
load_string_registry
|
9
9
|
|
10
|
+
if string_registry.entries.any?
|
11
|
+
if duplicates.any?
|
12
|
+
show_duplicate_error_message
|
13
|
+
else
|
14
|
+
do_push
|
15
|
+
end
|
16
|
+
else
|
17
|
+
show_no_entries_error_message
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def do_push
|
10
24
|
response = web_client.push(Config[:project_id], Config[:app_id], format_entries)
|
11
25
|
|
12
26
|
if response.success?
|
@@ -18,8 +32,6 @@ module Terrestrial
|
|
18
32
|
end
|
19
33
|
end
|
20
34
|
|
21
|
-
private
|
22
|
-
|
23
35
|
def format_entries
|
24
36
|
string_registry.entries.map do |entry|
|
25
37
|
{
|
@@ -41,6 +53,36 @@ module Terrestrial
|
|
41
53
|
def string_registry
|
42
54
|
@string_registry
|
43
55
|
end
|
56
|
+
|
57
|
+
def duplicates
|
58
|
+
@duplicates ||= string_registry
|
59
|
+
.entries.group_by {|e| e["identifier"] }
|
60
|
+
.select {|id, entries| entries.length > 1}
|
61
|
+
end
|
62
|
+
|
63
|
+
def show_duplicate_error_message
|
64
|
+
puts "- Push Failed"
|
65
|
+
puts "Terrestrial found some duplicate string identifiers:"
|
66
|
+
duplicates.each do |identifier, entries|
|
67
|
+
puts ""
|
68
|
+
puts " '#{identifier}' found in"
|
69
|
+
entries.each_with_index do |entry, i|
|
70
|
+
puts " #{i + 1}.) #{entry["file"]}"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
puts ""
|
74
|
+
puts "String identifiers must be unique in each push. Make sure you remove any duplicates."
|
75
|
+
puts "If you are accidentally tracking you base language files as well as some foreign language files,"
|
76
|
+
puts "make sure you only track the base language."
|
77
|
+
end
|
78
|
+
|
79
|
+
def show_no_entries_error_message
|
80
|
+
puts "Terrestrial could not find any strings in your project."
|
81
|
+
puts "Are you tracking the correct files in terrestrial.yml?"
|
82
|
+
puts ""
|
83
|
+
puts "For more information, you can find our documentation at http://docs.terrestrial.io/"
|
84
|
+
puts "You can also jump on our Slack via https://terrestrial-slack.herokuapp.com/"
|
85
|
+
end
|
44
86
|
end
|
45
87
|
end
|
46
88
|
end
|
data/lib/terrestrial/cli/scan.rb
CHANGED
@@ -19,16 +19,17 @@ module Terrestrial
|
|
19
19
|
private
|
20
20
|
|
21
21
|
def print_results
|
22
|
-
puts "New Strings: #{new_strings.count}"
|
23
|
-
puts "Removed Strings: #{removed_strings.count}"
|
24
|
-
|
25
22
|
if opts[:verbose]
|
26
23
|
print_diff
|
24
|
+
puts ""
|
27
25
|
else
|
28
26
|
if rand(10) == 1 # Show hint ~10% of the time
|
29
27
|
puts "(Hint: add --verbose to the 'scan' command to view the diff of local and remote strings.)"
|
30
28
|
end
|
31
29
|
end
|
30
|
+
|
31
|
+
puts "New Strings: #{new_strings.count}"
|
32
|
+
puts "Removed Strings: #{removed_strings.count}"
|
32
33
|
end
|
33
34
|
|
34
35
|
def print_diff
|
@@ -5,15 +5,16 @@ module Terrestrial
|
|
5
5
|
def self.load
|
6
6
|
entries = Config[:translation_files].flat_map do |file|
|
7
7
|
begin
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
AndroidXmlParser.parse_file(Config[:directory] + "/#{file}")
|
12
|
-
elsif Config[:platform] == "unity"
|
13
|
-
UnityParser.parse_file(Config[:directory] + "/#{file}")
|
8
|
+
entries = find_entries(file)
|
9
|
+
entries.each do |entry|
|
10
|
+
entry["file"] = file # Ensure paths are relative
|
14
11
|
end
|
15
12
|
rescue Errno::ENOENT
|
16
|
-
|
13
|
+
puts ""
|
14
|
+
puts "Could not find localization file."
|
15
|
+
puts "Looked in #{Config[:directory] + "/" + file}"
|
16
|
+
puts "If the file is no longer in your project, remove it from your tracked files in terrestrial.yml."
|
17
|
+
abort
|
17
18
|
end
|
18
19
|
end
|
19
20
|
|
@@ -27,6 +28,16 @@ module Terrestrial
|
|
27
28
|
def entries
|
28
29
|
@entries
|
29
30
|
end
|
31
|
+
|
32
|
+
def self.find_entries(file)
|
33
|
+
if Config[:platform] == "ios"
|
34
|
+
DotStringsParser.parse_file(Config[:directory] + "/#{file}")
|
35
|
+
elsif Config[:platform] == "android"
|
36
|
+
AndroidXmlParser.parse_file(Config[:directory] + "/#{file}")
|
37
|
+
elsif Config[:platform] == "unity"
|
38
|
+
UnityParser.parse_file(Config[:directory] + "/#{file}")
|
39
|
+
end
|
40
|
+
end
|
30
41
|
end
|
31
42
|
end
|
32
43
|
end
|
data/lib/terrestrial/config.rb
CHANGED
@@ -32,7 +32,16 @@ module Terrestrial
|
|
32
32
|
end
|
33
33
|
|
34
34
|
def [](key)
|
35
|
-
|
35
|
+
if key == :translation_files
|
36
|
+
# Translation files should be handed back as an
|
37
|
+
# empty array if it is nil.
|
38
|
+
# This can happen when users remove all translation
|
39
|
+
# files from terrestrial.yml instead of making
|
40
|
+
# it a valid YAML empty list
|
41
|
+
values[:translation_files] || []
|
42
|
+
else
|
43
|
+
values[key]
|
44
|
+
end
|
36
45
|
end
|
37
46
|
|
38
47
|
def reset!
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: terrestrial-cli
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.6.
|
4
|
+
version: 0.6.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Niklas Begley
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-02-
|
11
|
+
date: 2016-02-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: terminal-table
|