├── VERSION ├── lib ├── translate_routes.rb ├── tasks │ └── translate_routes_tasks.rake ├── translate_routes_test_helper.rb └── route_translator.rb ├── .gitignore ├── pkg └── translate_routes-.gem ├── test ├── locales │ └── routes.yml └── translate_routes_test.rb ├── MIT-LICENSE ├── Rakefile ├── translate_routes_rails_2_3.gemspec └── README.markdown /VERSION: -------------------------------------------------------------------------------- 1 | 3.0.3 -------------------------------------------------------------------------------- /lib/translate_routes.rb: -------------------------------------------------------------------------------- 1 | require 'route_translator' 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | SampleApp/log/*.log 3 | SampleApp/tmp/**/* 4 | SampleApp/db/test.sqlite3 -------------------------------------------------------------------------------- /pkg/translate_routes-.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/did/translate_routes/master/pkg/translate_routes-.gem -------------------------------------------------------------------------------- /test/locales/routes.yml: -------------------------------------------------------------------------------- 1 | # es: 2 | # people: gente 3 | # 4 | # en: 5 | 6 | es: 7 | people: gente 8 | 9 | en: -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2007 Raul Murciano [http://raul.murciano.net], Domestika INTERNET S.L. [http://domestika.org] 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. 21 | -------------------------------------------------------------------------------- /lib/tasks/translate_routes_tasks.rake: -------------------------------------------------------------------------------- 1 | namespace :translate_routes => :environment do 2 | 3 | desc "Updates yaml translation files for the given languages" 4 | task :update_yaml, :langs, :needs => :environment do |task, args| 5 | segments = ActionController::Routing::Translator.original_static_segments 6 | 7 | if args[:langs].is_a?(String) 8 | langs = args[:langs] + ' ' + ActionController::Routing::Translator.default_locale 9 | langs.split.uniq.each do |lang| 10 | 11 | file_path = File.join(config_path, "routes_#{lang}.yml"); 12 | 13 | if File.exists?(file_path) 14 | puts "Updating #{file_path}" 15 | translations = YAML.load_file(file_path) 16 | f = File.open(file_path,'w') 17 | else 18 | puts "Creating #{file_path}" 19 | translations = {} 20 | f = File.new(file_path, 'w') 21 | end 22 | 23 | f.write "#{lang}:\n" 24 | segments.each do |s| 25 | translation = translations[lang][s] rescue '' 26 | f.write " #{s}: #{translation}\n" 27 | end 28 | f.close 29 | end 30 | 31 | else 32 | puts 'Missing parameters, usage example: rake translate_routes:update_yaml["fr de es"]' 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'rake/testtask' 3 | require 'rake/rdoctask' 4 | 5 | desc 'Default: run unit tests.' 6 | task :default => :test 7 | 8 | begin 9 | require 'jeweler' 10 | Jeweler::Tasks.new do |gem| 11 | gem.name = "translate_routes_rails_2_3" 12 | gem.summary = %Q{Translate your Rails routes in a simple manner} 13 | gem.description = %Q{Translates the Rails routes of your application into the languages defined in your locale files} 14 | gem.email = "raul@murciano.net" 15 | gem.homepage = "http://github.com/raul/translate_routes" 16 | gem.authors = ["Raul Murciano"] 17 | gem.files = Dir.glob('lib/**/*.rb') 18 | end 19 | Jeweler::GemcutterTasks.new 20 | rescue LoadError 21 | puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler" 22 | end 23 | 24 | desc 'Test the translate_routes plugin.' 25 | Rake::TestTask.new(:test) do |t| 26 | t.libs << 'lib' 27 | t.pattern = 'test/**/*_test.rb' 28 | t.verbose = true 29 | end 30 | 31 | desc 'Generate documentation for the translate_routes plugin.' 32 | Rake::RDocTask.new(:rdoc) do |rdoc| 33 | rdoc.rdoc_dir = 'rdoc' 34 | rdoc.title = 'TranslateRoutes' 35 | rdoc.options << '--line-numbers' << '--inline-source' 36 | rdoc.rdoc_files.include('README') 37 | rdoc.rdoc_files.include('lib/**/*.rb') 38 | end -------------------------------------------------------------------------------- /translate_routes_rails_2_3.gemspec: -------------------------------------------------------------------------------- 1 | # Generated by jeweler 2 | # DO NOT EDIT THIS FILE DIRECTLY 3 | # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command 4 | # -*- encoding: utf-8 -*- 5 | 6 | Gem::Specification.new do |s| 7 | s.name = %q{translate_routes_rails_2_3} 8 | s.version = "3.0.3" 9 | 10 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 11 | s.authors = ["Raul Murciano"] 12 | s.date = %q{2011-02-14} 13 | s.description = %q{Translates the Rails routes of your application into the languages defined in your locale files} 14 | s.email = %q{raul@murciano.net} 15 | s.extra_rdoc_files = [ 16 | "README.markdown" 17 | ] 18 | s.files = [ 19 | "lib/route_translator.rb", 20 | "lib/translate_routes.rb", 21 | "lib/translate_routes_test_helper.rb" 22 | ] 23 | s.homepage = %q{http://github.com/raul/translate_routes} 24 | s.rdoc_options = ["--charset=UTF-8"] 25 | s.require_paths = ["lib"] 26 | s.rubygems_version = %q{1.4.2} 27 | s.summary = %q{Translate your Rails routes in a simple manner} 28 | s.test_files = [ 29 | "test/translate_routes_test.rb" 30 | ] 31 | 32 | if s.respond_to? :specification_version then 33 | s.specification_version = 3 34 | 35 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then 36 | else 37 | end 38 | else 39 | end 40 | end 41 | 42 | -------------------------------------------------------------------------------- /lib/translate_routes_test_helper.rb: -------------------------------------------------------------------------------- 1 | # Author: Raul Murciano [http://raul.murciano.net] for Domestika [http://domestika.org] 2 | # Copyright (c) 2007, Released under the MIT license (see MIT-LICENSE) 3 | 4 | require 'rails/test_help' 5 | 6 | # Include default lang on your test requests (test requests doesn't support default_url_options): 7 | ActionController::TestCase.class_eval do 8 | unless method_defined?(:process_without_default_language) 9 | def process_with_default_language(action, parameters = nil, session = nil, flash = nil, http_method = 'GET') 10 | lang_pair = {:locale, I18n.default_locale.to_s} 11 | parameters = lang_pair.merge(parameters) rescue lang_pair 12 | process_without_default_language(action, parameters, session, flash, http_method) 13 | end 14 | 15 | alias :process_without_default_language :process 16 | alias :process :process_with_default_language 17 | end 18 | end 19 | 20 | # Add untranslated helper for named routes to integration tests 21 | ActionController::Integration::Session.class_eval do 22 | ['path', 'url'].each do |suffix| 23 | ActionDispatch::Routing::Translator.original_names.each do |old_name| 24 | new_helper_name = "#{old_name}_#{suffix}" 25 | def_new_helper = <<-DEF_NEW_HELPER 26 | def #{new_helper_name}(*args) 27 | send("#{old_name}_#{ActionDispatch::Routing::Translator.locale_suffix(I18n.locale)}_#{suffix}", *args) 28 | end 29 | DEF_NEW_HELPER 30 | eval def_new_helper 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | TranslateRoutes 2 | =============== 3 | 4 | This branch works with Rails 3.x, you can find branches for Rails [2.1.x](http://github.com/raul/translate_routes/tree/rails2.1), [2.2.x](http://github.com/raul/translate_routes/tree/rails2.2) and [2.3.x](http://github.com/raul/translate_routes/tree/rails2.3) 5 | 6 | This Rails plugin provides a simple way to translate your URLs to any number of languages, even on a fully working application. 7 | 8 | It works fine with all kind of routing definitions, including RESTful and named routes. 9 | **Your current code will remain untouched**: your current routing code, helpers and links will be translated transparently - even in your tests. 10 | (Un)installing it is a very clean and simple process, so why don't you give it a chance? ;) 11 | 12 | Installation 13 | ------------ 14 | Add translate_routes to your `Gemfile`: 15 | 16 | gem 'translate_routes' 17 | 18 | And let bundle do the rest: 19 | 20 | bundle install 21 | 22 | Quick Start 23 | ----------- 24 | 25 | 1) Let's imagine you have this `routes.rb` file: 26 | 27 | TranslateRoutesApp::Application.routes.draw do 28 | match 'contact' => 'contact#index' 29 | end 30 | 31 | You can see the available routes with the `rake routes` task: 32 | 33 | 34 | contact /contact(.:format) {:controller=>"contact", :action=>"index"} 35 | 36 | 2) Now write your translations on a standard YAML file (e.g: in `/config/i18n-routes.yml`), including all the locales and their translations pairs: 37 | 38 | en: 39 | # you can leave empty locales, for example the default one 40 | es: 41 | contact: contacto 42 | 43 | 3) Append this line in your `routes.rb` file to activate the translations specifying the path of the translations file: 44 | 45 | ActionDispatch::Routing::Translator.translate_from_file('config','i18n-routes.yml') 46 | 47 | 4) Execute `rake routes` to see the new routes: 48 | 49 | contact_es /es/contacto(.:format) {:controller=>"contact", :action=>"index"} 50 | contact_en /contact(.:format) {:controller=>"contact", :action=>"index"} 51 | 52 | 5) Include this filter in your ApplicationController: 53 | 54 | before_filter :set_locale_from_url 55 | 56 | Now your application recognizes the different routes and sets the `I18n.locale` value in your controllers, 57 | but what about the routes generation? As you can see on the previous `rake routes` execution, the 58 | `contact_es_es_path` and `contact_en_us_path` routing helpers have been generated and are 59 | available in your controllers and views. Additionally, a `contact_path` helper has been generated, which 60 | generates the routes according to the current request's locale. This way your link 61 | 62 | This means that if you use named routes **you don't need to modify your application links** because the routing helpers are automatically adapted to the current locale. 63 | 64 | 6) Hey, but what about my tests? 65 | 66 | Of course, your functional and integration testing involves some requests. 67 | The plugin includes some code to add a default locale parameter so they can remain untouched. 68 | Append it to your `test_helper` and it will be applied. 69 | 70 | Documentation 71 | ------------- 72 | You can find additional information in [the translate_routes' wiki](http://wiki.github.com/raul/translate_routes). 73 | 74 | Questions, suggestions, bug reports... 75 | -------------------------------------- 76 | Feedback, questions and comments will be always welcome at raul@murciano.net 77 | 78 | Credits 79 | ------- 80 | * Main development: 81 | * Raul Murciano - code 82 | * Domestika INTERNET S.L - incredible support, really nice people to work with! 83 | 84 | * Contributors: 85 | * [Aitor Garay-Romero](http://github.com/aitorgr) 86 | * [Christian Hølmer](http://github.com/hoelmer) 87 | * Nico Ritsche 88 | * [Cedric Darricau](http://github.com/devsigner) 89 | * [Olivier Gonzalez](http://github.com/gonzoyumo) 90 | * [Kristian Mandrup](http://github.com/kristianmandrup) 91 | * [Pieter Visser](http://github.com/pietervisser) 92 | * [Marian Theisen](http://github.com/cice) 93 | * [Enric Lluelles](http://github.com/enriclluelles) 94 | * [Jaime Iniesta](http://github.com/jaimeiniesta) 95 | 96 | Rails routing resources 97 | ----------------------- 98 | * David Black's 'Rails Routing' ebook rocks! - 'Ruby for Rails' too, BTW. 99 | * Obie Fernandez's 'The Rails Way' - the definitive RoR reference, great work Obie! 100 | * As a part of the impressive Rails Guides set there is an [awesome document about rails routing](http://guides.rails.info/routing_outside_in.html) by Mike Gunderloy: 101 | 102 | 103 | License 104 | ------- 105 | Copyright (c) 2007 Released under the MIT license (see MIT-LICENSE) 106 | Raul Murciano 107 | Domestika INTERNET S.L. -------------------------------------------------------------------------------- /lib/route_translator.rb: -------------------------------------------------------------------------------- 1 | 2 | # This class knows nothing 3 | # about Rails.root or Rails.application.routes, and therefor is easier to 4 | # test without an Rails App. 5 | class RouteTranslator 6 | TRANSLATABLE_SEGMENT = /^(\w+)(\()?/.freeze 7 | LOCALE_PARAM_KEY = :locale.freeze 8 | ROUTE_HELPER_CONTAINER = [ 9 | ActionController::Base, 10 | ActionView::Base, 11 | ActionMailer::Base, 12 | ActionDispatch::Routing::UrlFor 13 | ].freeze 14 | 15 | # Attributes 16 | 17 | attr_accessor :dictionary 18 | 19 | def available_locales 20 | @available_locales ||= I18n.available_locales.map(&:to_s) 21 | end 22 | 23 | def available_locales= locales 24 | @available_locales = locales.map(&:to_s) 25 | end 26 | 27 | def default_locale 28 | @default_locale ||= I18n.default_locale.to_s 29 | end 30 | 31 | def default_locale= locale 32 | @default_locale = locale.to_s 33 | end 34 | 35 | def default_locale? locale 36 | default_locale == locale.to_s 37 | end 38 | 39 | 40 | class << self 41 | # Default locale suffix generator 42 | def locale_suffix locale 43 | locale.to_s.underscore 44 | end 45 | 46 | # Creates a RouteTranslator instance, using I18n dictionaries of 47 | # your app 48 | def init_with_i18n *wanted_locales 49 | new.tap do |t| 50 | t.init_i18n_dictionary *wanted_locales 51 | end 52 | end 53 | 54 | # Creates a RouteTranslator instance and evaluates given block 55 | # with an empty dictionary 56 | def init_with_yield &block 57 | new.tap do |t| 58 | t.yield_dictionary &block 59 | end 60 | end 61 | 62 | # Creates a RouteTranslator instance and reads the translations 63 | # from a specified file 64 | def init_from_file file_path 65 | new.tap do |t| 66 | t.load_dictionary_from_file file_path 67 | end 68 | end 69 | end 70 | 71 | module DictionaryManagement 72 | # Resets dictionary and yields the block wich can be used to manually fill the dictionary 73 | # with translations e.g. 74 | # route_translator = RouteTranslator.new 75 | # route_translator.yield_dictionary do |dict| 76 | # dict['en'] = { 'people' => 'people' } 77 | # dict['de'] = { 'people' => 'personen' } 78 | # end 79 | def yield_dictionary &block 80 | reset_dictionary 81 | yield @dictionary 82 | set_available_locales_from_dictionary 83 | end 84 | 85 | # Resets dictionary and loads translations from specified file 86 | # config/locales/routes.yml: 87 | # en: 88 | # people: people 89 | # de: 90 | # people: personen 91 | # routes.rb: 92 | # ... your routes ... 93 | # ActionDispatch::Routing::Translator.translate_from_file 94 | # or, to specify a custom file 95 | # ActionDispatch::Routing::Translator.translate_from_file 'config', 'locales', 'routes.yml' 96 | def load_dictionary_from_file file_path 97 | reset_dictionary 98 | add_dictionary_from_file file_path 99 | end 100 | 101 | # Add translations from another file to the dictionary. 102 | def add_dictionary_from_file file_path 103 | yaml = YAML.load_file(file_path) 104 | yaml.each_pair do |locale, translations| 105 | merge_translations locale, translations 106 | end 107 | set_available_locales_from_dictionary 108 | end 109 | 110 | # Merge translations for a specified locale into the dictionary 111 | def merge_translations locale, translations 112 | locale = locale.to_s 113 | if translations.blank? 114 | @dictionary[locale] ||= {} 115 | return 116 | end 117 | @dictionary[locale] = (@dictionary[locale] || {}).merge(translations) 118 | end 119 | 120 | # Init dictionary to use I18n to translate route parts. Creates 121 | # a hash with a block for each locale to lookup keys in I18n dynamically. 122 | def init_i18n_dictionary *wanted_locales 123 | wanted_locales = available_locales if wanted_locales.blank? 124 | reset_dictionary 125 | wanted_locales.each do |locale| 126 | @dictionary[locale] = Hash.new do |hsh, key| 127 | hsh[key] = I18n.translate key, :locale => locale #DISCUSS: caching or no caching (store key and translation in dictionary?) 128 | end 129 | end 130 | @available_locales = @dictionary.keys.map &:to_s 131 | end 132 | 133 | private 134 | def set_available_locales_from_dictionary 135 | @available_locales = @dictionary.keys.map &:to_s 136 | end 137 | 138 | # Resets dictionary 139 | def reset_dictionary 140 | @dictionary = { default_locale => {}} 141 | end 142 | end 143 | include DictionaryManagement 144 | 145 | module Translator 146 | # Translate a specific RouteSet, usually Rails.application.routes, but can 147 | # be a RouteSet of a gem, plugin/engine etc. 148 | def translate route_set 149 | Rails.logger.info "Translating routes (default locale: #{default_locale})" if defined?(Rails) && defined?(Rails.logger) 150 | 151 | # save original routes and clear route set 152 | original_routes = route_set.routes.dup # Array [routeA, routeB, ...] 153 | 154 | original_named_routes = route_set.named_routes.routes.dup # Hash {:name => :route} 155 | 156 | reset_route_set route_set 157 | 158 | original_routes.each do |original_route| 159 | translations_for(original_route).each do |translated_route_args| 160 | route_set.add_route *translated_route_args 161 | end 162 | end 163 | 164 | original_named_routes.each_key do |route_name| 165 | route_set.named_routes.helpers.concat add_untranslated_helpers_to_controllers_and_views(route_name) 166 | end 167 | 168 | end 169 | 170 | # Add unmodified root route to route_set 171 | def add_root_route root_route, route_set 172 | root_route.conditions[:path_info] = root_route.conditions[:path_info].dup 173 | route_set.set.add_route *root_route 174 | route_set.named_routes[root_route.name] = root_route 175 | route_set.routes << root_route 176 | end 177 | 178 | # Add standard route helpers for default locale e.g. 179 | # I18n.locale = :de 180 | # people_path -> people_de_path 181 | # I18n.locale = :fr 182 | # people_path -> people_fr_path 183 | def add_untranslated_helpers_to_controllers_and_views old_name 184 | ['path', 'url'].map do |suffix| 185 | new_helper_name = "#{old_name}_#{suffix}" 186 | 187 | ROUTE_HELPER_CONTAINER.each do |helper_container| 188 | helper_container.send :define_method, new_helper_name do |*args| 189 | send "#{old_name}_#{locale_suffix(I18n.locale)}_#{suffix}", *args 190 | end 191 | end 192 | 193 | new_helper_name.to_sym 194 | end 195 | end 196 | 197 | # Generate translations for a single route for all available locales 198 | def translations_for route 199 | available_locales.map do |locale| 200 | translate_route route, locale 201 | end 202 | end 203 | 204 | # Generate translation for a single route for one locale 205 | def translate_route route, locale 206 | conditions = { :path_info => translate_path(route.path, locale) } 207 | conditions[:request_method] = route.conditions[:request_method].source.upcase if route.conditions.has_key? :request_method 208 | requirements = route.requirements.merge LOCALE_PARAM_KEY => locale 209 | defaults = route.defaults.merge LOCALE_PARAM_KEY => locale 210 | new_name = "#{route.name}_#{locale_suffix(locale)}" if route.name 211 | 212 | [route.app, conditions, requirements, defaults, new_name] 213 | end 214 | 215 | # Add prefix for all non-default locales 216 | def add_prefix? locale 217 | !default_locale?(locale) 218 | end 219 | 220 | # Translates a path and adds the locale prefix. 221 | def translate_path path, locale 222 | final_optional_segments = path.match(/(\(.+\))$/)[1] rescue nil # i.e: (.:format) 223 | path_segments = path.gsub(final_optional_segments,'').split("/") 224 | new_path = path_segments.map{ |seg| translate_path_segment(seg, locale) }.join('/') 225 | new_path = "/#{locale.downcase}#{new_path}" if add_prefix? locale 226 | new_path = '/' if new_path.blank? 227 | final_optional_segments ? new_path + final_optional_segments : new_path 228 | end 229 | 230 | # Tries to translate a single path segment. If the path segment 231 | # contains sth. like a optional format "people(.:format)", only 232 | # "people" will be translated, if there is no translation, the path 233 | # segment is blank or begins with a ":" (param key), the segment 234 | # is returned untouched 235 | def translate_path_segment segment, locale 236 | return segment if segment.blank? or segment.starts_with?(":") 237 | 238 | match = TRANSLATABLE_SEGMENT.match(segment)[1] rescue nil 239 | 240 | (translate_string(match, locale) || segment).downcase 241 | end 242 | 243 | def translate_string str, locale 244 | @dictionary[locale.to_s][str.to_s] 245 | end 246 | 247 | private 248 | def reset_route_set route_set 249 | route_set.clear! 250 | remove_all_methods_in route_set.named_routes.module 251 | end 252 | 253 | def remove_all_methods_in mod 254 | mod.instance_methods.each do |method| 255 | mod.send :remove_method, method 256 | end 257 | end 258 | end 259 | include Translator 260 | 261 | def locale_suffix locale 262 | self.class.locale_suffix locale 263 | end 264 | end 265 | 266 | # Adapter for Rails 3 Apps 267 | module ActionDispatch 268 | module Routing 269 | module Translator 270 | class << self 271 | def translate &block 272 | RouteTranslator.init_with_yield(&block).translate Rails.application.routes 273 | end 274 | 275 | def translate_from_file *file_path 276 | file_path = %w(config locales routes.yml) if file_path.blank? 277 | RouteTranslator.init_from_file(File.join(Rails.root, *file_path)).translate Rails.application.routes 278 | end 279 | 280 | def i18n *locales 281 | RouteTranslator.init_with_i18n(*locales).translate Rails.application.routes 282 | end 283 | end 284 | end 285 | end 286 | end 287 | 288 | # Add set_locale_from_url to controllers 289 | ActionController::Base.class_eval do 290 | private 291 | # called by before_filter 292 | def set_locale_from_url 293 | I18n.locale = params[RouteTranslator::LOCALE_PARAM_KEY] 294 | default_url_options.merge! RouteTranslator::LOCALE_PARAM_KEY => I18n.locale 295 | end 296 | end 297 | 298 | # Add locale_suffix to controllers, views and mailers 299 | RouteTranslator::ROUTE_HELPER_CONTAINER.each do |klass| 300 | klass.class_eval do 301 | private 302 | def locale_suffix locale 303 | RouteTranslator.locale_suffix locale 304 | end 305 | end 306 | end -------------------------------------------------------------------------------- /test/translate_routes_test.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | require 'rubygems' 3 | require 'mocha' 4 | 5 | %w(actionpack activesupport actionmailer).each{ |gem_lib| gem gem_lib, '3.0.1' } 6 | %w(active_support action_pack action_mailer action_controller action_dispatch).each{ |lib| require lib } 7 | 8 | plugin_root = File.join(File.dirname(__FILE__), '..') 9 | require "#{plugin_root}/lib/route_translator" 10 | 11 | 12 | class PeopleController < ActionController::Base; end 13 | 14 | class TranslateRoutesTest < ActionController::TestCase 15 | include ActionDispatch::Assertions::RoutingAssertions 16 | 17 | def config_default_locale_settings(locale) 18 | I18n.default_locale = locale 19 | end 20 | 21 | def translate_routes 22 | @route_translator.translate @routes 23 | @routes.finalize! 24 | @routes.named_routes.install 25 | end 26 | 27 | def setup 28 | @controller = ActionController::Base.new 29 | @view = ActionView::Base.new 30 | @routes = ActionDispatch::Routing::RouteSet.new 31 | @route_translator = RouteTranslator.new 32 | end 33 | 34 | def test_unnamed_root_route 35 | @routes.draw { root :to => 'people#index' } 36 | config_default_locale_settings 'en' 37 | @route_translator.yield_dictionary { |t| t['en'] = {}; t['es'] = {'people' => 'gente'} } 38 | translate_routes 39 | 40 | assert_routing '/', :controller => 'people', :action => 'index', :locale => 'en' 41 | assert_routing '/es', :controller => 'people', :action => 'index', :locale => 'es' 42 | end 43 | 44 | def test_unnamed_root_route_without_prefix 45 | @routes.draw { root :to => 'people#index' } 46 | config_default_locale_settings 'es' 47 | @route_translator.load_dictionary_from_file File.expand_path('locales/routes.yml', File.dirname(__FILE__)) 48 | translate_routes 49 | 50 | assert_routing '/', :controller => 'people', :action => 'index', :locale => 'es' 51 | assert_routing '/en', :controller => 'people', :action => 'index', :locale => 'en' 52 | assert_unrecognized_route '/es', :controller => 'people', :action => 'index', :locale => 'es' 53 | end 54 | 55 | def test_unnamed_untranslated_route 56 | @routes.draw { match 'foo', :to => 'people#index' } 57 | config_default_locale_settings 'en' 58 | @route_translator.yield_dictionary { |t| t['en'] = {}; t['es'] = {'people' => 'gente'} } 59 | translate_routes 60 | 61 | assert_routing '/es/foo', :controller => 'people', :action => 'index', :locale => 'es' 62 | assert_routing '/foo', :controller => 'people', :action => 'index', :locale => 'en' 63 | end 64 | 65 | def test_unnamed_translated_route_on_default_locale 66 | @routes.draw { match 'people', :to => 'people#index' } 67 | config_default_locale_settings 'es' 68 | @route_translator.yield_dictionary { |t| t['en'] = {}; t['es'] = {'people' => 'gente'} } 69 | translate_routes 70 | 71 | assert_routing '/en/people', :controller => 'people', :action => 'index', :locale => 'en' 72 | assert_routing '/gente', :controller => 'people', :action => 'index', :locale => 'es' 73 | end 74 | 75 | def test_unnamed_translated_route_on_non_default_locale 76 | @routes.draw { match 'people', :to => 'people#index' } 77 | config_default_locale_settings 'en' 78 | @route_translator.yield_dictionary { |t| t['en'] = {}; t['es'] = {'people' => 'gente'} } 79 | translate_routes 80 | 81 | assert_routing '/es/gente', :controller => 'people', :action => 'index', :locale => 'es' 82 | assert_routing '/people', :controller => 'people', :action => 'index', :locale => 'en' 83 | end 84 | 85 | def test_named_translated_route_with_prefix_must_have_locale_as_static_segment 86 | @routes.draw { match 'people', :to => 'people#index', :as => 'people' } 87 | config_default_locale_settings 'en' 88 | @route_translator.yield_dictionary { |t| t['en'] = {}; t['es'] = {'people' => 'gente'} } 89 | translate_routes 90 | 91 | # we check the string representation of the route, 92 | # if it stores locale as a dynamic segment it would be represented as: "/:locale/gente" 93 | assert_equal "/es/gente(.:format)", path_string(named_route('people_es')) 94 | end 95 | 96 | def test_named_empty_route_without_prefix 97 | @routes.draw { root :to => 'people#index', :as => 'people' } 98 | config_default_locale_settings 'es' 99 | @route_translator.yield_dictionary { |t| t['es'] = {}; t['en'] = {'people' => 'gente'}; } 100 | translate_routes 101 | 102 | assert_routing '/en', :controller => 'people', :action => 'index', :locale => 'en' 103 | assert_routing '/', :controller => 'people', :action => 'index', :locale => 'es' 104 | assert_routing '', :controller => 'people', :action => 'index', :locale => 'es' 105 | end 106 | 107 | def test_named_root_route_without_prefix 108 | @routes.draw { root :to => 'people#index' } 109 | config_default_locale_settings 'es' 110 | @route_translator.load_dictionary_from_file File.expand_path('locales/routes.yml', File.dirname(__FILE__)) 111 | translate_routes 112 | 113 | assert_routing '/', :controller => 'people', :action => 'index', :locale => 'es' 114 | assert_routing '/en', :controller => 'people', :action => 'index', :locale => 'en' 115 | assert_unrecognized_route '/es', :controller => 'people', :action => 'index', :locale => 'es' 116 | end 117 | 118 | def test_named_untranslated_route_without_prefix 119 | @routes.draw { match 'foo', :to => 'people#index', :as => 'people' } 120 | config_default_locale_settings 'es' 121 | @route_translator.yield_dictionary { |t| t['en'] = {}; t['es'] = {'people' => 'gente'} } 122 | translate_routes 123 | 124 | assert_routing '/en/foo', :controller => 'people', :action => 'index', :locale => 'en' 125 | assert_routing 'foo', :controller => 'people', :action => 'index', :locale => 'es' 126 | assert_helpers_include :people_en, :people_es, :people 127 | end 128 | 129 | def test_named_translated_route_on_default_locale_without_prefix 130 | @routes.draw { match 'people', :to => 'people#index', :as => 'people'} 131 | config_default_locale_settings 'es' 132 | @route_translator.yield_dictionary { |t| t['en'] = {}; t['es'] = {'people' => 'gente'} } 133 | translate_routes 134 | 135 | assert_routing '/en/people', :controller => 'people', :action => 'index', :locale => 'en' 136 | assert_routing 'gente', :controller => 'people', :action => 'index', :locale => 'es' 137 | assert_helpers_include :people_en, :people_es, :people 138 | end 139 | 140 | def test_named_translated_route_on_non_default_locale_without_prefix 141 | @routes.draw { match 'people', :to => 'people#index', :as => 'people'} 142 | config_default_locale_settings 'en' 143 | @route_translator.yield_dictionary { |t| t['en'] = {}; t['es'] = {'people' => 'gente'} } 144 | translate_routes 145 | 146 | assert_routing '/people', :controller => 'people', :action => 'index', :locale => 'en' 147 | assert_routing '/es/gente', :controller => 'people', :action => 'index', :locale => 'es' 148 | assert_helpers_include :people_en, :people_es, :people 149 | end 150 | 151 | def test_formatted_root_route 152 | @routes.draw{ root :to => 'people#index', :as => 'root' } 153 | @route_translator.yield_dictionary { |t| t['en'] = {}; t['es'] = {'people' => 'gente'} } 154 | assert_equal '/(.:format)', path_string(named_route('root')) 155 | translate_routes 156 | assert_equal '/(.:format)', path_string(named_route('root_en')) 157 | assert_equal '/es(.:format)', path_string(named_route('root_es')) 158 | end 159 | 160 | def test_routes_translations_are_always_downcased 161 | @routes.draw { match 'people', :to => 'people#index', :as => 'people'} 162 | config_default_locale_settings 'en' 163 | @route_translator.yield_dictionary { |t| t['en'] = {}; t['es'] = {'people' => 'Gente'} } 164 | translate_routes 165 | assert_routing '/es/gente', :controller => 'people', :action => 'index', :locale => 'es' 166 | end 167 | 168 | def test_routes_locale_prefixes_are_always_downcased 169 | @routes.draw { match 'people', :to => 'people#index', :as => 'people'} 170 | config_default_locale_settings 'en' 171 | @route_translator.yield_dictionary { |t| t['en'] = {}; t['ES'] = {'people' => 'Gente'} } 172 | translate_routes 173 | assert_routing '/es/gente', :controller => 'people', :action => 'index', :locale => 'ES' 174 | end 175 | 176 | def test_languages_load_from_file 177 | @routes.draw { match 'people', :to => 'people#index', :as => 'people'} 178 | config_default_locale_settings 'en' 179 | @route_translator.load_dictionary_from_file File.expand_path('locales/routes.yml', File.dirname(__FILE__)) 180 | translate_routes 181 | 182 | assert_routing '/people', :controller => 'people', :action => 'index', :locale => 'en' 183 | assert_routing '/es/gente', :controller => 'people', :action => 'index', :locale => 'es' 184 | assert_helpers_include :people_en, :people_es, :people 185 | end 186 | 187 | def test_languages_load_from_file_without_dictionary_for_default_locale 188 | @routes.draw { match 'people', :to => 'people#index', :as => 'people'} 189 | config_default_locale_settings 'fr' 190 | @route_translator.load_dictionary_from_file File.expand_path('locales/routes.yml', File.dirname(__FILE__)) 191 | translate_routes 192 | 193 | assert_routing '/people', :controller => 'people', :action => 'index', :locale => 'fr' 194 | assert_routing '/en/people', :controller => 'people', :action => 'index', :locale => 'en' 195 | assert_routing '/es/gente', :controller => 'people', :action => 'index', :locale => 'es' 196 | assert_helpers_include :people_fr, :people_en, :people_es, :people 197 | end 198 | 199 | def test_i18n_based_translations_setting_locales 200 | @routes.draw { match 'people', :to => 'people#index', :as => 'people'} 201 | config_default_locale_settings 'en' 202 | I18n.backend = StubbedI18nBackend 203 | @route_translator.init_i18n_dictionary 'es' 204 | translate_routes 205 | 206 | assert_routing '/es/gente', :controller => 'people', :action => 'index', :locale => 'es' 207 | assert_routing '/people', :controller => 'people', :action => 'index', :locale => 'en' 208 | assert_helpers_include :people_en, :people_es, :people 209 | end 210 | 211 | def test_i18n_based_translations_taking_i18n_available_locales 212 | @routes.draw { match 'people', :to => 'people#index', :as => 'people'} 213 | config_default_locale_settings 'en' 214 | I18n.stubs(:available_locales).at_least_once.returns StubbedI18nBackend.available_locales 215 | I18n.backend = StubbedI18nBackend 216 | @route_translator.init_i18n_dictionary 217 | translate_routes 218 | 219 | assert_routing '/fr/people', :controller => 'people', :action => 'index', :locale => 'fr' 220 | assert_routing '/es/gente', :controller => 'people', :action => 'index', :locale => 'es' 221 | assert_routing '/people', :controller => 'people', :action => 'index', :locale => 'en' 222 | assert_helpers_include :people_fr, :people_en, :people_es, :people 223 | end 224 | 225 | def test_action_controller_gets_locale_setter 226 | ActionController::Base.instance_methods.include?('set_locale_from_url') 227 | end 228 | 229 | def test_action_controller_gets_locale_suffix_helper 230 | ActionController::Base.instance_methods.include?('locale_suffix') 231 | end 232 | 233 | def test_action_view_gets_locale_suffix_helper 234 | ActionView::Base.instance_methods.include?('locale_suffix') 235 | end 236 | 237 | private 238 | 239 | # Given a route defined as a string like this: 240 | # 'ANY /es(.:format) {:controller=>"people", :action=>"index"}' 241 | # returns "/es(.:format)" 242 | def path_string(route) 243 | route.to_s.split(' ')[1] 244 | end 245 | 246 | def named_route(name) 247 | @routes.routes.select{ |r| r.name == name }.first 248 | end 249 | 250 | def assert_helpers_include(*helpers) 251 | helpers.each do |helper| 252 | ['url', 'path'].each do |suffix| 253 | [@controller, @view].each { |obj| assert_respond_to obj, "#{helper}_#{suffix}".to_sym } 254 | end 255 | end 256 | end 257 | 258 | def assert_unrecognized_route(route_path, options) 259 | assert_raise ActionController::RoutingError do 260 | assert_routing route_path, options 261 | end 262 | end 263 | 264 | class StubbedI18nBackend 265 | 266 | 267 | @@translations = { 268 | 'es' => { 'people' => 'gente'}, 269 | 'fr' => {} # empty on purpose to test behaviour on incompleteness scenarios 270 | } 271 | 272 | def self.translate(locale, key, options) 273 | @@translations[locale.to_s][key] || options[:default] 274 | rescue 275 | options[:default] 276 | end 277 | 278 | def self.available_locales 279 | @@translations.keys 280 | end 281 | 282 | end 283 | 284 | end 285 | --------------------------------------------------------------------------------