i18nliner 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.
- data/LICENSE.txt +20 -0
- data/README.md +320 -0
- data/Rakefile +9 -0
- data/lib/i18nliner/call_helpers.rb +69 -0
- data/lib/i18nliner/commands/basic_formatter.rb +13 -0
- data/lib/i18nliner/commands/check.rb +59 -0
- data/lib/i18nliner/commands/color_formatter.rb +13 -0
- data/lib/i18nliner/commands/dump.rb +8 -0
- data/lib/i18nliner/commands/generic_command.rb +17 -0
- data/lib/i18nliner/errors.rb +29 -0
- data/lib/i18nliner/extractors/abstract_extractor.rb +33 -0
- data/lib/i18nliner/extractors/ruby_extractor.rb +102 -0
- data/lib/i18nliner/extractors/translate_call.rb +111 -0
- data/lib/i18nliner/extractors/translation_hash.rb +45 -0
- data/lib/i18nliner/processors/abstract_processor.rb +33 -0
- data/lib/i18nliner/processors/erb_processor.rb +16 -0
- data/lib/i18nliner/processors/ruby_processor.rb +22 -0
- data/lib/i18nliner/processors.rb +11 -0
- data/lib/i18nliner/scope.rb +24 -0
- data/lib/i18nliner.rb +31 -0
- data/lib/tasks/i18nliner.rake +14 -0
- data/spec/extractors/ruby_extractor_spec.rb +61 -0
- data/spec/extractors/translate_call_spec.rb +154 -0
- metadata +182 -0
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2013 Jon Jensen
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,320 @@
|
|
1
|
+
# I18nliner
|
2
|
+
|
3
|
+
yay readme-driven development!
|
4
|
+
|
5
|
+
## TODO
|
6
|
+
|
7
|
+
* inferred placeholders (instance vars and methods)
|
8
|
+
* ERB pre-processor
|
9
|
+
* wrapper inference
|
10
|
+
* helper/placeholder extraction
|
11
|
+
* rake tasks
|
12
|
+
* dump
|
13
|
+
* diff
|
14
|
+
* import
|
15
|
+
|
16
|
+
====
|
17
|
+
|
18
|
+
I18nliner is I18n made simple.
|
19
|
+
|
20
|
+
No .yml files. Inline defaults. Optional keys. Inferred interpolation values.
|
21
|
+
Wrappers and blocks, so your templates look template-y and your translations
|
22
|
+
HTML-free.
|
23
|
+
|
24
|
+
## TL;DR
|
25
|
+
|
26
|
+
I18nliner lets you do stuff like this:
|
27
|
+
|
28
|
+
t "Ohai %{@user.name}, my default translation is right here in the code. " +
|
29
|
+
"Inferred keys and placeholder values, oh my!"
|
30
|
+
|
31
|
+
and even this:
|
32
|
+
|
33
|
+
<%= t do %>
|
34
|
+
Hey <%= amigo %>!
|
35
|
+
Although I am <%= link_to "linking to something", random_path %> and
|
36
|
+
have some <strong>bold text</strong>, the translators will see
|
37
|
+
<strong><em>absolutely no markup</em></strong> and will only have a
|
38
|
+
single string to translate :o
|
39
|
+
<% end %>
|
40
|
+
|
41
|
+
## Installation
|
42
|
+
|
43
|
+
Add the following to your Gemfile:
|
44
|
+
|
45
|
+
gem 'i18nliner'
|
46
|
+
|
47
|
+
## Features
|
48
|
+
|
49
|
+
### No more en.yml
|
50
|
+
|
51
|
+
Instead of maintaining .yml files and doing stuff like this:
|
52
|
+
|
53
|
+
I18n.t :account_page_title
|
54
|
+
|
55
|
+
Forget the .yml and just do:
|
56
|
+
|
57
|
+
I18n.t :account_page_title, "My Account"
|
58
|
+
|
59
|
+
Regular I18n options follow the (optional) default translation, so you can do
|
60
|
+
the usual stuff (placeholders, etc.).
|
61
|
+
|
62
|
+
#### Okay, but don't the translators need en.yml?
|
63
|
+
|
64
|
+
Sure, but *you* don't need to write it. Just run:
|
65
|
+
|
66
|
+
rake i18nliner:dump
|
67
|
+
|
68
|
+
This extracts all default translations from your codebase, merges them with any
|
69
|
+
other ones (from rails or pre-existing .yml files), and outputs them to
|
70
|
+
`config/locales/generated/en.yml` (or rather, `"#{I18n.default_locale}.yml"`).
|
71
|
+
|
72
|
+
### It's okay to lose your keys
|
73
|
+
|
74
|
+
Why waste time coming up with keys that are less descriptive than the default
|
75
|
+
translation? I18nliner makes keys optional, so you can just do this:
|
76
|
+
|
77
|
+
I18n.t "My Account"
|
78
|
+
|
79
|
+
I18nliner will create a [unique key](CONFIG.md) based on the translation (e.g.
|
80
|
+
`:my_account`), so you don't have to.
|
81
|
+
|
82
|
+
This can actually be a **good thing**, because when the `en` changes, the key
|
83
|
+
changes, which means you know you need to get it retranslated (instead of
|
84
|
+
letting a now-inaccurate translation hang out indefinitely). Whether you want
|
85
|
+
to show "[ missing translation ]" or the `en` value in the meantime is up to
|
86
|
+
you.
|
87
|
+
|
88
|
+
### Inferred Interpolation Values
|
89
|
+
|
90
|
+
Interpolation values may be inferred by I18nliner if not provided. So long as
|
91
|
+
it's an instance variable or method (or chain), you don't need to specify its
|
92
|
+
value. So this:
|
93
|
+
|
94
|
+
<p>
|
95
|
+
<%= t "Hello, %{user}. This request was a %{request_method}.",
|
96
|
+
:user => @user.name,
|
97
|
+
:request_method => request.method
|
98
|
+
%>
|
99
|
+
</p>
|
100
|
+
|
101
|
+
Can just be this:
|
102
|
+
|
103
|
+
<p>
|
104
|
+
<%= t "Hello, %{@user.name}. This request was a %{request.method}." %>
|
105
|
+
</p>
|
106
|
+
|
107
|
+
Note that local variables cannot be inferred.
|
108
|
+
|
109
|
+
### Wrappers and Blocks
|
110
|
+
|
111
|
+
#### The Problem
|
112
|
+
|
113
|
+
Suppose you have something like this in your ERB:
|
114
|
+
|
115
|
+
<p>
|
116
|
+
You can <%= link_to "lead", new_discussion_path %> a new discussion or
|
117
|
+
<%= link_to "join", discussion_search_path %> an existing one.
|
118
|
+
</p>
|
119
|
+
|
120
|
+
You might try something like this:
|
121
|
+
|
122
|
+
<p>
|
123
|
+
<%= t "You can %{lead} a new discussion or %{join} an existing one.",
|
124
|
+
:lead => link_to(t("lead"), new_discussion_path),
|
125
|
+
:join => link_to(t("join"), discussion_search_path)
|
126
|
+
%>
|
127
|
+
</p>
|
128
|
+
|
129
|
+
This is not great, because:
|
130
|
+
|
131
|
+
1. There are three strings to translate.
|
132
|
+
2. When translating the verbs, the translator has no context for where it's
|
133
|
+
being used... Is "lead" a verb or a noun?
|
134
|
+
3. Translators have their hands somewhat tied as far as what is inside the
|
135
|
+
links and what is not.
|
136
|
+
|
137
|
+
So you might try this instead:
|
138
|
+
|
139
|
+
<p>
|
140
|
+
<%= t :discussion_html,
|
141
|
+
"You can <a href="%{lead_url}">lead</a> a new discussion or " +
|
142
|
+
"<a href="%{join_url}">join</a> an existing one.",
|
143
|
+
:lead_url => new_discussion_path,
|
144
|
+
:join_url => discussion_search_path
|
145
|
+
%>
|
146
|
+
</p>
|
147
|
+
|
148
|
+
This isn't much better, because now you have HTML in your translations. If you
|
149
|
+
want to add a class to the link, you have to go update all the translations.
|
150
|
+
A translator could accidentally break your page (or worse, cross-site script
|
151
|
+
it).
|
152
|
+
|
153
|
+
So what do you do?
|
154
|
+
|
155
|
+
#### Wrappers
|
156
|
+
|
157
|
+
I18nliner lets you specify wrappers, so you can keep HTML out the translations,
|
158
|
+
while still just having a single string needing translation:
|
159
|
+
|
160
|
+
<p>
|
161
|
+
<%= t "You can *lead* a new discussion or **join** an existing one.",
|
162
|
+
:wrappers => [
|
163
|
+
link_to('\1', new_discussion_path),
|
164
|
+
link_to('\1', discussion_search_path)
|
165
|
+
]
|
166
|
+
%>
|
167
|
+
</p>
|
168
|
+
|
169
|
+
Default delimiters are increasing numbers of asterisks, but you can specify
|
170
|
+
any string as a delimiter by using a hash rather than an array.
|
171
|
+
|
172
|
+
#### Blocks
|
173
|
+
|
174
|
+
But wait, there's more!
|
175
|
+
|
176
|
+
Perhaps you want your templates to look like, well, templates. Try this:
|
177
|
+
|
178
|
+
<p>
|
179
|
+
<%= t do %>
|
180
|
+
Welcome to the internets, <%= user.name %>
|
181
|
+
<% end %>
|
182
|
+
</p>
|
183
|
+
|
184
|
+
Or even this:
|
185
|
+
|
186
|
+
<p>
|
187
|
+
<%= t do %>
|
188
|
+
<b>Ohai <%= user.name %>,</b>
|
189
|
+
you can <%= link_to "lead", new_discussion_path %> a new discussion or
|
190
|
+
<%= link_to "join", discussion_search_path %> an existing one.
|
191
|
+
<% end %>
|
192
|
+
</p>
|
193
|
+
|
194
|
+
In case you're curious about the man behind the curtain, I18nliner adds an ERB
|
195
|
+
pre-processor that turns the second example into something like this right
|
196
|
+
before it hits ERB:
|
197
|
+
|
198
|
+
<p>
|
199
|
+
<%= t :some_unique_key,
|
200
|
+
"*Ohai %{user_name}*, you can **lead** a new discussion or ***join*** an existing one.",
|
201
|
+
:user_name => @user.name,
|
202
|
+
:wrappers => [
|
203
|
+
'<b>\1</b>',
|
204
|
+
link_to('\1', new_discussion_path),
|
205
|
+
link_to('\1', discussion_search_path)
|
206
|
+
]
|
207
|
+
%>
|
208
|
+
</p>
|
209
|
+
|
210
|
+
In other words, it will infer wrappers from your (balanced) markup and
|
211
|
+
[`link_to` calls](INFERRED_WRAPPERS.md), and will create placeholders for any
|
212
|
+
other ERB expressions. ERB statements (e.g.
|
213
|
+
`<% if some_condition %>...`) are *not* supported inside block translations, with
|
214
|
+
the notable exception of nested translations, e.g.
|
215
|
+
|
216
|
+
<%= t do %>
|
217
|
+
Be sure to
|
218
|
+
<a href="/account/" title="<%= t do %>Account Settings<% end %>">
|
219
|
+
set up your account
|
220
|
+
</a>.
|
221
|
+
<% end %>
|
222
|
+
|
223
|
+
#### HTML Safety
|
224
|
+
|
225
|
+
I18nliner ensures translations, interpolated values, and wrappers all play
|
226
|
+
nicely (and safely) when it comes to HTML escaping. If any translation,
|
227
|
+
interpolated value, or wrapper is HTML-safe, everything else will be HTML-
|
228
|
+
escaped.
|
229
|
+
|
230
|
+
### Inline Pluralization Support
|
231
|
+
|
232
|
+
Pluralization can be tricky, but [I18n gives you some flexibility](http://guides.rubyonrails.org/i18n.html#pluralization).
|
233
|
+
I18nliner brings this inline with a default translation hash, e.g.
|
234
|
+
|
235
|
+
t({:one => "There is one light!", :other => "There are %{count} lights!"},
|
236
|
+
:count => picard.visible_lights.count)
|
237
|
+
|
238
|
+
Note that the :count interpolation value needs to be explicitly set when doing
|
239
|
+
pluralization. On the ERB side, I18nliner enhances pluralize to take a block,
|
240
|
+
so you can do this:
|
241
|
+
|
242
|
+
<%= pluralize picard.visible_lights.count do %>
|
243
|
+
<% one do %>There is one light!<% end %>
|
244
|
+
<% other do %>There are <%= count %> lights!<% end %>
|
245
|
+
<% end %>
|
246
|
+
|
247
|
+
If you just want to pluralize a single word, there's a shortcut:
|
248
|
+
|
249
|
+
t "person", :count => users.count
|
250
|
+
|
251
|
+
This is equivalent to:
|
252
|
+
|
253
|
+
t({:one => "1 person", :other => "%{count} people"},
|
254
|
+
:count => users.count)
|
255
|
+
|
256
|
+
I18nliner uses the pluralize helper to determine the default one/other values,
|
257
|
+
so if your `I18n.default_locale` is something other than English, you may need
|
258
|
+
to [add some inflections](https://gist.github.com/838188).
|
259
|
+
|
260
|
+
## Rake Tasks
|
261
|
+
|
262
|
+
### i18nliner:check
|
263
|
+
|
264
|
+
Ensures that there are no problems with your translate calls (e.g. missing
|
265
|
+
interpolation values, reusing a key for a different translation, etc.). **Go
|
266
|
+
add this to your Jenkins/Travis tasks.**
|
267
|
+
|
268
|
+
### i18nliner:dump
|
269
|
+
|
270
|
+
Does an i18nliner:check, and then extracts all default translations from your
|
271
|
+
codebase, merges them with any other ones (from rails or pre-existing .yml
|
272
|
+
files), and outputs them to `config/locales/generated/en.yml`.
|
273
|
+
|
274
|
+
### i18nliner:diff
|
275
|
+
|
276
|
+
Does an i18nliner:dump and creates a diff from a previous one (path or git
|
277
|
+
commit hash). This is useful if you only want to see what has changed since a
|
278
|
+
previous release of your app.
|
279
|
+
|
280
|
+
### i18nliner:import
|
281
|
+
|
282
|
+
Imports a translated yml file. Ensures that all placeholders and wrappers are
|
283
|
+
present.
|
284
|
+
|
285
|
+
#### .i18nignore and more
|
286
|
+
|
287
|
+
By default, the check and dump tasks will look for inline translations in any
|
288
|
+
.rb or .erb files. You can tell it to always skip certain
|
289
|
+
files/directories/patterns by creating a .i18nignore file. The syntax is the
|
290
|
+
same as [.gitignore](http://www.kernel.org/pub/software/scm/git/docs/gitignore.html),
|
291
|
+
though it supports
|
292
|
+
[a few extra things](https://github.com/jenseng/globby#compatibility-notes).
|
293
|
+
|
294
|
+
If you only want to check a particular file/directory/pattern, you can set the
|
295
|
+
environment variable `ONLY` when you run the command, e.g.
|
296
|
+
|
297
|
+
rake i18nliner:check ONLY=/app/**/user*
|
298
|
+
|
299
|
+
## Compatibility
|
300
|
+
|
301
|
+
I18nliner is backwards compatible with I18n, so you can add it to an
|
302
|
+
established (and already internationalized) Rails app. Your existing
|
303
|
+
translation calls, keys and yml files will still just work without
|
304
|
+
modification.
|
305
|
+
|
306
|
+
I18nliner requires at least Ruby 1.9.3 and Rails 3.
|
307
|
+
|
308
|
+
## What about JavaScript/Handlebars?
|
309
|
+
|
310
|
+
Coming soon. I18nliner was inspired by some [I18n extensions](https://github.com/instructure/canvas-lms/tree/master/lib/i18n_extraction)
|
311
|
+
I did in [Canvas-LMS](https://github.com/instructure/canvas-lms). While
|
312
|
+
it also has the JavaScript/Handlebars equivalents, they are tightly
|
313
|
+
coupled to Canvas-LMS and are written in Ruby. So basically we're talking
|
314
|
+
a full reimplementation in JavaScript using
|
315
|
+
[esprima](http://esprima.org/) instead of the [shameful, brittle, regexy hack](http://cdn.memegenerator.net/instances/400x/24091937.jpg)
|
316
|
+
that is js_extractor.
|
317
|
+
|
318
|
+
## License
|
319
|
+
|
320
|
+
Copyright (c) 2013 Jon Jensen, released under the MIT license
|
data/Rakefile
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'iconv'
|
2
|
+
require 'zlib'
|
3
|
+
|
4
|
+
module I18nliner
|
5
|
+
module CallHelpers
|
6
|
+
ALLOWED_PLURALIZATION_KEYS = [:zero, :one, :few, :many, :other]
|
7
|
+
REQUIRED_PLURALIZATION_KEYS = [:one, :other]
|
8
|
+
|
9
|
+
def normalize_key(key, scope, receiver)
|
10
|
+
key = key.to_s
|
11
|
+
scope.normalize_key(key)
|
12
|
+
end
|
13
|
+
|
14
|
+
def normalize_default(default, translate_options = {})
|
15
|
+
default = infer_pluralization_hash(default, translate_options)
|
16
|
+
default.strip! if default.is_a?(String)
|
17
|
+
default
|
18
|
+
end
|
19
|
+
|
20
|
+
def infer_pluralization_hash(default, translate_options)
|
21
|
+
return default unless default.is_a?(String) &&
|
22
|
+
default =~ /\A[\w\-]+\z/ &&
|
23
|
+
translate_options.include?(:count)
|
24
|
+
{:one => "1 #{default}", :other => "%{count} #{default.pluralize}"}
|
25
|
+
end
|
26
|
+
|
27
|
+
def infer_key(default, translate_options = {})
|
28
|
+
default = default[:other].to_s if default.is_a?(Hash)
|
29
|
+
keyify(normalize_default(default, translate_options))
|
30
|
+
end
|
31
|
+
|
32
|
+
def keyify_underscored(string)
|
33
|
+
Iconv.iconv('ascii//translit//ignore', 'utf-8', string).
|
34
|
+
to_s.
|
35
|
+
downcase.
|
36
|
+
gsub(/[^a-z0-9_\.]+/, '_').
|
37
|
+
gsub(/\A_|_\z/, '')[0..50]
|
38
|
+
end
|
39
|
+
|
40
|
+
def keyify_underscored_crc32(string)
|
41
|
+
checksum = Zlib.crc32(string.size.to_s + ":" + string).to_s(16)
|
42
|
+
keyify_underscored(string) + "_#{checksum}"
|
43
|
+
end
|
44
|
+
|
45
|
+
def keyify(string)
|
46
|
+
case I18nliner.inferred_key_format
|
47
|
+
when :underscored then keyify_underscored(string)
|
48
|
+
when :underscored_crc32 then keyify_underscored_crc32(string)
|
49
|
+
else string
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Possible translate signatures:
|
54
|
+
#
|
55
|
+
# key [, options]
|
56
|
+
# key, default_string [, options]
|
57
|
+
# key, default_hash, options
|
58
|
+
# default_string [, options]
|
59
|
+
# default_hash, options
|
60
|
+
def key_provided?(scope, receiver, key_or_default = nil, default_or_options = nil, maybe_options = nil, *others)
|
61
|
+
return false if key_or_default.is_a?(Hash)
|
62
|
+
return true if key_or_default.is_a?(Symbol)
|
63
|
+
return true if default_or_options.is_a?(String)
|
64
|
+
return true if maybe_options
|
65
|
+
return true if I18nliner.look_up(normalize_key(key_or_default, scope, receiver))
|
66
|
+
false
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module I18nliner
|
2
|
+
module Commands
|
3
|
+
class Check < GenericCommand
|
4
|
+
attr_reader :translations
|
5
|
+
|
6
|
+
def initialize(options)
|
7
|
+
super
|
8
|
+
@errors = []
|
9
|
+
@translations = TranslationHash.new(I18nliner.manual_translations)
|
10
|
+
end
|
11
|
+
|
12
|
+
def processors
|
13
|
+
@processors ||= I18nliner::Processors.all.map do |klass|
|
14
|
+
klass.new :only => @options[:only],
|
15
|
+
:translations => @translations,
|
16
|
+
:checker => method(:check_file)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def check_files
|
21
|
+
processors.each &:check_files
|
22
|
+
end
|
23
|
+
|
24
|
+
def check_file(file)
|
25
|
+
print green(".") if yield file
|
26
|
+
rescue SyntaxError, StandardError, ExtractionError
|
27
|
+
@errors << "#{$!}\n#{file}"
|
28
|
+
print red("F")
|
29
|
+
end
|
30
|
+
|
31
|
+
def failure
|
32
|
+
@errors.size > 0
|
33
|
+
end
|
34
|
+
|
35
|
+
def print_summary
|
36
|
+
translation_count = processors.sum(&:translation_count)
|
37
|
+
file_count = processors.sum(&:file_count)
|
38
|
+
|
39
|
+
print "\n\n"
|
40
|
+
|
41
|
+
@errors.each_with_index do |error, i|
|
42
|
+
puts "#{i+1})"
|
43
|
+
puts red(error)
|
44
|
+
print "\n"
|
45
|
+
end
|
46
|
+
|
47
|
+
print "Finished in #{Time.now - @start} seconds\n\n"
|
48
|
+
summary = "#{file_count} files, #{translation_count} strings, #{@errors.size} failures"
|
49
|
+
puts failure ? red(summary) : green(summary)
|
50
|
+
end
|
51
|
+
|
52
|
+
def run
|
53
|
+
check_files
|
54
|
+
print_summary
|
55
|
+
raise "check command encountered errors" if failure
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module I18nliner
|
2
|
+
module Commands
|
3
|
+
class GenericCommand
|
4
|
+
include BasicFormatter
|
5
|
+
|
6
|
+
def initialize(options)
|
7
|
+
@options = options
|
8
|
+
@start = Time.now
|
9
|
+
extend ColorFormatter if $stdout.tty?
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.run(options)
|
13
|
+
new(options).run
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module I18nliner
|
2
|
+
class ExtractionError < StandardError
|
3
|
+
def initialize(line, detail = nil)
|
4
|
+
@line = line
|
5
|
+
@detail = detail
|
6
|
+
end
|
7
|
+
|
8
|
+
def to_s
|
9
|
+
error = self.class.name.humanize.sub(/ error\z/, '')
|
10
|
+
error = "#{error} on line #{@line}"
|
11
|
+
@detail ?
|
12
|
+
error + " (got #{@detail.inspect})" :
|
13
|
+
error
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class InvalidSignatureError < ExtractionError; end
|
18
|
+
class MissingDefaultError < ExtractionError; end
|
19
|
+
class AmbiguousKeyError < ExtractionError; end
|
20
|
+
class InvalidPluralizationKeyError < ExtractionError; end
|
21
|
+
class MissingPluralizationKeyError < ExtractionError; end
|
22
|
+
class InvalidPluralizationDefaultError < ExtractionError; end
|
23
|
+
class MissingCountValueError < ExtractionError; end
|
24
|
+
class MissingInterpolationValueError < ExtractionError; end
|
25
|
+
class InvalidOptionsError < ExtractionError; end
|
26
|
+
class InvalidOptionKeyError < ExtractionError; end
|
27
|
+
class KeyAsScopeError < ExtractionError; end
|
28
|
+
class KeyInUseError < ExtractionError; end
|
29
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module I18nliner
|
2
|
+
module Extractors
|
3
|
+
module AbstractExtractor
|
4
|
+
def initialize(options = {})
|
5
|
+
@scope = options[:scope] || ''
|
6
|
+
@translations = TranslationHash.new(options[:translations] || {})
|
7
|
+
@total = 0
|
8
|
+
super()
|
9
|
+
end
|
10
|
+
|
11
|
+
def look_up(key)
|
12
|
+
@translations[key]
|
13
|
+
end
|
14
|
+
|
15
|
+
def add_translation(full_key, default)
|
16
|
+
@total += 1
|
17
|
+
@translations[full_key] = default
|
18
|
+
end
|
19
|
+
|
20
|
+
def total_unique
|
21
|
+
@translations.total_size
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.included(base)
|
25
|
+
base.instance_eval do
|
26
|
+
attr_reader :total
|
27
|
+
attr_accessor :translations, :scope
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module I18nliner
|
2
|
+
module Extractors
|
3
|
+
class RubyExtractor < SexpProcessor
|
4
|
+
TRANSLATE_CALLS = [:t, :translate]
|
5
|
+
attr_reader :current_line
|
6
|
+
|
7
|
+
def initialize(sexps, scope)
|
8
|
+
@sexps = sexps
|
9
|
+
@scope = scope
|
10
|
+
super()
|
11
|
+
end
|
12
|
+
|
13
|
+
def each_translation(&block)
|
14
|
+
@block = block
|
15
|
+
process(@sexps)
|
16
|
+
end
|
17
|
+
|
18
|
+
def process_call(exp)
|
19
|
+
exp.shift
|
20
|
+
receiver = process(exp.shift)
|
21
|
+
receiver = receiver.last if receiver
|
22
|
+
method = exp.shift
|
23
|
+
|
24
|
+
if extractable_call?(receiver, method)
|
25
|
+
@current_line = exp.line
|
26
|
+
|
27
|
+
# convert s-exps into literals where possible
|
28
|
+
args = process_arguments(exp)
|
29
|
+
|
30
|
+
process_translate_call(receiver, method, args)
|
31
|
+
else
|
32
|
+
# even if this isn't a translate call, its arguments might contain
|
33
|
+
# one
|
34
|
+
process exp.shift until exp.empty?
|
35
|
+
end
|
36
|
+
|
37
|
+
s
|
38
|
+
end
|
39
|
+
|
40
|
+
protected
|
41
|
+
|
42
|
+
def extractable_call?(receiver, method)
|
43
|
+
TRANSLATE_CALLS.include?(method) && (receiver.nil? || receiver == :I18n)
|
44
|
+
end
|
45
|
+
|
46
|
+
def process_translate_call(receiver, method, args)
|
47
|
+
call = TranslateCall.new(@scope, @current_line, receiver, method, args)
|
48
|
+
call.translations.each &@block
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def process_arguments(args)
|
54
|
+
values = []
|
55
|
+
while arg = args.shift
|
56
|
+
values << evaluate_expression(arg)
|
57
|
+
end
|
58
|
+
values
|
59
|
+
end
|
60
|
+
|
61
|
+
def evaluate_expression(exp)
|
62
|
+
if exp.sexp_type == :lit || exp.sexp_type == :str
|
63
|
+
exp.shift
|
64
|
+
return exp.shift
|
65
|
+
end
|
66
|
+
return string_from(exp) if string_concatenation?(exp)
|
67
|
+
return hash_from(exp) if exp.sexp_type == :hash
|
68
|
+
process(exp)
|
69
|
+
UnsupportedExpression
|
70
|
+
end
|
71
|
+
|
72
|
+
def string_concatenation?(exp)
|
73
|
+
exp.sexp_type == :call &&
|
74
|
+
exp[2] == :+ &&
|
75
|
+
exp.last &&
|
76
|
+
exp.last.sexp_type == :str
|
77
|
+
end
|
78
|
+
|
79
|
+
def string_from(exp)
|
80
|
+
exp.shift
|
81
|
+
lhs = exp.shift
|
82
|
+
exp.shift
|
83
|
+
rhs = exp.shift
|
84
|
+
if lhs.sexp_type == :str
|
85
|
+
lhs.last + rhs.last
|
86
|
+
elsif string_concatenation?(lhs)
|
87
|
+
string_from(lhs) + rhs.last
|
88
|
+
else
|
89
|
+
return UnsupportedExpression
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def hash_from(exp)
|
94
|
+
exp.shift
|
95
|
+
values = exp.map{ |e| evaluate_expression(e) }
|
96
|
+
Hash[*values]
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
class UnsupportedExpression; end
|
101
|
+
end
|
102
|
+
end
|