message_format 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e901569363c40dffd88d1690540f7a2bddc5aa5c
4
+ data.tar.gz: 64bbb73e1cfc027ec033e59a068b532763ac4d3d
5
+ SHA512:
6
+ metadata.gz: ec37a34fe69cdf69069d6a4ae36950611ed64827577230ed8c9be6c0267ad1dbb553a2cc295c91908cae9ebb384629363a7becd1304ca58dc74b8a63a05fdc40
7
+ data.tar.gz: c29c673fb869551da456265b6ba2562c9884f1f11fd65f7a4af934e3a02daf8c64d8255a77c2e4ec3cee204739b2c0ab814c9e3cae278597324fe413be604f59
data/.gitignore ADDED
@@ -0,0 +1,40 @@
1
+ # ignore system files
2
+
3
+ # OS X
4
+ .DS_Store
5
+ .Spotlight-V100
6
+ .Trashes
7
+ ._*
8
+
9
+ # Win
10
+ Thumbs.db
11
+ Desktop.ini
12
+
13
+ # vim
14
+ *~
15
+ .swp
16
+ .*.sw[a-z]
17
+ Session.vim
18
+
19
+ *.gem
20
+ *.rbc
21
+ .bundle
22
+ .config
23
+ .yardoc
24
+ Gemfile.lock
25
+ InstalledFiles
26
+ _yardoc
27
+ coverage
28
+ doc/
29
+ lib/bundler/man
30
+ pkg
31
+ rdoc
32
+ spec/reports
33
+ test/tmp
34
+ test/version_tmp
35
+ tmp
36
+ *.bundle
37
+ *.so
38
+ *.o
39
+ *.a
40
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in message_format.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Andy VanWagoner
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # MessageFormat
2
+
3
+ Parse and format i18n messages using ICU MessageFormat patterns
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'message_format'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install message_format
18
+
19
+ ## Usage
20
+
21
+ ```ruby
22
+ require 'message_format'
23
+
24
+ message = MessageFormat.new('Hello { place }!', 'en-US')
25
+ formatted = message.format({ :place => 'World' })
26
+ ```
27
+
28
+ The [ICU Message Format][icu-message] is a great format for user-visible strings, and includes simple placeholders, number and date placeholders, and selecting among submessages for gender and plural arguments. The format is used in apis in [C++][icu-cpp], [PHP][icu-php], [Java][icu-java], and [JavaScript][icu-javascript].
29
+
30
+ ## Contributing
31
+
32
+ 1. Fork it ( https://github.com/thetalecrafter/message-format-rb/fork )
33
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
34
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
35
+ 4. Push to the branch (`git push origin my-new-feature`)
36
+ 5. Create a new Pull Request
37
+
38
+ ## License
39
+
40
+ This software is free to use under the MIT license. See the [LICENSE.txt file][LICENSE] for license text and copyright information.
41
+
42
+ [icu-message]: http://userguide.icu-project.org/formatparse/messages
43
+ [icu-cpp]: http://icu-project.org/apiref/icu4c/classicu_1_1MessageFormat.html
44
+ [icu-php]: http://php.net/manual/en/class.messageformatter.php
45
+ [icu-java]: http://icu-project.org/apiref/icu4j/
46
+ [icu-javascript]: https://github.com/thetalecrafter/message-format
47
+ [LICENSE]: https://github.com/thetalecrafter/message-format-rb/blob/master/LICENSE.txt
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
data/benchmark.rb ADDED
@@ -0,0 +1,73 @@
1
+ require 'benchmark'
2
+ require_relative 'lib/message_format'
3
+
4
+ iterations = 100_000
5
+
6
+ Benchmark.bm do |bm|
7
+ bm.report('parse simple message') do
8
+ parser = MessageFormat::Parser.new()
9
+ iterations.times do
10
+ parser.parse("I'm a super simple message")
11
+ end
12
+ end
13
+
14
+ bm.report('format simple message') do
15
+ message = MessageFormat.new("I'm a super simple message")
16
+ iterations.times do
17
+ message.format()
18
+ end
19
+ end
20
+
21
+ bm.report('parse one arg message') do
22
+ parser = MessageFormat::Parser.new()
23
+ iterations.times do
24
+ parser.parse("I'm a { arg } message")
25
+ end
26
+ end
27
+
28
+ bm.report('format one arg message') do
29
+ message = MessageFormat.new("I'm a { arg } message")
30
+ iterations.times do
31
+ message.format({ :arg => 'awesome' })
32
+ end
33
+ end
34
+
35
+ bm.report('parse complex message') do
36
+ parser = MessageFormat::Parser.new()
37
+ iterations.times do
38
+ parser.parse('On {day, date, short} {
39
+ count, plural, offset:1
40
+ =0 {nobody carpooled.}
41
+ =1 {{driverName} drove {
42
+ driverGender, select,
43
+ male {himself}
44
+ female {herself}
45
+ other {themself}
46
+ }.}
47
+ other {{driverName} drove # people.}
48
+ }')
49
+ end
50
+ end
51
+
52
+ bm.report('format complex message') do
53
+ message = MessageFormat.new('On {day, date, short} {
54
+ count, plural, offset:1
55
+ =0 {nobody carpooled.}
56
+ =1 {{driverName} drove {
57
+ driverGender, select,
58
+ male {himself}
59
+ female {herself}
60
+ other {themself}
61
+ }.}
62
+ other {{driverName} drove # people.}
63
+ }')
64
+ iterations.times do
65
+ message.format({
66
+ :day => DateTime.now,
67
+ :count => 5,
68
+ :driverName => 'Jeremy',
69
+ :driverGender => 'male'
70
+ })
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,30 @@
1
+ require 'twitter_cldr'
2
+ require_relative 'message_format/version'
3
+ require_relative 'message_format/parser'
4
+ require_relative 'message_format/interpreter'
5
+
6
+ module MessageFormat
7
+ class MessageFormat
8
+
9
+ def initialize ( pattern, locale=nil )
10
+ @locale = (locale || TwitterCldr.locale).to_sym
11
+ @format = Interpreter.interpret(
12
+ Parser.parse(pattern),
13
+ { :locale => @locale }
14
+ )
15
+ end
16
+
17
+ def format ( args=nil )
18
+ return @format.call(args)
19
+ end
20
+
21
+ end
22
+
23
+ class << self
24
+
25
+ def new ( pattern, locale=nil )
26
+ return MessageFormat.new(pattern, locale)
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,171 @@
1
+ require 'twitter_cldr'
2
+
3
+ #
4
+ # Interpreter
5
+ #
6
+ # Turns this:
7
+ # [ "You have ", [ "numBananas", "plural", 0, {
8
+ # "=0": [ "no bananas" ],
9
+ # "one": [ "a banana" ],
10
+ # "other": [ [ '#' ], " bananas" ]
11
+ # } ], " for sale." ]
12
+ #
13
+ # into this:
14
+ # format({ numBananas:0 })
15
+ # "You have no bananas for sale."
16
+ #
17
+ module MessageFormat
18
+ class Interpreter
19
+
20
+ def initialize ( options=nil )
21
+ if options and options.has_key?(:locale)
22
+ @originalLocale = options[:locale]
23
+ else
24
+ @originalLocale = TwitterCldr.locale
25
+ end
26
+ end
27
+
28
+ def interpret ( elements )
29
+ return interpretSubs(elements)
30
+ end
31
+
32
+ def interpretSubs ( elements, parent=nil )
33
+ elements = elements.map do |element|
34
+ interpretElement(element, parent)
35
+ end
36
+
37
+ # optimize common case
38
+ if elements.length == 1
39
+ return elements[0]
40
+ end
41
+
42
+ return lambda do |args|
43
+ message = ''
44
+ elements.map do |element|
45
+ message += element.call(args)
46
+ end
47
+ return message
48
+ end
49
+ end
50
+
51
+ def interpretElement ( element, parent=nil )
52
+ if element.is_a?(String)
53
+ return lambda { |args=nil| return element }
54
+ end
55
+
56
+ id, type, style = element
57
+ offset = 0
58
+
59
+ if id == '#'
60
+ id = parent[0]
61
+ type = 'number'
62
+ offset = parent[2] || 0
63
+ style = nil
64
+ end
65
+
66
+ id = id.to_sym # actual arguments should always be keyed by symbols
67
+
68
+ case type
69
+ when 'number'
70
+ return interpretNumber(id, offset, style)
71
+ when 'date', 'time'
72
+ return interpretDateTime(id, type, style)
73
+ when 'plural', 'selectordinal'
74
+ offset = element[2]
75
+ options = element[3]
76
+ return interpretPlural(id, type, offset, options)
77
+ when 'select'
78
+ return interpretSelect(id, style)
79
+ when 'spellout', 'ordinal', 'duration'
80
+ return interpretNumber(id, offset, type)
81
+ else
82
+ return interpretSimple(id)
83
+ end
84
+ end
85
+
86
+ def interpretNumber ( id, offset, style )
87
+ locale = @originalLocale
88
+ return lambda do |args|
89
+ number = TwitterCldr::Localized::LocalizedNumber.new(args[id] - offset, locale)
90
+ if style == 'integer'
91
+ return number.to_decimal(:precision => 0).to_s
92
+ elsif style == 'percent'
93
+ return number.to_percent.to_s
94
+ elsif style == 'currency'
95
+ return number.to_currency.to_s
96
+ elsif style == 'spellout'
97
+ return number.spellout
98
+ elsif style == 'ordinal'
99
+ return number.to_rbnf_s('OrdinalRules', 'digits-ordinal')
100
+ else
101
+ return number.to_s
102
+ end
103
+ end
104
+ end
105
+
106
+ def interpretDateTime ( id, type, style='medium' )
107
+ locale = @originalLocale
108
+ return lambda do |args|
109
+ datetime = TwitterCldr::Localized::LocalizedDateTime.new(args[id], locale)
110
+ datetime = type == 'date' ? datetime.to_date : datetime.to_time
111
+ if style == 'medium'
112
+ return datetime.to_medium_s
113
+ elsif style == 'long'
114
+ return datetime.to_long_s
115
+ elsif style == 'short'
116
+ return datetime.to_short_s
117
+ elsif style == 'full'
118
+ return datetime.to_full_s
119
+ else
120
+ return datetime.to_additional_s(style)
121
+ end
122
+ end
123
+ end
124
+
125
+ def interpretPlural ( id, type, offset, children )
126
+ parent = [ id, type, offset ]
127
+ options = {}
128
+ children.each do |key, value|
129
+ options[key.to_sym] = interpretSubs(value, parent)
130
+ end
131
+
132
+ locale = @originalLocale
133
+ pluralType = type == 'selectordinal' ? :ordinal : :cardinal
134
+ return lambda do |args|
135
+ arg = args[id]
136
+ exactSelector = ('=' + arg.to_s).to_sym
137
+ keywordSelector = TwitterCldr::Formatters::Plurals::Rules.rule_for(arg - offset, locale, pluralType)
138
+ func =
139
+ options[exactSelector] ||
140
+ options[keywordSelector] ||
141
+ options[:other]
142
+ return func.call(args)
143
+ end
144
+ end
145
+
146
+ def interpretSelect ( id, children )
147
+ options = {}
148
+ children.each do |key, value|
149
+ options[key.to_sym] = interpretSubs(value, nil)
150
+ end
151
+ return lambda do |args|
152
+ selector = args[id].to_sym
153
+ func =
154
+ options[selector] ||
155
+ options[:other]
156
+ return func.call(args)
157
+ end
158
+ end
159
+
160
+ def interpretSimple ( id )
161
+ return lambda do |args|
162
+ return args[id].to_s
163
+ end
164
+ end
165
+
166
+ def self.interpret ( elements, options=nil )
167
+ return Interpreter.new(options).interpret(elements)
168
+ end
169
+
170
+ end
171
+ end
@@ -0,0 +1,387 @@
1
+ #
2
+ # Parser
3
+ #
4
+ # Turns this:
5
+ # `You have { numBananas, plural,
6
+ # =0 {no bananas}
7
+ # one {a banana}
8
+ # other {# bananas}
9
+ # } for sale`
10
+ #
11
+ # into this:
12
+ # [ "You have ", [ "numBananas", "plural", 0, {
13
+ # "=0": [ "no bananas" ],
14
+ # "one": [ "a banana" ],
15
+ # "other": [ [ '#' ], " bananas" ]
16
+ # } ], " for sale." ]
17
+ #
18
+ module MessageFormat
19
+ class Parser
20
+
21
+ def initialize ()
22
+ @pattern = nil
23
+ @length = 0
24
+ @index = 0
25
+ end
26
+
27
+ def parse ( pattern )
28
+ if !pattern.is_a?(String)
29
+ throwExpected('String pattern', pattern.class.to_s)
30
+ end
31
+
32
+ @pattern = pattern
33
+ @length = pattern.length
34
+ @index = 0
35
+ return parseMessage("message")
36
+ end
37
+
38
+ def isDigit ( char )
39
+ return (
40
+ char == '0' or
41
+ char == '1' or
42
+ char == '2' or
43
+ char == '3' or
44
+ char == '4' or
45
+ char == '5' or
46
+ char == '6' or
47
+ char == '7' or
48
+ char == '8' or
49
+ char == '9'
50
+ )
51
+ end
52
+
53
+ def isWhitespace ( char )
54
+ return (
55
+ char == "\s" or
56
+ char == "\t" or
57
+ char == "\n" or
58
+ char == "\r" or
59
+ char == "\f" or
60
+ char == "\v" or
61
+ char == "\u00A0" or
62
+ char == "\u2028" or
63
+ char == "\u2029"
64
+ )
65
+ end
66
+
67
+ def skipWhitespace ()
68
+ while @index < @length and isWhitespace(@pattern[@index])
69
+ @index += 1
70
+ end
71
+ end
72
+
73
+ def parseText ( parentType )
74
+ isHashSpecial = (parentType == 'plural' or parentType == 'selectordinal')
75
+ isArgStyle = (parentType == 'style')
76
+ text = ''
77
+ while @index < @length
78
+ char = @pattern[@index]
79
+ if (
80
+ char == '{' or
81
+ char == '}' or
82
+ (isHashSpecial and char == '#') or
83
+ (isArgStyle and isWhitespace(char))
84
+ )
85
+ break
86
+ elsif char == '\''
87
+ @index += 1
88
+ char = @pattern[@index]
89
+ if char == '\'' # double is always 1 '
90
+ text += char
91
+ @index += 1
92
+ elsif (
93
+ # only when necessary
94
+ char == '{' or
95
+ char == '}' or
96
+ (isHashSpecial and char == '#') or
97
+ (isArgStyle and isWhitespace(char))
98
+ )
99
+ text += char
100
+ while @index + 1 < @length
101
+ @index += 1
102
+ char = @pattern[@index]
103
+ if @pattern.slice(@index, 2) == '\'\'' # double is always 1 '
104
+ text += char
105
+ @index += 1
106
+ elsif char == '\'' # end of quoted
107
+ @index += 1
108
+ break
109
+ else
110
+ text += char
111
+ end
112
+ end
113
+ else # lone ' is just a '
114
+ text += '\''
115
+ # already incremented
116
+ end
117
+ else
118
+ text += char
119
+ @index += 1
120
+ end
121
+ end
122
+
123
+ return text
124
+ end
125
+
126
+ def parseArgument ()
127
+ if @pattern[@index] == '#'
128
+ @index += 1 # move passed #
129
+ return [ '#' ]
130
+ end
131
+
132
+ @index += 1 # move passed {
133
+ id = parseArgId()
134
+ char = @pattern[@index]
135
+ if char == '}' # end argument
136
+ @index += 1 # move passed }
137
+ return [ id ]
138
+ end
139
+ if char != ','
140
+ throwExpected(',')
141
+ end
142
+ @index += 1 # move passed ,
143
+
144
+ type = parseArgType()
145
+ char = @pattern[@index]
146
+ if char == '}' # end argument
147
+ if (
148
+ type == 'plural' or
149
+ type == 'selectordinal' or
150
+ type == 'select'
151
+ )
152
+ throwExpected(type + ' message options')
153
+ end
154
+ @index += 1 # move passed }
155
+ return [ id, type ]
156
+ end
157
+ if char != ','
158
+ throwExpected(',')
159
+ end
160
+ @index += 1 # move passed ,
161
+
162
+ format = nil
163
+ offset = nil
164
+ if type == 'plural' or type == 'selectordinal'
165
+ offset = parsePluralOffset()
166
+ format = parseSubMessages(type)
167
+ elsif type == 'select'
168
+ format = parseSubMessages(type)
169
+ else
170
+ format = parseSimpleFormat()
171
+ end
172
+ char = @pattern[@index]
173
+ if char != '}' # not ended argument
174
+ throwExpected('}')
175
+ end
176
+ @index += 1 # move passed
177
+
178
+ return (type == 'plural' or type == 'selectordinal') ?
179
+ [ id, type, offset, format ] :
180
+ [ id, type, format ]
181
+ end
182
+
183
+ def parseArgId ()
184
+ skipWhitespace()
185
+ id = ''
186
+ while @index < @length
187
+ char = @pattern[@index]
188
+ if char == '{' or char == '#'
189
+ throwExpected('argument id')
190
+ end
191
+ if char == '}' or char == ',' or isWhitespace(char)
192
+ break
193
+ end
194
+ id += char
195
+ @index += 1
196
+ end
197
+ if id.empty?
198
+ throwExpected('argument id')
199
+ end
200
+ skipWhitespace()
201
+ return id
202
+ end
203
+
204
+ def parseArgType ()
205
+ skipWhitespace()
206
+ argType = nil
207
+ types = [
208
+ 'number', 'date', 'time', 'ordinal', 'duration', 'spellout', 'plural', 'selectordinal', 'select'
209
+ ]
210
+ types.each do |type|
211
+ if @pattern.slice(@index, type.length) == type
212
+ argType = type
213
+ @index += type.length
214
+ break
215
+ end
216
+ end
217
+ if !argType
218
+ throwExpected(types.join(', '))
219
+ end
220
+ skipWhitespace()
221
+ return argType
222
+ end
223
+
224
+ def parseSimpleFormat ()
225
+ skipWhitespace()
226
+ style = parseText('style')
227
+ if style.empty?
228
+ throwExpected('argument style name')
229
+ end
230
+ skipWhitespace()
231
+ return style
232
+ end
233
+
234
+ def parsePluralOffset ()
235
+ skipWhitespace()
236
+ offset = 0
237
+ if @pattern.slice(@index, 7) == 'offset:'
238
+ @index += 7 # move passed offset:
239
+ skipWhitespace()
240
+ start = @index
241
+ while (
242
+ @index < @length and
243
+ isDigit(@pattern[@index])
244
+ )
245
+ @index += 1
246
+ end
247
+ if start == @index
248
+ throwExpected('offset number')
249
+ end
250
+ offset = @pattern[start..@index].to_i
251
+ skipWhitespace()
252
+ end
253
+ return offset
254
+ end
255
+
256
+ def parseSubMessages ( parentType )
257
+ skipWhitespace()
258
+ options = {}
259
+ hasSubs = false
260
+ while (
261
+ @index < @length and
262
+ @pattern[@index] != '}'
263
+ )
264
+ selector = parseSelector()
265
+ skipWhitespace()
266
+ options[selector] = parseSubMessage(parentType)
267
+ hasSubs = true
268
+ skipWhitespace()
269
+ end
270
+ if !hasSubs
271
+ throwExpected(parentType + ' message options')
272
+ end
273
+ if !options.has_key?('other') # does not have an other selector
274
+ throwExpected(nil, nil, '"other" option must be specified in ' + parentType)
275
+ end
276
+ return options
277
+ end
278
+
279
+ def parseSelector ()
280
+ selector = ''
281
+ while @index < @length
282
+ char = @pattern[@index]
283
+ if char == '}' or char == ','
284
+ throwExpected('{')
285
+ end
286
+ if char == '{' or isWhitespace(char)
287
+ break
288
+ end
289
+ selector += char
290
+ @index += 1
291
+ end
292
+ if selector.empty?
293
+ throwExpected('selector')
294
+ end
295
+ skipWhitespace()
296
+ return selector
297
+ end
298
+
299
+ def parseSubMessage ( parentType )
300
+ char = @pattern[@index]
301
+ if char != '{'
302
+ throwExpected('{')
303
+ end
304
+ @index += 1 # move passed {
305
+ message = parseMessage(parentType)
306
+ char = @pattern[@index]
307
+ if char != '}'
308
+ throwExpected('}')
309
+ end
310
+ @index += 1 # move passed }
311
+ return message
312
+ end
313
+
314
+ def parseMessage ( parentType )
315
+ elements = []
316
+ text = parseText(parentType)
317
+ if !text.empty?
318
+ elements.push(text)
319
+ end
320
+ while @index < @length
321
+ if @pattern[@index] == '}'
322
+ if parentType == 'message'
323
+ throwExpected()
324
+ end
325
+ break
326
+ end
327
+ elements.push(parseArgument())
328
+ text = parseText(parentType)
329
+ if !text.empty?
330
+ elements.push(text)
331
+ end
332
+ end
333
+ return elements
334
+ end
335
+
336
+ def throwExpected ( expected=nil, found=nil, message=nil )
337
+ lines = @pattern[0..@index].split(/\r?\n/)
338
+ line = lines.length
339
+ column = lines.last.length
340
+ if !found
341
+ found = @index < @length ? @pattern[@index] : 'end of input'
342
+ end
343
+ if !message
344
+ message = errorMessage(expected, found)
345
+ end
346
+ message += ' in "' + @pattern.gsub(/\r?\n/, "\n") + '"'
347
+
348
+ raise SyntaxError.new(message, expected, found, @index, line, column)
349
+ end
350
+
351
+ def errorMessage ( expected=nil, found )
352
+ if !expected
353
+ return "Unexpected \"#{ found }\" found"
354
+ end
355
+ return "Expected \"#{ expected }\" but found \"#{ found }\""
356
+ end
357
+
358
+ def self.parse ( pattern )
359
+ return Parser.new().parse(pattern)
360
+ end
361
+
362
+ #
363
+ # Syntax Error
364
+ # Holds information about bad syntax found in a message pattern
365
+ #
366
+ class SyntaxError < StandardError
367
+
368
+ attr_reader :message
369
+ attr_reader :expected
370
+ attr_reader :found
371
+ attr_reader :offset
372
+ attr_reader :line
373
+ attr_reader :column
374
+
375
+ def initialize (message, expected, found, offset, line, column)
376
+ @message = message
377
+ @expected = expected
378
+ @found = found
379
+ @offset = offset
380
+ @line = line
381
+ @column = column
382
+ end
383
+
384
+ end
385
+
386
+ end
387
+ end
@@ -0,0 +1,3 @@
1
+ module MessageFormat
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'message_format/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "message_format"
8
+ spec.version = MessageFormat::VERSION
9
+ spec.authors = ["Andy VanWagoner"]
10
+ spec.email = ["andy@instructure.com"]
11
+ spec.summary = %q{Parse and format i18n messages using ICU MessageFormat patterns}
12
+ spec.description = %q{Parse and format i18n messages using ICU MessageFormat patterns, including simple placeholders, number and date placeholders, and selecting among submessages for gender and plural arguments.}
13
+ spec.homepage = "https://github.com/thetalecrafter/message-format-rb"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_runtime_dependency "twitter_cldr", "~> 3.1"
22
+ spec.add_development_dependency "bundler", "~> 1.6"
23
+ spec.add_development_dependency "rake", "~> 10.0"
24
+ end
@@ -0,0 +1,131 @@
1
+ require 'spec_helper'
2
+
3
+ describe MessageFormat do
4
+ describe '#new' do
5
+ it 'throws an error on bad syntax' do
6
+ expect { MessageFormat.new({}) }.to raise_error
7
+ expect { MessageFormat.new('no finish arg {') }.to raise_error
8
+ expect { MessageFormat.new('no start arg }') }.to raise_error
9
+ expect { MessageFormat.new('empty arg {}') }.to raise_error
10
+ expect { MessageFormat.new('unfinished select { a, select }') }.to raise_error
11
+ expect { MessageFormat.new('unfinished select { a, select, }') }.to raise_error
12
+ expect { MessageFormat.new('sub with no selector { a, select, {hi} }') }.to raise_error
13
+ expect { MessageFormat.new('sub with no other { a, select, foo {hi} }') }.to raise_error
14
+ expect { MessageFormat.new('wrong escape \\{') }.to raise_error
15
+ expect { MessageFormat.new('wrong escape \'{\'', 'en', { escape: '\\' }) }.to raise_error
16
+ expect { MessageFormat.new('bad arg type { a, bogus, nope }') }.to raise_error
17
+ expect { MessageFormat.new('bad arg separator { a bogus, nope }') }.to raise_error
18
+ end
19
+ end
20
+
21
+ describe '#format' do
22
+ it 'formats a simple message' do
23
+ pattern = 'Simple string with nothing special'
24
+ message = MessageFormat.new(pattern, 'en-US').format()
25
+
26
+ expect(message).to eql('Simple string with nothing special')
27
+ end
28
+
29
+ it 'handles pattern with escaped text' do
30
+ pattern = 'This isn\'\'t a \'{\'\'simple\'\'}\' \'string\''
31
+ message = MessageFormat.new(pattern, 'en-US').format()
32
+
33
+ expect(message).to eql('This isn\'t a {\'simple\'} \'string\'')
34
+ end
35
+
36
+ it 'accepts arguments' do
37
+ pattern = 'x{ arg }z'
38
+ message = MessageFormat.new(pattern, 'en-US').format({ :arg => 'y' })
39
+
40
+ expect(message).to eql('xyz')
41
+ end
42
+
43
+ it 'formats numbers, dates, and times' do
44
+ pattern = '{ n, number } : { d, date, short } { d, time, short }'
45
+ message = MessageFormat.new(pattern, 'en-US').format({ :n => 0, :d => DateTime.new(0) })
46
+
47
+ expect(message).to match(/^0 \: \d\d?\/\d\d?\/\d{2,4} \d\d?\:\d\d [AP]M$/)
48
+ end
49
+
50
+ it 'handles plurals' do
51
+ pattern =
52
+ 'On {takenDate, date, short} {name} {numPeople, plural, offset:1
53
+ =0 {didn\'t carpool.}
54
+ =1 {drove himself.}
55
+ other {drove # people.}}'
56
+ message = MessageFormat.new(pattern, 'en-US')
57
+ .format({ :takenDate => DateTime.now, :name => 'Bob', :numPeople => 5 })
58
+
59
+ expect(message).to match(/^On \d\d?\/\d\d?\/\d{2,4} Bob drove 4 people.$/)
60
+ end
61
+
62
+ it 'handles plurals for other locales' do
63
+ pattern =
64
+ '{n, plural,
65
+ zero {zero}
66
+ one {one}
67
+ two {two}
68
+ few {few}
69
+ many {many}
70
+ other {other}}'
71
+ message = MessageFormat.new(pattern, 'ar')
72
+
73
+ expect(message.format({ n: 0 })).to eql('zero')
74
+ expect(message.format({ n: 1 })).to eql('one')
75
+ expect(message.format({ n: 2 })).to eql('two')
76
+ expect(message.format({ n: 3 })).to eql('few')
77
+ expect(message.format({ n: 11 })).to eql('many')
78
+ end
79
+
80
+ it 'handles selectordinals' do
81
+ pattern =
82
+ '{n, selectordinal,
83
+ one {#st}
84
+ two {#nd}
85
+ few {#rd}
86
+ other {#th}}'
87
+ message = MessageFormat.new(pattern, 'en')
88
+
89
+ expect(message.format({ n: 1 })).to eql('1st')
90
+ expect(message.format({ n: 22 })).to eql('22nd')
91
+ expect(message.format({ n: 103 })).to eql('103rd')
92
+ expect(message.format({ n: 4 })).to eql('4th')
93
+ end
94
+
95
+ it 'handles select' do
96
+ pattern =
97
+ '{ gender, select,
98
+ male {it\'s his turn}
99
+ female {it\'s her turn}
100
+ other {it\'s their turn}}'
101
+ message = MessageFormat.new(pattern, 'en-US')
102
+ .format({ gender: 'female' })
103
+
104
+ expect(message).to eql('it\'s her turn')
105
+ end
106
+
107
+ it 'should throw an error when args are expected and not passed' do
108
+ expect { MessageFormat.new('{a}').format() }.to raise_error
109
+ end
110
+ end
111
+
112
+ describe 'locales' do
113
+ it 'doesn\'t throw for any locale\'s plural function' do
114
+ pattern =
115
+ '{n, plural,
116
+ zero {zero}
117
+ one {one}
118
+ two {two}
119
+ few {few}
120
+ many {many}
121
+ other {other}}'
122
+ TwitterCldr.supported_locales.each do |locale|
123
+ message = MessageFormat.new(pattern, locale)
124
+ for n in 0..200 do
125
+ result = message.format({ :n => n })
126
+ expect(result).to match(/^(zero|one|two|few|many|other)$/)
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1 @@
1
+ require_relative '../lib/message_format'
metadata ADDED
@@ -0,0 +1,104 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: message_format
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Andy VanWagoner
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-04-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: twitter_cldr
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.6'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.6'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ description: Parse and format i18n messages using ICU MessageFormat patterns, including
56
+ simple placeholders, number and date placeholders, and selecting among submessages
57
+ for gender and plural arguments.
58
+ email:
59
+ - andy@instructure.com
60
+ executables: []
61
+ extensions: []
62
+ extra_rdoc_files: []
63
+ files:
64
+ - ".gitignore"
65
+ - Gemfile
66
+ - LICENSE.txt
67
+ - README.md
68
+ - Rakefile
69
+ - benchmark.rb
70
+ - lib/message_format.rb
71
+ - lib/message_format/interpreter.rb
72
+ - lib/message_format/parser.rb
73
+ - lib/message_format/version.rb
74
+ - message_format.gemspec
75
+ - spec/message_format_spec.rb
76
+ - spec/spec_helper.rb
77
+ homepage: https://github.com/thetalecrafter/message-format-rb
78
+ licenses:
79
+ - MIT
80
+ metadata: {}
81
+ post_install_message:
82
+ rdoc_options: []
83
+ require_paths:
84
+ - lib
85
+ required_ruby_version: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ required_rubygems_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ requirements: []
96
+ rubyforge_project:
97
+ rubygems_version: 2.2.2
98
+ signing_key:
99
+ specification_version: 4
100
+ summary: Parse and format i18n messages using ICU MessageFormat patterns
101
+ test_files:
102
+ - spec/message_format_spec.rb
103
+ - spec/spec_helper.rb
104
+ has_rdoc: