forked from francesc/rails-translate-routes
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathrails-translate-routes.rb
426 lines (361 loc) · 13.6 KB
/
rails-translate-routes.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
# coding: UTF-8
# This class knows nothing about Rails.root or Rails.application.routes,
# and therefore is easier to test without a Rails app.
class RailsTranslateRoutes
TRANSLATABLE_SEGMENT = /^([\w-]+)(\()?/.freeze
LOCALE_PARAM_KEY = :locale.freeze
ROUTE_HELPER_CONTAINER = [
ActionController::Base,
ActionView::Base,
ActionMailer::Base,
ActionDispatch::Routing::UrlFor
].freeze
# Attributes
attr_accessor :dictionary
def available_locales
@available_locales ||= I18n.available_locales.map(&:to_s)
end
def available_locales= locales
@available_locales = locales.map(&:to_s)
end
def default_locale
@default_locale ||= I18n.default_locale.to_s
end
def default_locale= locale
@default_locale = locale.to_s
end
def default_locale? locale
default_locale == locale.to_s
end
def prefix_on_default_locale
@prefix_on_default_locale ||= I18n.default_locale.to_s
end
def prefix_on_default_locale= locale
@prefix_on_default_locale = locale.to_s
end
def no_prefixes
@no_prefixes ||= false
end
def no_prefixes= no_prefixes
@no_prefixes = no_prefixes
end
# option allowing to keep untranslated routes
# *Ex:
# *resources :users
# *translated routes
# en/members
# fr/membres
# /users
def keep_untranslated_routes
@keep_untranslated_routes ||= false
end
def keep_untranslated_routes= keep_untranslated_routes
@keep_untranslated_routes = keep_untranslated_routes
end
class << self
# Default locale suffix generator
def locale_suffix locale
locale.to_s.underscore
end
# Creates a RailsTranslateRoutes instance, using I18n dictionaries of
# your app
def init_with_i18n *wanted_locales
new.tap do |t|
t.init_i18n_dictionary *wanted_locales
end
end
# Creates a RailsTranslateRoutes instance and evaluates given block
# with an empty dictionary
def init_with_yield &block
new.tap do |t|
t.yield_dictionary &block
end
end
# Creates a RailsTranslateRoutes instance and reads the translations
# from a specified file
def init_from_file file_path
new.tap do |t|
t.load_dictionary_from_file file_path
end
end
end
module DictionaryManagement
# Resets dictionary and yields the block wich can be used to manually fill the dictionary
# with translations e.g.
# route_translator = RailsTranslateRoutes.new
# route_translator.yield_dictionary do |dict|
# dict['en'] = { 'people' => 'people' }
# dict['de'] = { 'people' => 'personen' }
# end
def yield_dictionary &block
reset_dictionary
yield @dictionary
set_available_locales_from_dictionary
end
# Resets dictionary and loads translations from specified file
# config/locales/routes.yml:
# en:
# people: people
# de:
# people: personen
# routes.rb:
# ... your routes ...
# ActionDispatch::Routing::Translator.translate_from_file
# or, to specify a custom file
# ActionDispatch::Routing::Translator.translate_from_file 'config', 'locales', 'routes.yml'
def load_dictionary_from_file file_path
reset_dictionary
add_dictionary_from_file file_path
end
# Add translations from another file to the dictionary (supports a single file or an array to have translations splited in several folders)
def add_dictionary_from_file file_path
file_path.class == Array ? files = file_path : files = [ file_path ]
yaml = Hash.new
files.each do |file|
yaml.merge! YAML.load_file(File.join(Rails.root, file))
end
yaml.each_pair do |locale, translations|
merge_translations locale, translations['routes']
end
set_available_locales_from_dictionary
end
# Merge translations for a specified locale into the dictionary
def merge_translations locale, translations
locale = locale.to_s
if translations.blank?
@dictionary[locale] ||= {}
return
end
@dictionary[locale] = (@dictionary[locale] || {}).merge(translations)
end
# Init dictionary to use I18n to translate route parts. Creates
# a hash with a block for each locale to lookup keys in I18n dynamically.
def init_i18n_dictionary *wanted_locales
wanted_locales = available_locales if wanted_locales.blank?
reset_dictionary
wanted_locales.each do |locale|
@dictionary[locale] = Hash.new do |hsh, key|
hsh[key] = I18n.translate key, :locale => locale #DISCUSS: caching or no caching (store key and translation in dictionary?)
end
end
@available_locales = @dictionary.keys.map &:to_s
end
private
def set_available_locales_from_dictionary
@available_locales = @dictionary.keys.map &:to_s
end
# Resets dictionary
def reset_dictionary
@dictionary = { default_locale => {}}
end
end
include DictionaryManagement
module Translator
# Translate a specific RouteSet, usually Rails.application.routes, but can
# be a RouteSet of a gem, plugin/engine etc.
def translate route_set
Rails.logger.info "Translating routes (default locale: #{default_locale})" if defined?(Rails) && defined?(Rails.logger)
# save original routes and clear route set
original_routes = route_set.routes.dup # Array [routeA, routeB, ...]
if Rails.version >= '3.2'
original_routes.routes.delete_if{|r| r.path.spec.to_s == '/assets' }
else
original_routes.delete_if{|r| r.path == '/assets'}
end
original_named_routes = route_set.named_routes.routes.dup # Hash {:name => :route}
if Rails.version >= '3.2'
translated_routes = []
original_routes.each do |original_route|
translations_for(original_route).each do |translated_route_args|
translated_routes << translated_route_args
end
end
reset_route_set route_set
translated_routes.each do |translated_route_args|
route_set.add_route *translated_route_args
end
else
reset_route_set route_set
original_routes.each do |original_route|
translations_for(original_route).each do |translated_route_args|
route_set.add_route *translated_route_args
end
end
end
original_named_routes.each_key do |route_name|
route_set.named_routes.helpers.concat add_untranslated_helpers_to_controllers_and_views(route_name)
end
if root_route = original_named_routes[:root]
add_root_route root_route, route_set
end
end
# Add unmodified root route to route_set
def add_root_route root_route, route_set
if @prefix_on_default_locale
if Rails.version >= '3.2'
conditions = { :path_info => root_route.path.spec.to_s }
conditions[:request_method] = parse_request_methods root_route.verb if root_route.verb != //
route_set.add_route root_route.app, conditions, root_route.requirements, root_route.defaults, root_route.name
else
root_route.conditions[:path_info] = root_route.conditions[:path_info].dup
route_set.set.add_route *root_route
route_set.named_routes[root_route.name] = root_route
route_set.routes << root_route
end
end
end
# Add standard route helpers for default locale e.g.
# I18n.locale = :de
# people_path -> people_de_path
# I18n.locale = :fr
# people_path -> people_fr_path
def add_untranslated_helpers_to_controllers_and_views old_name
['path', 'url'].map do |suffix|
new_helper_name = "#{old_name}_#{suffix}"
ROUTE_HELPER_CONTAINER.each do |helper_container|
helper_container.send :define_method, new_helper_name do |*args|
send "#{old_name}_#{locale_suffix(I18n.locale)}_#{suffix}", *args
end
end
new_helper_name.to_sym
end
end
# Generate translations for a single route for all available locales
def translations_for route
translated_routes = []
available_locales.map do |locale|
translated_routes << translate_route(route, locale)
end
# add untranslated_route without url helper if we want to keep untranslated routes
translated_routes << untranslated_route(route) if @keep_untranslated_routes
translated_routes
end
# Generate translation for a single route for one locale
def translate_route route, locale
if Rails.version >= '3.2'
conditions = { :path_info => translate_path(route.path.spec.to_s, locale) }
conditions[:request_method] = parse_request_methods route.verb if route.verb != //
if route.constraints
route.constraints.each do |k,v|
conditions[k] = v unless k == :request_method
end
end
defaults = route.defaults
else
conditions = { :path_info => translate_path(route.path, locale) }
conditions[:request_method] = parse_request_methods route.conditions[:request_method] if route.conditions.has_key? :request_method
defaults = route.defaults.merge LOCALE_PARAM_KEY => locale
end
requirements = route.requirements.merge LOCALE_PARAM_KEY => locale
new_name = "#{route.name}_#{locale_suffix(locale)}" if route.name
[route.app, conditions, requirements, defaults, new_name]
end
# Re-generate untranslated routes (original routes) with name set to nil (which prevents conflict with default untranslated_urls)
def untranslated_route route
conditions = {}
if Rails.version >= '3.2'
conditions[:path_info] = route.path.spec.to_s
conditions[:request_method] = parse_request_methods route.verb if route.verb != //
conditions[:subdomain] = route.constraints[:subdomain] if route.constraints
else
conditions[:path_info] = route.path
conditions[:request_method] = parse_request_methods route.conditions[:request_method] if route.conditions.has_key? :request_method
end
requirements = route.requirements
defaults = route.defaults
[route.app, conditions, requirements, defaults]
end
# Add prefix for all non-default locales
def add_prefix? locale
if @no_prefixes
false
elsif !default_locale?(locale) || @prefix_on_default_locale
true
else
false
end
end
# Translates a path and adds the locale prefix.
def translate_path path, locale
final_optional_segments = path.match(/(\(.+\))$/)[1] rescue nil # i.e: (.:format)
path_without_optional_segments = final_optional_segments ? path.gsub(final_optional_segments,'') : path
path_segments = path_without_optional_segments.split("/")
new_path = path_segments.map{ |seg| translate_path_segment(seg, locale) }.join('/')
new_path = "/#{locale}#{new_path}" if add_prefix?(locale)
new_path = '/' if new_path.blank?
final_optional_segments ? new_path + final_optional_segments : new_path
end
# Tries to translate a single path segment. If the path segment
# contains sth. like an optional format "people(.:format)", only
# "people" will be translated, if there is no translation, the path
# segment is blank or begins with a ":" (param key), the segment
# is returned untouched
def translate_path_segment segment, locale
return segment if segment.blank? or segment.starts_with?(":")
match = TRANSLATABLE_SEGMENT.match(segment)[1] rescue nil
(translate_string(match, locale) || segment)
end
def translate_string str, locale
@dictionary[locale.to_s][str.to_s]
end
private
def reset_route_set route_set
route_set.clear!
remove_all_methods_in route_set.named_routes.module
end
def remove_all_methods_in mod
mod.instance_methods.each do |method|
mod.send :remove_method, method
end
end
# expects methods regexp to be in a format: /^GET$/ or /^GET|POST$/ and returns array ["GET", "POST"]
def parse_request_methods methods_regexp
methods_regexp.source.gsub(/\^([a-zA-Z\|]+)\$/, "\\1").split("|")
end
end
include Translator
def locale_suffix locale
self.class.locale_suffix locale
end
end
# Adapter for Rails 3 apps
module ActionDispatch
module Routing
module Translator
class << self
def translate &block
RailsTranslateRoutes.init_with_yield(&block).translate Rails.application.routes
end
def translate_from_file(file_path, options = {})
file_path = %w(config locales routes.yml) if file_path.blank?
r = RailsTranslateRoutes.init_from_file(file_path)
r.prefix_on_default_locale = true if options && options[:prefix_on_default_locale] == true
r.no_prefixes = true if options && options[:no_prefixes] == true
r.keep_untranslated_routes = true if options && options[:keep_untranslated_routes] == true
r.translate Rails.application.routes
end
def i18n *locales
RailsTranslateRoutes.init_with_i18n(*locales).translate Rails.application.routes
end
end
end
end
end
# Add set_locale_from_url to controllers
ActionController::Base.class_eval do
private
# called by before_filter
def set_locale_from_url
I18n.locale = params[RailsTranslateRoutes::LOCALE_PARAM_KEY]
default_url_options = {RailsTranslateRoutes::LOCALE_PARAM_KEY => I18n.locale}
end
end
# Add locale_suffix to controllers, views and mailers
RailsTranslateRoutes::ROUTE_HELPER_CONTAINER.each do |klass|
klass.class_eval do
private
def locale_suffix locale
RailsTranslateRoutes.locale_suffix locale
end
end
end