dotstrings 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9c23d2110a873db44936fc87e1c89e41300a82edfdde9c8a357679c1b378cc27
4
+ data.tar.gz: 81363243a7425d43383246594362b23740ee22a03b3d8d74f871cf8ea113809c
5
+ SHA512:
6
+ metadata.gz: 01a9055615619969260d1012c0a027338d85d73495ac664e6ac72c4a6bb7d3368eb4554496d5622940e660ea01d44fe2949bbf3aa88149ab05b284c021792456
7
+ data.tar.gz: ca19e173e63d4b820b9cd2ebbd4030ef70bb33080822738a4e290e4ce01b6f6be64c9578a858dffd8ae46d6423e343f0345611f1f1a5a04ca48a2ad9fc7b145f
data/.editorconfig ADDED
@@ -0,0 +1,13 @@
1
+ # EditorConfig is awesome: https://EditorConfig.org
2
+
3
+ # top-most EditorConfig file
4
+ root = true
5
+
6
+ [*]
7
+ end_of_line = lf
8
+ insert_final_newline = true
9
+
10
+ [{*.rb,Gemfile,Rakefile,*.yml}]
11
+ charset = utf-8
12
+ indent_style = space
13
+ indent_size = 2
@@ -0,0 +1,32 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: "*"
6
+ pull_request:
7
+ branches: "*"
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ ruby-version:
15
+ - "2.5"
16
+ - "2.6"
17
+ - "2.7"
18
+ - "3.0"
19
+
20
+ steps:
21
+ - uses: actions/checkout@v2
22
+ - name: Set up Ruby
23
+ uses: ruby/setup-ruby@v1
24
+ with:
25
+ ruby-version: ${{ matrix.ruby-version }}
26
+ bundler-cache: true # runs 'bundle install' and caches installed gems automatically
27
+ - name: Lint
28
+ run: bundle exec rake rubocop
29
+ - name: Run tests
30
+ run: bundle exec rake
31
+ - name: Install
32
+ run: bundle exec rake install
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ pkg
2
+ .vscode
data/.rubocop.yml ADDED
@@ -0,0 +1,33 @@
1
+ require:
2
+ - rubocop-rake
3
+ - rubocop-minitest
4
+
5
+ AllCops:
6
+ TargetRubyVersion: 2.5
7
+ NewCops: enable
8
+
9
+ Style/StringLiterals:
10
+ Enabled: true
11
+ EnforcedStyle: single_quotes
12
+
13
+ Metrics/ClassLength:
14
+ Enabled: true
15
+ Max: 150
16
+
17
+ Naming/MethodParameterName:
18
+ Enabled: false
19
+
20
+ Metrics/MethodLength:
21
+ Enabled: false
22
+
23
+ Metrics/AbcSize:
24
+ Enabled: false
25
+
26
+ Style/Documentation:
27
+ Enabled: false
28
+
29
+ Style/ParallelAssignment:
30
+ Enabled: false
31
+
32
+ Minitest/AssertInDelta:
33
+ Enabled: false
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,49 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ dotstrings (0.1.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ ast (2.4.2)
10
+ minitest (5.15.0)
11
+ parallel (1.22.1)
12
+ parser (3.1.2.0)
13
+ ast (~> 2.4.1)
14
+ rainbow (3.1.1)
15
+ rake (13.0.6)
16
+ regexp_parser (2.5.0)
17
+ rexml (3.2.5)
18
+ rubocop (1.28.2)
19
+ parallel (~> 1.10)
20
+ parser (>= 3.1.0.0)
21
+ rainbow (>= 2.2.2, < 4.0)
22
+ regexp_parser (>= 1.8, < 3.0)
23
+ rexml
24
+ rubocop-ast (>= 1.17.0, < 2.0)
25
+ ruby-progressbar (~> 1.7)
26
+ unicode-display_width (>= 1.4.0, < 3.0)
27
+ rubocop-ast (1.17.0)
28
+ parser (>= 3.1.1.0)
29
+ rubocop-minitest (0.19.1)
30
+ rubocop (>= 0.90, < 2.0)
31
+ rubocop-rake (0.6.0)
32
+ rubocop (~> 1.0)
33
+ ruby-progressbar (1.11.0)
34
+ unicode-display_width (2.2.0)
35
+
36
+ PLATFORMS
37
+ ruby
38
+
39
+ DEPENDENCIES
40
+ bundler
41
+ dotstrings!
42
+ minitest (~> 5.14)
43
+ rake
44
+ rubocop
45
+ rubocop-minitest
46
+ rubocop-rake
47
+
48
+ BUNDLED WITH
49
+ 2.2.18
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2022 Ramon Torres
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # DotStrings
2
+
3
+ A parser for Apple *strings* files (`.strings`) written in Ruby. Some of the features of DotStrings include:
4
+
5
+ * A fast and memory-efficient streaming parser.
6
+ * Support for multiline (`/* ... */`) comments as well as single-line comments (`// ...`).
7
+ * An API for creating strings files programmatically.
8
+ * Handles Unicode and escaped characters.
9
+ * Helpful error messages: know which line and column fail to parse and why.
10
+
11
+ ## Installing
12
+
13
+ You can install DotStrings manually by running:
14
+
15
+ ```shell
16
+ $ gem install dotstrings
17
+ ```
18
+
19
+ Or by adding the following entry to your [Gemfile](https://guides.cocoapods.org/using/a-gemfile.html), then running `$ bundle install`.
20
+
21
+ ```ruby
22
+ gem "dotstrings"
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ You can load `.strings` files using the `DotString.parse()` utility method. This method returns a `DotStrings::File` object or raises an exception if the file is invalid.
28
+
29
+ ```ruby
30
+ file = DotStrings.parse_file('en-US/Localizable.strings')
31
+ file.items.each do |item|
32
+ puts item.comment
33
+ puts item.key
34
+ puts item.value
35
+ end
36
+ ```
37
+
38
+ ## Examples
39
+
40
+ ### Listing keys
41
+
42
+ ```ruby
43
+ puts file.keys
44
+ # => ["key 1", "key 2", ...]
45
+ ```
46
+
47
+ ### Accessing items by key
48
+
49
+ ```ruby
50
+ puts file['key 1'].value
51
+ # => "value 1"
52
+ ```
53
+
54
+ ### Deleting items by key
55
+
56
+ ```ruby
57
+ file.delete('key 1')
58
+ ```
59
+
60
+ ### Appending items
61
+
62
+ ```ruby
63
+ file << DotStrings::Item(
64
+ comment: 'Title for the cancel button',
65
+ key: 'button.cancel.title',
66
+ value: 'Cancel'
67
+ )
68
+ ```
69
+
70
+ ### Saving a file
71
+
72
+ ```ruby
73
+ File.write('en-US/Localizable.strings', file.to_s)
74
+ ```
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rake/testtask'
5
+ require 'rubocop/rake_task'
6
+
7
+ Rake::TestTask.new
8
+ RuboCop::RakeTask.new
9
+
10
+ task default: :test
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require './lib/dotstrings/version'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'dotstrings'
7
+ s.version = DotStrings::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ['Ramon Torres']
10
+ s.email = ['raymondjavaxx@gmail.com']
11
+ s.homepage = 'https://github.com/raymondjavaxx/dotstrings'
12
+ s.description = s.summary = 'Parse and create .strings files used in localization of iOS and macOS apps.'
13
+ s.files = `git ls-files`.split("\n")
14
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
15
+ s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
16
+ s.require_paths = ['lib']
17
+ s.license = 'MIT'
18
+
19
+ s.add_development_dependency 'bundler'
20
+ s.add_development_dependency 'minitest', '~> 5.14'
21
+ s.add_development_dependency 'rake'
22
+ s.add_development_dependency 'rubocop'
23
+ s.add_development_dependency 'rubocop-minitest'
24
+ s.add_development_dependency 'rubocop-rake'
25
+
26
+ s.required_ruby_version = '>= 2.5.0'
27
+ s.metadata['rubygems_mfa_required'] = 'true'
28
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DotStrings
4
+ class ParsingError < RuntimeError
5
+ end
6
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dotstrings/parser'
4
+ require 'dotstrings/errors'
5
+ require 'dotstrings/item'
6
+
7
+ module DotStrings
8
+ class File
9
+ attr_reader :items
10
+
11
+ def initialize(items = [])
12
+ @items = items
13
+ end
14
+
15
+ def self.parse(io)
16
+ items = []
17
+
18
+ parser = Parser.new
19
+ parser.on_item { |item| items << item }
20
+ parser << normalize_encoding(io.read)
21
+
22
+ File.new(items)
23
+ end
24
+
25
+ def self.parse_file(path)
26
+ ::File.open(path, 'r') do |file|
27
+ parse(file)
28
+ end
29
+ end
30
+
31
+ def keys
32
+ @items.map(&:key)
33
+ end
34
+
35
+ def [](key)
36
+ @items.find { |item| item.key == key }
37
+ end
38
+
39
+ def <<(item)
40
+ @items << item
41
+ end
42
+
43
+ def append(item)
44
+ self << item
45
+ end
46
+
47
+ def delete(key)
48
+ @items.delete_if { |item| item.key == key }
49
+ end
50
+
51
+ def to_s
52
+ result = []
53
+
54
+ @items.each do |item|
55
+ result << item.to_s
56
+ result << ''
57
+ end
58
+
59
+ result.join("\n")
60
+ end
61
+
62
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
63
+ def self.normalize_encoding(str)
64
+ if str.bytesize >= 3 && str.getbyte(0) == 0xEF && str.getbyte(1) == 0xBB && str.getbyte(2) == 0xBF
65
+ # UTF-8 BOM
66
+ str.byteslice(3, str.bytesize - 3)
67
+ elsif str.bytesize >= 2 && str.getbyte(0) == 0xFE && str.getbyte(1) == 0xFF
68
+ # UTF-16 (BE) BOM
69
+ converter = Encoding::Converter.new('UTF-16BE', 'UTF-8')
70
+ str = converter.convert(str)
71
+ str.byteslice(3, str.bytesize - 3)
72
+ elsif str.bytesize >= 2 && str.getbyte(0) == 0xFF && str.getbyte(1) == 0xFE
73
+ # UTF-16 (LE) BOM
74
+ converter = Encoding::Converter.new('UTF-16LE', 'UTF-8')
75
+ str = converter.convert(str)
76
+ str.byteslice(3, str.bytesize - 3)
77
+ else
78
+ str.force_encoding('UTF-8')
79
+ end
80
+ end
81
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
82
+
83
+ private_class_method :normalize_encoding
84
+ end
85
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DotStrings
4
+ class Item
5
+ attr_reader :comment, :key, :value
6
+
7
+ def initialize(key:, value:, comment: nil)
8
+ @comment = comment
9
+ @key = key
10
+ @value = value
11
+ end
12
+
13
+ def to_s
14
+ result = []
15
+
16
+ result << "/* #{comment} */" unless comment.nil?
17
+ result << "\"#{serialize_string(key)}\" = \"#{serialize_string(value)}\";"
18
+
19
+ result.join("\n")
20
+ end
21
+
22
+ private
23
+
24
+ def serialize_string(string)
25
+ replacements = [
26
+ ['"', '\\"'],
27
+ ["'", "\\'"],
28
+ ["\t", '\t'],
29
+ ["\n", '\n'],
30
+ ["\r", '\r'],
31
+ ["\0", '\\0']
32
+ ]
33
+
34
+ replacements.each do |replacement|
35
+ string = string.gsub(replacement[0]) { replacement[1] }
36
+ end
37
+
38
+ string
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,250 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dotstrings/errors'
4
+
5
+ module DotStrings
6
+ # rubocop:disable Metrics/ClassLength
7
+ class Parser
8
+ # Special tokens
9
+ TOK_SLASH = '/'
10
+ TOK_ASTERISK = '*'
11
+ TOK_QUOTE = '"'
12
+ TOK_SINGLE_QUOTE = "'"
13
+ TOK_BACKSLASH = '\\'
14
+ TOK_EQUALS = '='
15
+ TOK_SEMICOLON = ';'
16
+ TOK_NEW_LINE = "\n"
17
+ TOK_N = 'n'
18
+ TOK_R = 'r'
19
+ TOK_T = 't'
20
+ TOK_CAP_U = 'U'
21
+ TOK_ZERO = '0'
22
+ TOK_HEX_DIGIT = /[0-9a-fA-F]/.freeze
23
+
24
+ # States
25
+ STATE_START = 0
26
+ STATE_COMMENT_START = 1
27
+ STATE_COMMENT = 2
28
+ STATE_MULTILINE_COMMENT = 3
29
+ STATE_COMMENT_END = 4
30
+ STATE_KEY = 5
31
+ STATE_KEY_END = 6
32
+ STATE_VALUE_SEPARATOR = 7
33
+ STATE_VALUE = 8
34
+ STATE_VALUE_END = 9
35
+ STATE_UNICODE = 10
36
+ STATE_UNICODE_SURROGATE = 11
37
+ STATE_UNICODE_SURROGATE_U = 12
38
+
39
+ attr_reader :items
40
+
41
+ def initialize
42
+ @state = STATE_START
43
+ @temp_state = nil
44
+
45
+ @buffer = []
46
+ @unicode_buffer = []
47
+ @high_surrogate = nil
48
+
49
+ @escaping = false
50
+
51
+ @current_comment = nil
52
+ @current_key = nil
53
+ @current_value = nil
54
+
55
+ @item_block = nil
56
+
57
+ @offset = 0
58
+ @line = 1
59
+ @column = 1
60
+ end
61
+
62
+ def on_item(&block)
63
+ @item_block = block
64
+ end
65
+
66
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/BlockLength
67
+ def <<(data)
68
+ data.each_char do |ch|
69
+ case @state
70
+ when STATE_START
71
+ start_value(ch)
72
+ when STATE_COMMENT_START
73
+ case ch
74
+ when TOK_SLASH
75
+ @state = STATE_COMMENT
76
+ when TOK_ASTERISK
77
+ @state = STATE_MULTILINE_COMMENT
78
+ else
79
+ raise_error("Unexpected character '#{ch}'")
80
+ end
81
+ when STATE_COMMENT
82
+ if ch == TOK_NEW_LINE
83
+ @state = STATE_COMMENT_END
84
+ @current_comment = @buffer.join.strip
85
+ @buffer.clear
86
+ else
87
+ @buffer << ch
88
+ end
89
+ when STATE_MULTILINE_COMMENT
90
+ if ch == TOK_SLASH && @buffer.last == TOK_ASTERISK
91
+ @state = STATE_COMMENT_END
92
+ @current_comment = @buffer.slice(0, @buffer.length - 1).join.strip
93
+ @buffer.clear
94
+ else
95
+ @buffer << ch
96
+ end
97
+ when STATE_COMMENT_END
98
+ @state = STATE_KEY if ch == TOK_QUOTE
99
+ when STATE_KEY
100
+ parse_string(ch) do |key|
101
+ @current_key = key
102
+ @state = STATE_KEY_END
103
+ end
104
+ when STATE_KEY_END
105
+ @state = STATE_VALUE_SEPARATOR if ch == TOK_EQUALS
106
+ when STATE_VALUE_SEPARATOR
107
+ if ch == TOK_QUOTE
108
+ @state = STATE_VALUE
109
+ else
110
+ raise_error("Unexpected character '#{ch}'") unless ch.strip.empty?
111
+ end
112
+ when STATE_VALUE
113
+ parse_string(ch) do |value|
114
+ @current_value = value
115
+ @state = STATE_VALUE_END
116
+
117
+ @item_block&.call(Item.new(
118
+ comment: @current_comment,
119
+ key: @current_key,
120
+ value: @current_value
121
+ ))
122
+ end
123
+ when STATE_VALUE_END
124
+ @state = STATE_START if ch == TOK_SEMICOLON
125
+ when STATE_UNICODE
126
+ parse_unicode(ch) do |unicode_ch|
127
+ @buffer << unicode_ch
128
+ # Restore state
129
+ @state = @temp_state
130
+ end
131
+ when STATE_UNICODE_SURROGATE
132
+ if ch == TOK_BACKSLASH
133
+ @state = STATE_UNICODE_SURROGATE_U
134
+ else
135
+ raise_error("Unexpected character '#{ch}', expecting another unicode codepoint")
136
+ end
137
+ when STATE_UNICODE_SURROGATE_U
138
+ if ch == TOK_CAP_U
139
+ @state = STATE_UNICODE
140
+ else
141
+ raise_error("Unexpected character '#{ch}', expecting '#{TOK_CAP_U}'")
142
+ end
143
+ end
144
+
145
+ update_position(ch)
146
+ end
147
+ end
148
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/BlockLength
149
+
150
+ private
151
+
152
+ def raise_error(message)
153
+ raise ParsingError, "#{message} at line #{@line}, column #{@column} (offset: #{@offset})"
154
+ end
155
+
156
+ def parse_string(ch, &block)
157
+ if @escaping
158
+ parse_escaped_character(ch, &block)
159
+ else
160
+ case ch
161
+ when TOK_BACKSLASH
162
+ @escaping = true
163
+ when TOK_QUOTE
164
+ block.call(@buffer.join)
165
+ @buffer.clear
166
+ else
167
+ @buffer << ch
168
+ end
169
+ end
170
+ end
171
+
172
+ def parse_escaped_character(ch)
173
+ @escaping = false
174
+
175
+ case ch
176
+ when TOK_QUOTE, TOK_SINGLE_QUOTE, TOK_BACKSLASH
177
+ @buffer << ch
178
+ when TOK_N
179
+ @buffer << "\n"
180
+ when TOK_R
181
+ @buffer << "\r"
182
+ when TOK_T
183
+ @buffer << "\t"
184
+ when TOK_CAP_U
185
+ @temp_state = @state
186
+ @state = STATE_UNICODE
187
+ when TOK_ZERO
188
+ @buffer << "\0"
189
+ else
190
+ raise_error("Unexpected character '#{ch}'")
191
+ end
192
+ end
193
+
194
+ # rubocop:disable Style/GuardClause
195
+ def parse_unicode(ch, &block)
196
+ raise_error("Unexpected character '#{ch}', expecting a hex digit") unless ch =~ TOK_HEX_DIGIT
197
+
198
+ @unicode_buffer << ch
199
+
200
+ if @unicode_buffer.length == 4
201
+ codepoint = @unicode_buffer.join.hex
202
+
203
+ if codepoint >= 0xD800 && codepoint <= 0xDBFF
204
+ @high_surrogate = codepoint
205
+ @state = STATE_UNICODE_SURROGATE
206
+ elsif codepoint >= 0xDC00 && codepoint <= 0xDFFF
207
+ character = ((@high_surrogate - 0xD800) * 0x400) + (codepoint - 0xDC00) + 0x10000
208
+ block.call(character.chr('UTF-8'))
209
+ else
210
+ block.call(codepoint.chr('UTF-8'))
211
+ end
212
+
213
+ # Clear buffer after codepoint is parsed
214
+ @unicode_buffer.clear
215
+ end
216
+ end
217
+ # rubocop:enable Style/GuardClause
218
+
219
+ def update_position(ch)
220
+ @offset += 1
221
+
222
+ if ch == TOK_NEW_LINE
223
+ @column = 1
224
+ @line += 1
225
+ else
226
+ @column += 1
227
+ end
228
+ end
229
+
230
+ def start_value(ch)
231
+ case ch
232
+ when TOK_SLASH
233
+ @state = STATE_COMMENT_START
234
+ reset_state
235
+ when TOK_QUOTE
236
+ @state = STATE_KEY
237
+ reset_state
238
+ else
239
+ raise_error("Unexpected character '#{ch}'") unless ch.strip.empty?
240
+ end
241
+ end
242
+
243
+ def reset_state
244
+ @current_comment = nil
245
+ @current_key = nil
246
+ @current_value = nil
247
+ end
248
+ end
249
+ # rubocop:enable Metrics/ClassLength
250
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DotStrings
4
+ VERSION = '0.1.0'
5
+ end
data/lib/dotstrings.rb ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dotstrings/version'
4
+ require 'dotstrings/file'
5
+ require 'dotstrings/item'
6
+ require 'dotstrings/errors'
7
+
8
+ module DotStrings
9
+ def self.parse(io)
10
+ File.parse(io)
11
+ end
12
+
13
+ def self.parse_file(path)
14
+ File.parse_file(path)
15
+ end
16
+ end
@@ -0,0 +1,2 @@
1
+ /* Escaped carriage backslashes */
2
+ "some\\key" = "some\\value";
@@ -0,0 +1,2 @@
1
+ /* Escaped carriage returns */
2
+ "some\rkey" = "some\rvalue";
@@ -0,0 +1,2 @@
1
+ /* Escaped new lines */
2
+ "some\nkey" = "some\nvalue";
@@ -0,0 +1,2 @@
1
+ /* Escaped nil */
2
+ "key\0" = "value\0";
@@ -0,0 +1,2 @@
1
+ /* Escaped quotes */
2
+ "some \"key\"" = "some \"value\"";
@@ -0,0 +1,2 @@
1
+ /* Escaped quotes */
2
+ "some \'key\'" = "some \'value\'";
@@ -0,0 +1,2 @@
1
+ /* Escaped tabs */
2
+ "some\tkey" = "some\tvalue";
@@ -0,0 +1,2 @@
1
+ /* Unicode characters. Key is a dollar sign, value is: ⚡👻 */
2
+ "\U0024" = "\U26A1\UD83D\UDC7B";
Binary file
Binary file
@@ -0,0 +1,2 @@
1
+ /* Comment */
2
+ "key" = "value";
@@ -0,0 +1,8 @@
1
+ // Single line comment
2
+ "key 1" = "value 1";
3
+
4
+ /* Multi line
5
+ comment */
6
+ "key 2" = "value 2";
7
+
8
+ "key 3" = "value 3";
data/test/helper.rb ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ gem 'minitest'
4
+
5
+ require 'minitest/pride'
6
+ require 'minitest/autorun'
7
+
8
+ require_relative '../lib/dotstrings'
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helper'
4
+
5
+ class TestDotStrings < MiniTest::Test
6
+ def test_parse_can_parse_valid_files
7
+ file = DotStrings.parse_file('test/fixtures/valid.strings')
8
+
9
+ assert_equal 3, file.items.size
10
+
11
+ assert_equal 'Single line comment', file.items[0].comment
12
+ assert_equal 'key 1', file.items[0].key
13
+ assert_equal 'value 1', file.items[0].value
14
+
15
+ assert_equal "Multi line\ncomment", file.items[1].comment
16
+ assert_equal 'key 2', file.items[1].key
17
+ assert_equal 'value 2', file.items[1].value
18
+ end
19
+
20
+ def test_can_parse_file_with_escaped_quotes
21
+ file = DotStrings.parse_file('test/fixtures/escaped_quotes.strings')
22
+
23
+ assert_equal 1, file.items.size
24
+ assert_equal 'some "key"', file.items[0].key
25
+ assert_equal 'some "value"', file.items[0].value
26
+ end
27
+
28
+ def test_can_parse_file_with_escaped_single_quotes
29
+ file = DotStrings.parse_file('test/fixtures/escaped_single_quotes.strings')
30
+
31
+ assert_equal 1, file.items.size
32
+ assert_equal 'some \'key\'', file.items[0].key
33
+ assert_equal 'some \'value\'', file.items[0].value
34
+ end
35
+
36
+ def test_can_parse_file_with_escaped_tabs
37
+ file = DotStrings.parse_file('test/fixtures/escaped_tabs.strings')
38
+
39
+ assert_equal 1, file.items.size
40
+ assert_equal "some\tkey", file.items[0].key
41
+ assert_equal "some\tvalue", file.items[0].value
42
+ end
43
+
44
+ def test_can_parse_files_with_escaped_carriage_returns
45
+ file = DotStrings.parse_file('test/fixtures/escaped_carriage_returns.strings')
46
+
47
+ assert_equal 1, file.items.size
48
+ assert_equal "some\rkey", file.items[0].key
49
+ assert_equal "some\rvalue", file.items[0].value
50
+ end
51
+
52
+ def test_can_parse_files_with_escaped_nil
53
+ file = DotStrings.parse_file('test/fixtures/escaped_nil.strings')
54
+
55
+ assert_equal 1, file.items.size
56
+ assert_equal "key\0", file.items[0].key
57
+ assert_equal "value\0", file.items[0].value
58
+ end
59
+
60
+ def test_can_parse_files_with_escaped_new_lines
61
+ file = DotStrings.parse_file('test/fixtures/escaped_new_lines.strings')
62
+
63
+ assert_equal 1, file.items.size
64
+ assert_equal "some\nkey", file.items[0].key
65
+ assert_equal "some\nvalue", file.items[0].value
66
+ end
67
+
68
+ def test_can_parse_files_with_escaped_backslashes
69
+ file = DotStrings.parse_file('test/fixtures/escaped_backslashes.strings')
70
+
71
+ assert_equal 1, file.items.size
72
+ assert_equal 'some\\key', file.items[0].key
73
+ assert_equal 'some\\value', file.items[0].value
74
+ end
75
+
76
+ def test_can_parse_files_with_escaped_unicode
77
+ file = DotStrings.parse_file('test/fixtures/escaped_unicode.strings')
78
+
79
+ assert_equal 1, file.items.size
80
+ assert_equal '$', file.items[0].key
81
+ assert_equal '⚡👻', file.items[0].value
82
+ end
83
+
84
+ def test_can_parse_utf16le_files_with_bom
85
+ file = DotStrings.parse_file('test/fixtures/utf16le_bom.strings')
86
+
87
+ assert_equal 1, file.items.size
88
+ assert_equal 'key', file.items[0].key
89
+ assert_equal 'value', file.items[0].value
90
+ end
91
+
92
+ def test_can_parse_utf16be_files_with_bom
93
+ file = DotStrings.parse_file('test/fixtures/utf16be_bom.strings')
94
+
95
+ assert_equal 1, file.items.size
96
+ assert_equal 'key', file.items[0].key
97
+ assert_equal 'value', file.items[0].value
98
+ end
99
+
100
+ def test_can_parse_utf8_files_with_bom
101
+ file = DotStrings.parse_file('test/fixtures/utf8_bom.strings')
102
+
103
+ assert_equal 1, file.items.size
104
+ assert_equal 'key', file.items[0].key
105
+ assert_equal 'value', file.items[0].value
106
+ end
107
+ end
data/test/test_file.rb ADDED
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helper'
4
+
5
+ class TestFile < MiniTest::Test
6
+ def test_delete
7
+ items = [
8
+ DotStrings::Item.new(key: 'key 1', value: 'value 1'),
9
+ DotStrings::Item.new(key: 'key 2', value: 'value 2'),
10
+ DotStrings::Item.new(key: 'key 3', value: 'value 3')
11
+ ]
12
+
13
+ file = DotStrings::File.new(items)
14
+
15
+ file.delete('key 2')
16
+ assert_equal 2, file.items.size
17
+ assert_equal ['key 1', 'key 3'], file.keys
18
+ end
19
+
20
+ def test_access_by_key
21
+ items = [
22
+ DotStrings::Item.new(key: 'key 1', value: 'value 1'),
23
+ DotStrings::Item.new(key: 'key 2', value: 'value 2'),
24
+ DotStrings::Item.new(key: 'key 3', value: 'value 3')
25
+ ]
26
+
27
+ file = DotStrings::File.new(items)
28
+
29
+ assert_equal 'value 1', file['key 1'].value
30
+ assert_equal 'value 2', file['key 2'].value
31
+ assert_equal 'value 3', file['key 3'].value
32
+ end
33
+
34
+ def test_append
35
+ file = DotStrings::File.new
36
+ file.append(DotStrings::Item.new(key: 'key 1', value: 'value 1'))
37
+ assert_equal 1, file.items.size
38
+ end
39
+
40
+ def test_to_string
41
+ items = [
42
+ DotStrings::Item.new(comment: 'Comment 1', key: 'key 1', value: 'value 1'),
43
+ DotStrings::Item.new(comment: 'Comment 2', key: 'key 2', value: 'value 2'),
44
+ DotStrings::Item.new(comment: 'Comment 3', key: 'key 3', value: '👻'),
45
+ DotStrings::Item.new(comment: 'Comment 4', key: "\"'\t\n\r\0", value: "\"'\t\n\r\0")
46
+ ]
47
+
48
+ file = DotStrings::File.new(items)
49
+
50
+ expected = <<~'END_OF_DOCUMENT'
51
+ /* Comment 1 */
52
+ "key 1" = "value 1";
53
+
54
+ /* Comment 2 */
55
+ "key 2" = "value 2";
56
+
57
+ /* Comment 3 */
58
+ "key 3" = "👻";
59
+
60
+ /* Comment 4 */
61
+ "\"\'\t\n\r\0" = "\"\'\t\n\r\0";
62
+ END_OF_DOCUMENT
63
+
64
+ assert_equal expected, file.to_s
65
+ end
66
+ end
data/test/test_item.rb ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helper'
4
+
5
+ class TestFile < MiniTest::Test
6
+ def test_to_s
7
+ item = DotStrings::Item.new(comment: 'Comment', key: 'key 1', value: 'value 1')
8
+ assert_equal "/* Comment */\n\"key 1\" = \"value 1\";", item.to_s
9
+ end
10
+
11
+ def test_to_s_with_nil_comment
12
+ item = DotStrings::Item.new(comment: nil, key: 'key 1', value: 'value 1')
13
+ assert_equal '"key 1" = "value 1";', item.to_s
14
+ end
15
+ end
metadata ADDED
@@ -0,0 +1,177 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dotstrings
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Ramon Torres
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-07-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: minitest
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '5.14'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '5.14'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubocop
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rubocop-minitest
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubocop-rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description: Parse and create .strings files used in localization of iOS and macOS
98
+ apps.
99
+ email:
100
+ - raymondjavaxx@gmail.com
101
+ executables: []
102
+ extensions: []
103
+ extra_rdoc_files: []
104
+ files:
105
+ - ".editorconfig"
106
+ - ".github/workflows/ci.yml"
107
+ - ".gitignore"
108
+ - ".rubocop.yml"
109
+ - Gemfile
110
+ - Gemfile.lock
111
+ - LICENSE
112
+ - README.md
113
+ - Rakefile
114
+ - dotstrings.gemspec
115
+ - lib/dotstrings.rb
116
+ - lib/dotstrings/errors.rb
117
+ - lib/dotstrings/file.rb
118
+ - lib/dotstrings/item.rb
119
+ - lib/dotstrings/parser.rb
120
+ - lib/dotstrings/version.rb
121
+ - test/fixtures/escaped_backslashes.strings
122
+ - test/fixtures/escaped_carriage_returns.strings
123
+ - test/fixtures/escaped_new_lines.strings
124
+ - test/fixtures/escaped_nil.strings
125
+ - test/fixtures/escaped_quotes.strings
126
+ - test/fixtures/escaped_single_quotes.strings
127
+ - test/fixtures/escaped_tabs.strings
128
+ - test/fixtures/escaped_unicode.strings
129
+ - test/fixtures/utf16be_bom.strings
130
+ - test/fixtures/utf16le_bom.strings
131
+ - test/fixtures/utf8_bom.strings
132
+ - test/fixtures/valid.strings
133
+ - test/helper.rb
134
+ - test/test_dotstrings.rb
135
+ - test/test_file.rb
136
+ - test/test_item.rb
137
+ homepage: https://github.com/raymondjavaxx/dotstrings
138
+ licenses:
139
+ - MIT
140
+ metadata:
141
+ rubygems_mfa_required: 'true'
142
+ post_install_message:
143
+ rdoc_options: []
144
+ require_paths:
145
+ - lib
146
+ required_ruby_version: !ruby/object:Gem::Requirement
147
+ requirements:
148
+ - - ">="
149
+ - !ruby/object:Gem::Version
150
+ version: 2.5.0
151
+ required_rubygems_version: !ruby/object:Gem::Requirement
152
+ requirements:
153
+ - - ">="
154
+ - !ruby/object:Gem::Version
155
+ version: '0'
156
+ requirements: []
157
+ rubygems_version: 3.0.1
158
+ signing_key:
159
+ specification_version: 4
160
+ summary: Parse and create .strings files used in localization of iOS and macOS apps.
161
+ test_files:
162
+ - test/fixtures/escaped_backslashes.strings
163
+ - test/fixtures/escaped_carriage_returns.strings
164
+ - test/fixtures/escaped_new_lines.strings
165
+ - test/fixtures/escaped_nil.strings
166
+ - test/fixtures/escaped_quotes.strings
167
+ - test/fixtures/escaped_single_quotes.strings
168
+ - test/fixtures/escaped_tabs.strings
169
+ - test/fixtures/escaped_unicode.strings
170
+ - test/fixtures/utf16be_bom.strings
171
+ - test/fixtures/utf16le_bom.strings
172
+ - test/fixtures/utf8_bom.strings
173
+ - test/fixtures/valid.strings
174
+ - test/helper.rb
175
+ - test/test_dotstrings.rb
176
+ - test/test_file.rb
177
+ - test/test_item.rb