babelfish-ruby 1.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 +7 -0
- data/.gitignore +4 -0
- data/.rspec +2 -0
- data/Gemfile +3 -0
- data/LICENSE +21 -0
- data/README.md +137 -0
- data/Rakefile +25 -0
- data/babelfish.gemspec +27 -0
- data/lib/babelfish-ruby.rb +2 -0
- data/lib/babelfish.rb +289 -0
- data/lib/babelfish/phrase/compiler.rb +70 -0
- data/lib/babelfish/phrase/literal.rb +12 -0
- data/lib/babelfish/phrase/node.rb +14 -0
- data/lib/babelfish/phrase/parser.rb +173 -0
- data/lib/babelfish/phrase/parser_base.rb +73 -0
- data/lib/babelfish/phrase/plural_forms.rb +65 -0
- data/lib/babelfish/phrase/plural_forms_parser.rb +64 -0
- data/lib/babelfish/phrase/pluralizer.rb +409 -0
- data/lib/babelfish/phrase/string_to_compile.rb +8 -0
- data/lib/babelfish/phrase/variable.rb +12 -0
- data/lib/babelfish/version.rb +4 -0
- data/spec/lib/babelfish_spec.rb +105 -0
- data/spec/locales/test.en-US.yml +7 -0
- data/spec/locales/test.ru-RU.yml +9 -0
- data/spec/spec_helper.rb +21 -0
- metadata +147 -0
@@ -0,0 +1,70 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
require 'babelfish/phrase/literal'
|
3
|
+
require 'babelfish/phrase/variable'
|
4
|
+
require 'babelfish/phrase/plural_forms'
|
5
|
+
|
6
|
+
class Babelfish
|
7
|
+
module Phrase
|
8
|
+
# Babelfish AST Compiler.
|
9
|
+
# Compiles AST to string or to Proc.
|
10
|
+
class Compiler
|
11
|
+
attr_accessor :ast
|
12
|
+
|
13
|
+
def initialize( ast = nil )
|
14
|
+
init( ast ) unless ast.nil?
|
15
|
+
end
|
16
|
+
|
17
|
+
# Initializes compiler. Should not be called directly.
|
18
|
+
def init( ast )
|
19
|
+
self.ast = ast
|
20
|
+
end
|
21
|
+
|
22
|
+
# Throws given message in compiler context.
|
23
|
+
def throw( message )
|
24
|
+
raise "Cannot compile: #{message}";
|
25
|
+
end
|
26
|
+
|
27
|
+
# Compiles AST.
|
28
|
+
|
29
|
+
# Result is string when possible; Proc otherwise.
|
30
|
+
def compile ( ast )
|
31
|
+
init( ast ) unless ast.nil?
|
32
|
+
|
33
|
+
throw("No AST given") if ast.nil?
|
34
|
+
throw("Empty AST given") if ast.length == 0;
|
35
|
+
|
36
|
+
if ast.length == 1 && ast.first.kind_of?(Babelfish::Phrase::Literal)
|
37
|
+
# просто строка
|
38
|
+
return ast.first.text
|
39
|
+
end
|
40
|
+
|
41
|
+
ready = ast.map do |node|
|
42
|
+
case node
|
43
|
+
when Babelfish::Phrase::Literal
|
44
|
+
node.text
|
45
|
+
when Babelfish::Phrase::Variable
|
46
|
+
node
|
47
|
+
when Babelfish::Phrase::PluralForms
|
48
|
+
sub = node.to_ruby_method
|
49
|
+
else
|
50
|
+
throw("Unknown AST node: #{node}")
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
lambda do |params|
|
55
|
+
data = ready.map do |what|
|
56
|
+
case what
|
57
|
+
when Babelfish::Phrase::Variable
|
58
|
+
params[what.name.to_s].to_s
|
59
|
+
when Proc
|
60
|
+
what = what.call(params)
|
61
|
+
else
|
62
|
+
what
|
63
|
+
end
|
64
|
+
end.join('')
|
65
|
+
data
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
class Babelfish
|
3
|
+
module Phrase
|
4
|
+
# Babelfish AST abstract node.
|
5
|
+
class Node
|
6
|
+
|
7
|
+
def initialize( args = {} )
|
8
|
+
args.keys.each do |key|
|
9
|
+
send("#{key}=", args[key]) if respond_to?("#{key}=")
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,173 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
require 'babelfish/phrase/parser_base'
|
3
|
+
require 'babelfish/phrase/literal'
|
4
|
+
require 'babelfish/phrase/variable'
|
5
|
+
require 'babelfish/phrase/plural_forms'
|
6
|
+
require 'babelfish/phrase/plural_forms_parser'
|
7
|
+
|
8
|
+
class Babelfish
|
9
|
+
module Phrase
|
10
|
+
# Babelfish syntax parser.
|
11
|
+
class Parser < ParserBase
|
12
|
+
|
13
|
+
attr_accessor :locale, :mode, :pieces, :escape, :pf0
|
14
|
+
|
15
|
+
LITERAL_MODE = 'Literal'.freeze
|
16
|
+
VARIABLE_MODE = 'Variable'.freeze
|
17
|
+
PLURALS_MODE = 'Plurals'.freeze
|
18
|
+
VARIABLE_RE = /^[a-zA-Z0-9_\.]+$/
|
19
|
+
|
20
|
+
AST_MAP = {
|
21
|
+
LITERAL_MODE => Babelfish::Phrase::Literal,
|
22
|
+
VARIABLE_MODE => Babelfish::Phrase::Variable,
|
23
|
+
PLURALS_MODE => Babelfish::Phrase::PluralForms,
|
24
|
+
}
|
25
|
+
|
26
|
+
# Instantiates parser.
|
27
|
+
def initialize( phrase = nil, locale = nil )
|
28
|
+
super( phrase )
|
29
|
+
init( phrase ) unless phrase.nil?
|
30
|
+
self.locale = locale if locale
|
31
|
+
end
|
32
|
+
|
33
|
+
# Initializes parser. Should not be called directly.
|
34
|
+
def init( phrase )
|
35
|
+
super( phrase )
|
36
|
+
self.mode = LITERAL_MODE
|
37
|
+
self.pieces = []
|
38
|
+
self.pf0 = nil # plural forms without name yet
|
39
|
+
end
|
40
|
+
|
41
|
+
# Finalizes all operations after phrase end.
|
42
|
+
def finalize_mode
|
43
|
+
case mode
|
44
|
+
when LITERAL_MODE
|
45
|
+
pieces.push( AST_MAP[LITERAL_MODE].new( text: piece ) ) if !piece.empty? || pieces.size == 0;
|
46
|
+
when VARIABLE_MODE
|
47
|
+
throw( "Variable definition not ended with \"}\": " + piece )
|
48
|
+
when PLURALS_MODE
|
49
|
+
throw( "Plural forms definition not ended with \"))\": " + piece ) if pf0.nil?
|
50
|
+
pieces.push( AST_MAP[PLURALS_MODE].new( forms: pf0, name: piece, locale: locale ) )
|
51
|
+
else
|
52
|
+
throw( "Logic broken, unknown parser mode: " + mode );
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class << self
|
57
|
+
attr_accessor :plurals_parser
|
58
|
+
end
|
59
|
+
|
60
|
+
def plurals_parser
|
61
|
+
Parser.plurals_parser ||= Babelfish::Phrase::PluralFormsParser.new
|
62
|
+
end
|
63
|
+
|
64
|
+
def escape?
|
65
|
+
!! self.escape
|
66
|
+
end
|
67
|
+
|
68
|
+
# Parses specified phrase.
|
69
|
+
def parse( phrase = nil, locale = nil )
|
70
|
+
super( phrase )
|
71
|
+
self.locale = locale unless locale.nil?
|
72
|
+
|
73
|
+
while true
|
74
|
+
_char = to_next_char
|
75
|
+
|
76
|
+
if _char.empty?
|
77
|
+
finalize_mode
|
78
|
+
return pieces
|
79
|
+
end
|
80
|
+
|
81
|
+
case mode
|
82
|
+
when LITERAL_MODE
|
83
|
+
if escape?
|
84
|
+
add_to_piece( _char )
|
85
|
+
self.escape = false
|
86
|
+
next
|
87
|
+
end
|
88
|
+
|
89
|
+
if _char == "\\"
|
90
|
+
self.escape = true
|
91
|
+
next
|
92
|
+
end
|
93
|
+
|
94
|
+
if _char == '#' && next_char == '{'
|
95
|
+
unless piece.empty?
|
96
|
+
pieces.push( AST_MAP[LITERAL_MODE].new( text: piece ) )
|
97
|
+
self.piece = ''
|
98
|
+
end
|
99
|
+
self.to_next_char # skip "{"
|
100
|
+
self.mode = VARIABLE_MODE
|
101
|
+
next
|
102
|
+
end
|
103
|
+
|
104
|
+
if _char == '(' && next_char == '('
|
105
|
+
unless piece.empty?
|
106
|
+
pieces.push( AST_MAP[LITERAL_MODE].new( text: piece ) )
|
107
|
+
self.piece = ''
|
108
|
+
end
|
109
|
+
to_next_char # skip second "("
|
110
|
+
self.mode = PLURALS_MODE
|
111
|
+
next
|
112
|
+
end
|
113
|
+
|
114
|
+
when VARIABLE_MODE
|
115
|
+
if escape?
|
116
|
+
add_to_piece( _char )
|
117
|
+
self.escape = false
|
118
|
+
next
|
119
|
+
end
|
120
|
+
|
121
|
+
if _char == "\\"
|
122
|
+
self.escape = true
|
123
|
+
next
|
124
|
+
end
|
125
|
+
|
126
|
+
if _char == '}'
|
127
|
+
name = piece.strip
|
128
|
+
if name.empty?
|
129
|
+
throw( "No variable name given." );
|
130
|
+
end
|
131
|
+
if name !~ VARIABLE_RE
|
132
|
+
throw( "Variable name doesn't meet conditions: #{name}." );
|
133
|
+
end
|
134
|
+
pieces.push( AST_MAP[VARIABLE_MODE].new( name: name ) )
|
135
|
+
self.piece = ''
|
136
|
+
self.mode = LITERAL_MODE
|
137
|
+
next
|
138
|
+
end
|
139
|
+
|
140
|
+
when PLURALS_MODE
|
141
|
+
unless pf0.nil?
|
142
|
+
if _char =~ VARIABLE_RE && (_char != '.' || next_char =~ VARIABLE_RE)
|
143
|
+
add_to_piece( _char )
|
144
|
+
next
|
145
|
+
else
|
146
|
+
pieces.push( AST_MAP[PLURALS_MODE].new( forms: pf0, name: piece, locale:locale ) )
|
147
|
+
self.pf0 = nil
|
148
|
+
self.mode = LITERAL_MODE
|
149
|
+
self.piece = ''
|
150
|
+
backward
|
151
|
+
next
|
152
|
+
end
|
153
|
+
end
|
154
|
+
if _char == ')' && next_char == ')'
|
155
|
+
self.pf0 = plurals_parser.parse( piece )
|
156
|
+
self.piece = ''
|
157
|
+
to_next_char # skip second ")"
|
158
|
+
if next_char == ':'
|
159
|
+
to_next_char # skip ":"
|
160
|
+
next
|
161
|
+
end
|
162
|
+
pieces.push( AST_MAP[PLURALS_MODE].new( forms: pf0, name: 'count', locale: locale ) )
|
163
|
+
self.pf0 = nil
|
164
|
+
self.mode = LITERAL_MODE
|
165
|
+
next
|
166
|
+
end
|
167
|
+
end
|
168
|
+
add_to_piece( _char )
|
169
|
+
end # while ( 1 )
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
class Babelfish
|
3
|
+
module Phrase
|
4
|
+
# Babelfish abstract parser.
|
5
|
+
class ParserBase
|
6
|
+
attr_accessor :phrase, :index, :length, :prev, :piece, :escape
|
7
|
+
|
8
|
+
|
9
|
+
def initialize(phrase = nil)
|
10
|
+
init(phrase) if phrase
|
11
|
+
end
|
12
|
+
|
13
|
+
def init(phrase)
|
14
|
+
self.phrase = phrase
|
15
|
+
self.index = -1
|
16
|
+
self.prev = nil
|
17
|
+
self.length = phrase.length
|
18
|
+
self.piece = ''
|
19
|
+
self.escape = false
|
20
|
+
end
|
21
|
+
|
22
|
+
# Gets character on current cursor position.
|
23
|
+
# Will return empty string if no character.
|
24
|
+
def char
|
25
|
+
r = phrase[ index ]
|
26
|
+
r.nil? ? '' : r
|
27
|
+
end
|
28
|
+
|
29
|
+
# Gets character on next cursor position.
|
30
|
+
# Will return empty string if no character.
|
31
|
+
def next_char
|
32
|
+
return '' if index >= length - 1
|
33
|
+
r = phrase[ index + 1 ]
|
34
|
+
r.nil? ? '' : r
|
35
|
+
end
|
36
|
+
|
37
|
+
# Moves cursor to next position.
|
38
|
+
# Return new current character.
|
39
|
+
def to_next_char
|
40
|
+
self.prev = char if self.index >= 0
|
41
|
+
self.index = self.index + 1
|
42
|
+
return '' if self.index == length
|
43
|
+
char()
|
44
|
+
end
|
45
|
+
|
46
|
+
# Throws given message in phrase context.
|
47
|
+
def throw( message )
|
48
|
+
raise "Cannot parse phrase \""+ ( phrase || 'nil' )+ "\" at ". ( index || '-1' )+ " index: #{message}"
|
49
|
+
end
|
50
|
+
|
51
|
+
# Adds given chars to current piece.
|
52
|
+
def add_to_piece(chars)
|
53
|
+
self.piece += chars
|
54
|
+
end
|
55
|
+
|
56
|
+
# Moves cursor backward.
|
57
|
+
def backward
|
58
|
+
self.index = index - 1
|
59
|
+
if index > 0
|
60
|
+
r = phrase[ index - 1 ]
|
61
|
+
self.prev = r.nil? ? '' : r
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Parses specified phrase.
|
66
|
+
def parse( phrase = nil )
|
67
|
+
init(phrase) unless phrase.nil?
|
68
|
+
throw( "No phrase given" ) if phrase.nil?
|
69
|
+
phrase
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
require 'babelfish/phrase/node'
|
3
|
+
require 'babelfish/phrase/pluralizer'
|
4
|
+
require 'babelfish/phrase/compiler'
|
5
|
+
|
6
|
+
class Babelfish
|
7
|
+
module Phrase
|
8
|
+
# Babelfish AST pluralization node.
|
9
|
+
class PluralForms < Node
|
10
|
+
|
11
|
+
class << self
|
12
|
+
attr_accessor :sub_data, :compiler
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
PluralForms.sub_data = []
|
17
|
+
|
18
|
+
attr_accessor :forms, :name, :compiled, :locale
|
19
|
+
|
20
|
+
def to_ruby_method
|
21
|
+
unless compiled
|
22
|
+
PluralForms.compiler ||= Babelfish::Phrase::Compiler.new
|
23
|
+
forms[:regular].map! do |form|
|
24
|
+
PluralForms.compiler.compile( form )
|
25
|
+
end
|
26
|
+
new_strict = {}
|
27
|
+
forms[:strict].each_pair do |key, form|
|
28
|
+
new_strict[key] = PluralForms.compiler.compile( form )
|
29
|
+
end
|
30
|
+
forms[:strict].replace(new_strict)
|
31
|
+
self.compiled = true
|
32
|
+
end
|
33
|
+
|
34
|
+
rule = Babelfish::Phrase::Pluralizer::find_rule( locale )
|
35
|
+
|
36
|
+
PluralForms.sub_data << [
|
37
|
+
rule,
|
38
|
+
forms[:strict],
|
39
|
+
forms[:regular],
|
40
|
+
]
|
41
|
+
|
42
|
+
return _to_ruby_method( name, PluralForms.sub_data.length - 1 );
|
43
|
+
end
|
44
|
+
|
45
|
+
def _to_ruby_method( name, index )
|
46
|
+
lambda do |params|
|
47
|
+
value = params[name].to_f
|
48
|
+
rule, strict_forms, regular_forms = PluralForms.sub_data[index]
|
49
|
+
r = nil
|
50
|
+
if value.nan?
|
51
|
+
warn "#{name} parameter is not numeric"
|
52
|
+
r = regular_forms[-1]
|
53
|
+
else
|
54
|
+
r = strict_forms[value] || regular_forms[rule.call(value)] || regular_forms[-1];
|
55
|
+
end
|
56
|
+
return r if r.kind_of?(String)
|
57
|
+
return '' if r.nil?
|
58
|
+
r.call(params)
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
require 'babelfish/phrase/plural_forms'
|
3
|
+
|
4
|
+
class Babelfish
|
5
|
+
module Phrase
|
6
|
+
# Babelfish plurals syntax parser.
|
7
|
+
|
8
|
+
# Returns { script_forms: {}, regular_forms: [] }
|
9
|
+
|
10
|
+
# Every plural form represented as AST.
|
11
|
+
|
12
|
+
class PluralFormsParser
|
13
|
+
attr_accessor :phrase, :strict_forms, :regular_forms
|
14
|
+
|
15
|
+
class << self
|
16
|
+
attr_accessor :phrase_parser
|
17
|
+
end
|
18
|
+
|
19
|
+
def phrase_parser
|
20
|
+
PluralFormsParser.phrase_parser ||= Babelfish::Phrase::Parser.new
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
# Instantiates parser.
|
25
|
+
def initialize( phrase = nil )
|
26
|
+
init( phrase ) unless phrase.nil?
|
27
|
+
end
|
28
|
+
|
29
|
+
# Initializes parser. Should not be called directly.
|
30
|
+
def init( phrase )
|
31
|
+
self.phrase = phrase
|
32
|
+
self.regular_forms = []
|
33
|
+
self.strict_forms = {}
|
34
|
+
end
|
35
|
+
|
36
|
+
# Parses specified phrase.
|
37
|
+
def parse( phrase )
|
38
|
+
init( $phrase ) unless phrase.nil?
|
39
|
+
|
40
|
+
# тут проще регуляркой
|
41
|
+
forms = phrase.split( /(?<!\\)\|/ )
|
42
|
+
|
43
|
+
forms.each do |form|
|
44
|
+
value = nil
|
45
|
+
if form =~ /^=([0-9]+)\s*(.+)$/
|
46
|
+
value, form = $1, $2
|
47
|
+
end
|
48
|
+
form = phrase_parser.parse( form )
|
49
|
+
|
50
|
+
if value.nil?
|
51
|
+
regular_forms.push form
|
52
|
+
else
|
53
|
+
strict_forms[value] = form
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
return {
|
58
|
+
strict: strict_forms,
|
59
|
+
regular: regular_forms,
|
60
|
+
}
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|