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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ec8435d8164544f6ad8189497150081967d15eeb
4
- data.tar.gz: 6d009683f9fa3f714afba2565251a0f595528167
3
+ metadata.gz: 022e7e24bdc8f7eac43ef37debe3a0a5c9adf049
4
+ data.tar.gz: 961b8861d2b68f04f6b53a854e45fef56faf0a0c
5
5
  SHA512:
6
- metadata.gz: ebe46a52b38c1b783d9dcce80a59c96ea7aca70e1d9f46f08b92eb4b27b7004f465270492e01f6e860462d8dfdb8983076ee7e9b6b14fd280a6441db996c3aa8
7
- data.tar.gz: 2ae0304f7b6ff1eac841e28d6845cba14a8ccaf65ca3c0dd2351e98d7c9181690dfc8363f440cbdac9d331a9bda96523c68c9f3bf7817d3d50461254c846a14c
6
+ metadata.gz: b28356338e252dc9de49880b725d20a65276cec919292ae7d639807beefb47fcab627eccd37c411807841684be5ae44a4a050e1f85292c11221dbb53c5e5ba08
7
+ data.tar.gz: b5740884faa0dd0f12ba8fe98ca1e8a69d155792afe10010b3286637cf6384362f923a981e44fb433c2750ca2411cf587f83e86a0b2b046540a2a795ca132b24
data/README.md CHANGED
@@ -1,8 +1,10 @@
1
1
  # Terrestrial::Cli
2
2
 
3
- Official Terrestrial command line tool. For more documentation visit the [official docs](http://docs.terrestrial.io/).
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
- If you have any questions, join us on [Slack](https://terrestrial-slack.herokuapp.com/)!
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
@@ -32,7 +32,7 @@ OptionParser.new do |opts|
32
32
  options[:verbose] = true
33
33
  end
34
34
 
35
- opts.on_tail("--version", "Show version") do
35
+ opts.on_tail("-v", "--version", "Show version") do
36
36
  puts "Terrestrial CLI: #{Terrestrial::Cli::VERSION}"
37
37
  exit
38
38
  end
@@ -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 << "\"#{entry.identifier}\"=\"#{entry.string}\";"
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 << "\"#{entry.identifier}\"=\"#{entry.string}\";"
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
- ["\"#{entry.identifier}\"=\"#{entry.formatted_string}\";"]
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
- module DotStringsParser
3
+ class LocaliableStringsParserError < RuntimeError; end
4
+
5
+ class DotStringsParser
4
6
  class << self
5
7
 
6
8
  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
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
- 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 = {}
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
- raise "Don't know what to do with '#{line.inspect}'"
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
- 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]
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
- 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
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
- def remove_comments(line)
133
- line = line.split("//")[0] || ""
134
- line.rstrip
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
- # 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.
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
- # Source:
12
- # http://stackoverflow.com/questions/574724/rexml-preserve-attributes-order
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
- # 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
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
- # 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
+ 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)
@@ -63,6 +63,7 @@ module Terrestrial
63
63
 
64
64
  def save_document
65
65
  File.open(@path, "w") do |f|
66
+ f.write "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n"
66
67
  f.write format_document
67
68
  end
68
69
  end
@@ -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)
@@ -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
@@ -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
- if Config[:platform] == "ios"
9
- DotStringsParser.parse_file(Config[:directory] + "/#{file}")
10
- elsif Config[:platform] == "android"
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
- abort "Could not find #{file}. If the file is no longer in your project, remove it from your tracked files in terrestrial.yml."
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
@@ -1,5 +1,5 @@
1
1
  module Terrestrial
2
2
  module Cli
3
- VERSION = "0.6.2"
3
+ VERSION = "0.6.3"
4
4
  end
5
5
  end
@@ -32,7 +32,16 @@ module Terrestrial
32
32
  end
33
33
 
34
34
  def [](key)
35
- values[key]
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.2
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-22 00:00:00.000000000 Z
11
+ date: 2016-02-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: terminal-table