splendeo_translator 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +163 -0
- data/Rakefile +75 -0
- data/VERSION.yml +4 -0
- data/lib/splendeo_translator.rb +385 -0
- data/test/fixtures/app/controllers/blog_posts_controller.rb +95 -0
- data/test/fixtures/app/helpers/blog_posts_helper.rb +10 -0
- data/test/fixtures/app/models/blog_comment_mailer.rb +10 -0
- data/test/fixtures/app/models/blog_post.rb +14 -0
- data/test/fixtures/app/views/blog_comment_mailer/comment_notification.rhtml +5 -0
- data/test/fixtures/app/views/blog_posts/_footer.erb +3 -0
- data/test/fixtures/app/views/blog_posts/about.erb +6 -0
- data/test/fixtures/app/views/blog_posts/archives.erb +2 -0
- data/test/fixtures/app/views/blog_posts/missing_translation.erb +2 -0
- data/test/fixtures/app/views/blog_posts/show.erb +4 -0
- data/test/fixtures/app/views/layouts/blog_layout.erb +9 -0
- data/test/fixtures/app/views/shared/_header.erb +2 -0
- data/test/fixtures/schema.rb +11 -0
- data/test/locales/en.yml +60 -0
- data/test/locales/es.yml +16 -0
- data/test/test_helper.rb +90 -0
- data/test/translator_test.rb +359 -0
- metadata +94 -0
data/README.rdoc
ADDED
@@ -0,0 +1,163 @@
|
|
1
|
+
= SplendeoSplendeoTranslator - i18n tooling for Rails
|
2
|
+
|
3
|
+
SplendeoTranslator makes using the internationalization (i18n) facility introduced in Rails 2.2 simpler through:
|
4
|
+
* keeping your code DRY using simple conventions
|
5
|
+
* make testing easier to catch missing keys
|
6
|
+
* supplying a graceful "fallback" mode when a translation is missing in a user's locale
|
7
|
+
|
8
|
+
== The Problem
|
9
|
+
|
10
|
+
The (very!) helpful I18n[http://api.rubyonrails.org/classes/I18n.html] library finds keys in locale bundles (and more), but doesn't know anything about Rails applications. Applications that have a lot of strings need a system of keeping them organized. SplendeoTranslator adds smarts to controllers, views, models & mailers to follow a simple convention when finding keys. Having a convention for the hierarchy of keys within locale bundles that makes it easier to code and maintain, while still using the capabilities of the underlying I18n library. (Note: SplendeoTranslator does not depend on how the actual YAML/Ruby files are stored in the filesystem.)
|
11
|
+
|
12
|
+
Quick example - if you follow the key convention of structuring your locale bundle like:
|
13
|
+
blog_posts: # controller
|
14
|
+
show: # action
|
15
|
+
title: "My Awesome Blog Post"
|
16
|
+
byline: "Written by {{author}}"
|
17
|
+
|
18
|
+
Then when writing the <tt>BlogPostsController.show</tt> action you can just use <tt>t('title')</tt> to fetch the string (equivalent to <tt>I18n.translate('blog_posts.show.title')</tt>). Similarly, in the <tt>show.erb</tt> template you can get use <tt>t('byline', :author => "Mike")</tt>. This extends to models and mailers as well. As they say, "Look at all the things I'm not doing!"
|
19
|
+
|
20
|
+
== Installation
|
21
|
+
|
22
|
+
To install this plugin into your Rails app (2.2 or 2.3+):
|
23
|
+
|
24
|
+
./script/plugin install git://github.com/splendeo/SplendeoTranslator.git
|
25
|
+
|
26
|
+
To install as a gem add the following to config/environment.rb:
|
27
|
+
|
28
|
+
config.gem "SplendeoTranslator"
|
29
|
+
|
30
|
+
== RDocs
|
31
|
+
|
32
|
+
{The RDocs are online}[http://splendeo.github.com/SplendeoTranslator/rdoc/index.html] or can be generated via <tt>rake rdoc</tt> in the SplendeoTranslator plugin directory.
|
33
|
+
|
34
|
+
== Problems or Suggestions
|
35
|
+
|
36
|
+
Please {file an issue in the Github bug tracker}[http://github.com/splendeo/SplendeoTranslator/issues/] or contact me.
|
37
|
+
|
38
|
+
== Simple +translate+ Everywhere
|
39
|
+
|
40
|
+
SplendeoTranslator adds an enhanced +translate+ (or shorter +t+) method to:
|
41
|
+
* ActionController
|
42
|
+
* ActionView
|
43
|
+
* ActiveRecord
|
44
|
+
* ActionMailer
|
45
|
+
|
46
|
+
In the spirit of Rails, the convention for a hierarchy of keys borrows the same layout as the typical "views" directory. A sample Blog application is used as an example.
|
47
|
+
|
48
|
+
For controllers/views/mailers it is:
|
49
|
+
en: # locale
|
50
|
+
# the controller name
|
51
|
+
blog_posts:
|
52
|
+
# the action name
|
53
|
+
index:
|
54
|
+
key: "Hello World"
|
55
|
+
|
56
|
+
# partials w/o underscore (template "_footer.erb")
|
57
|
+
footer:
|
58
|
+
key: "My Copyright"
|
59
|
+
|
60
|
+
# "layouts" is fixed
|
61
|
+
layouts:
|
62
|
+
# the layout name (template "main.erb")
|
63
|
+
main:
|
64
|
+
key: "My App Name"
|
65
|
+
|
66
|
+
# for shared partials called like: render :template => "shared/user"
|
67
|
+
# where "shared" is the directory name
|
68
|
+
shared:
|
69
|
+
# partial name w/o underscore (template "_user.erb")
|
70
|
+
user:
|
71
|
+
key: "Foo"
|
72
|
+
|
73
|
+
# the full mailer name
|
74
|
+
blog_comment_mailer:
|
75
|
+
# the method name (does not include "deliver")
|
76
|
+
comment_notification:
|
77
|
+
subject: "New Comment"
|
78
|
+
|
79
|
+
For models it is:
|
80
|
+
en:
|
81
|
+
# The model name
|
82
|
+
blog_post:
|
83
|
+
key: "Custom validation error"
|
84
|
+
|
85
|
+
|
86
|
+
=== Key Lookup
|
87
|
+
|
88
|
+
When a key is looked up, SplendeoTranslator adds extra scoping to the lookup based on where it is called from. For:
|
89
|
+
* Controllers & views the scope includes <tt>[:controller_name, :action_name]</tt>. (For shared partials it is <tt>[:template_path, :partial_name]</tt>)
|
90
|
+
* Mailers the scope includes <tt>[:mailer_name, :method_name]</tt>
|
91
|
+
* Models the scope includes <tt>[:model_name]</tt>
|
92
|
+
|
93
|
+
But what happens if you want to share strings across a controller? Let's say you have error messages that are set in flash notices
|
94
|
+
and then are shared between actions in a controller defined in the locale bundle like:
|
95
|
+
blog_posts:
|
96
|
+
errors:
|
97
|
+
permission_denied: "Permission denied to read this blog post"
|
98
|
+
|
99
|
+
If SplendeoTranslator doesn't find the original key, it will remove a layer of scoping and try again.
|
100
|
+
So if in our Blogs controller +show+ action we want to set a <tt>flash[:error]</tt> to a permission denied message it can find the string by calling <tt>t('errors.permission_denied')</tt>.
|
101
|
+
SplendeoTranslator will first look for "blog_posts.show.errors.permission_denied", which doesn't exist. So it will then try to find
|
102
|
+
"blog_posts.errors.permission_denied" and return the correct string. This can be used to create greater levels of scoping, or to force finding
|
103
|
+
global strings (e.g. <tt>t("global.app_name")</tt>).
|
104
|
+
|
105
|
+
== Graceful Locale Fallback
|
106
|
+
|
107
|
+
Let's say you've extracted all your English strings, and even had them translated to Spanish to make your Spanish-speaking users extra happy. Then you have a brilliant idea for a new feature that needs to go live before the new pages are translated into Spanish. You still want your Spanish-speaking users to keep seeing the site in Spanish, but for these new pages to fallback to English. (While not exactly ideal, it is better than having "translation missing" messages or not externalizing strings.) To enable this fallback behavior:
|
108
|
+
|
109
|
+
# In the configuration
|
110
|
+
I18n.default_locale = :en
|
111
|
+
|
112
|
+
# Enable the fallback mode to try :es first, then :en
|
113
|
+
SplendeoTranslator.fallback(true)
|
114
|
+
|
115
|
+
# Set in the code based on user's preference, their IP address, etc.
|
116
|
+
I18n.locale = :es
|
117
|
+
|
118
|
+
# Everything else stays the same, but after SplendeoTranslator tries the normal scoping rules
|
119
|
+
# in Spanish (:es), it will apply the same rules for the default locale (:en)
|
120
|
+
t('page_title')
|
121
|
+
|
122
|
+
|
123
|
+
== Key fallback
|
124
|
+
|
125
|
+
Let's say that you don't want to have to extract your English strings. You can use them directly as strings for other languages:
|
126
|
+
|
127
|
+
# In the configuration
|
128
|
+
SplendeoTranslator.key_fallback(true)
|
129
|
+
|
130
|
+
# In your views/controllers
|
131
|
+
t('My Title in Plain English') will return 'My Title in Plain English' if no translation is found
|
132
|
+
|
133
|
+
# In your es.yml file, you will have to quote your plain-english keys:
|
134
|
+
'My Title in Plain English': "Mi Título en Español"
|
135
|
+
|
136
|
+
== Testing Help
|
137
|
+
|
138
|
+
* <tt>SplendeoTranslator.strict_mode</tt> will cause an exception to be raised for any missing translations. Enabled by default during testing to help find mistyped or accidently forgotten keys. It can be disabled by calling <tt>SplendeoTranslator.strict_mode(false)</tt> (in test_helper for example).
|
139
|
+
* <tt>assert_translated</tt> takes a block and asserts that all lookups within that block have real translations. It is a more targeted version of <tt>strict_mode</tt>. Example:
|
140
|
+
|
141
|
+
assert_translated do
|
142
|
+
# Will assert that all keys find valid translations inside the block
|
143
|
+
get :show
|
144
|
+
end
|
145
|
+
|
146
|
+
* If you're trying to avoid hard-coding strings in tests, you can still use the lookup that is added to models and controllers:
|
147
|
+
|
148
|
+
# Inside a test exercising a BlogPostController (@controller created in setup method)
|
149
|
+
get :show, :id => 123
|
150
|
+
# the byline should be in the body - uses @controller to make lookup easy (automatically knows controller name and action)
|
151
|
+
assert_match @controller.t('byline', :name => "Mike"), @response.body
|
152
|
+
|
153
|
+
* Pseudo-translation mode. Pseudo-translation wraps all extracted strings with leading and trailing text so that you can spot if you forgot any. It can be enabled by <tt>SplendeoTranslator.pseudo_translate</tt> (in an environment file or locale.rb for example). It does not change the lookup process (e.g. <tt>t('blog_title')</tt>) but will transform the returned string from "My Blog" to "[[ My Blog ]]". The text that is prepended / appended can be set by calling <tt>SplendeoTranslator.pseudo_prepend = "@@"</tt> (or +append+). <b>Pro Tip:</b> This can also be used to see how a layout will display in a localized language that is longer than the default. or example, German words tend to be significantly longer than their English equivalents. By padding all strings you can test how a layout will adapt and make changes.
|
154
|
+
|
155
|
+
* Rake task to validate that YAML files are, in fact, valid YAML. Useful when getting back translations from a 3rd party service, this can be a quick way to catch a missing quote. Run like <tt>rake i18n:validate_yml</tt> and it will check all .yml files below <tt>RAILS_ROOT/config/locales</tt>.
|
156
|
+
|
157
|
+
== Changelog
|
158
|
+
|
159
|
+
1.0.0 - 4/17/2009 - Declaring 1.0 after successfully using SplendeoTranslator in production.
|
160
|
+
|
161
|
+
Bug reports welcome. {Patches very welcome}[http://github.com/splendeo/SplendeoTranslator].
|
162
|
+
|
163
|
+
Copyright (c) 2009 {Mike Champion}[http://splendeo.org], released under the MIT license.
|
data/Rakefile
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/testtask'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gemspec|
|
7
|
+
gemspec.name = "translator"
|
8
|
+
gemspec.summary = "Rails extentions to simplify internationalization"
|
9
|
+
gemspec.email = "mike@graysky.org"
|
10
|
+
gemspec.homepage = "http://github.com/graysky/translator"
|
11
|
+
gemspec.description = "Translator makes using Rails internationalization simpler"
|
12
|
+
gemspec.authors = ["Mike Champion"]
|
13
|
+
end
|
14
|
+
rescue LoadError
|
15
|
+
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
# Use Hanna for pretty RDocs (if installed), otherwise normal rdocs
|
20
|
+
begin
|
21
|
+
require 'hanna/rdoctask'
|
22
|
+
rescue LoadError
|
23
|
+
require 'rake/rdoctask'
|
24
|
+
end
|
25
|
+
|
26
|
+
desc 'Default: run unit tests.'
|
27
|
+
task :default => :test
|
28
|
+
|
29
|
+
desc 'Test the translator plugin.'
|
30
|
+
Rake::TestTask.new(:test) do |t|
|
31
|
+
t.libs << 'lib'
|
32
|
+
t.libs << 'test'
|
33
|
+
t.pattern = 'test/**/*_test.rb'
|
34
|
+
t.verbose = true
|
35
|
+
end
|
36
|
+
|
37
|
+
desc 'Generate documentation for the translator plugin.'
|
38
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
39
|
+
rdoc.rdoc_dir = 'rdoc'
|
40
|
+
rdoc.title = 'Translator - i18n tooling for Rails'
|
41
|
+
rdoc.options << '--line-numbers' << '--inline-source' << '--webcvs=http://github.com/graysky/translator/tree/master/'
|
42
|
+
rdoc.rdoc_files.include('README.rdoc')
|
43
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
44
|
+
end
|
45
|
+
|
46
|
+
desc "Publish rdocs to Github on special gh-pages branch. Assumes local branch gh-pages"
|
47
|
+
task :publish_rdoc do
|
48
|
+
# Build the rdocs
|
49
|
+
safe_system("rake rerdoc")
|
50
|
+
move("rdoc", "rdoc-tmp")
|
51
|
+
|
52
|
+
git("co gh-pages")
|
53
|
+
# Remove existing docs
|
54
|
+
git("rm -rf --quiet rdoc")
|
55
|
+
move("rdoc-tmp", "rdoc")
|
56
|
+
# Add new ones
|
57
|
+
git("add .")
|
58
|
+
# Push the changes
|
59
|
+
git("commit -a -m 'updating rdocs'")
|
60
|
+
git("push origin HEAD")
|
61
|
+
|
62
|
+
git("co master")
|
63
|
+
#system("open coverage/index.html") if PLATFORM['darwin']
|
64
|
+
end
|
65
|
+
|
66
|
+
def git(cmd)
|
67
|
+
safe_system("git " + cmd)
|
68
|
+
end
|
69
|
+
|
70
|
+
def safe_system(cmd)
|
71
|
+
if !system(cmd)
|
72
|
+
puts "Failed: #{cmd}"
|
73
|
+
exit
|
74
|
+
end
|
75
|
+
end
|
data/VERSION.yml
ADDED
@@ -0,0 +1,385 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
require 'action_view/helpers/translation_helper'
|
3
|
+
|
4
|
+
# Extentions to make internationalization (i18n) of a Rails application simpler.
|
5
|
+
# Support the method +translate+ (or shorter +t+) in models/view/controllers/mailers.
|
6
|
+
module SplendeoTranslator
|
7
|
+
# Error for use within SplendeoTranslator
|
8
|
+
class SplendeoTranslatorError < StandardError #:nodoc:
|
9
|
+
end
|
10
|
+
|
11
|
+
# SplendeoTranslator version
|
12
|
+
VERSION = '1.0.0'
|
13
|
+
|
14
|
+
# Whether strict mode is enabled
|
15
|
+
@@strict_mode = false
|
16
|
+
|
17
|
+
# Whether to fallback from the set locale to the default locale
|
18
|
+
@@fallback_mode = false
|
19
|
+
|
20
|
+
# Whether to show the raw key if the locale doesn't provide a translation
|
21
|
+
@@key_fallback_mode = false
|
22
|
+
|
23
|
+
# Whether to pseudo-translate all fetched strings
|
24
|
+
@@pseudo_translate = false
|
25
|
+
|
26
|
+
# Pseudo-translation text to prend to fetched strings.
|
27
|
+
# Used as a visible marker. Default is "["
|
28
|
+
@@pseudo_prepend = "["
|
29
|
+
|
30
|
+
# Pseudo-translation text to append to fetched strings.
|
31
|
+
# Used as a visible marker. Default is "]"
|
32
|
+
@@pseudo_append = "]"
|
33
|
+
|
34
|
+
# An optional callback to be notified when there are missing translations in views
|
35
|
+
@@missing_translation_callback = nil
|
36
|
+
|
37
|
+
# Invokes the missing translation callback, if it is defined
|
38
|
+
def self.missing_translation_callback(exception, key, options = {}) #:nodoc:
|
39
|
+
@@missing_translation_callback.call(exception, key, options) if !@@missing_translation_callback.nil?
|
40
|
+
end
|
41
|
+
|
42
|
+
# Set an optional block that gets called when there's a missing translation within a view.
|
43
|
+
# This can be used to log missing translations in production.
|
44
|
+
#
|
45
|
+
# Block takes two required parameters:
|
46
|
+
# - exception (original I18n::MissingTranslationData that was raised for the failed translation)
|
47
|
+
# - key (key that was missing)
|
48
|
+
# - options (hash of options sent to SplendeoTranslator)
|
49
|
+
# Example:
|
50
|
+
# set_missing_translation_callback do |ex, key, options|
|
51
|
+
# logger.info("Failed to find #{key}")
|
52
|
+
# end
|
53
|
+
def self.set_missing_translation_callback(&block)
|
54
|
+
@@missing_translation_callback = block
|
55
|
+
end
|
56
|
+
|
57
|
+
# Performs lookup with a given scope. The scope should be an array of strings or symbols
|
58
|
+
# ordered from highest to lowest scoping. For example, for a given PicturesController
|
59
|
+
# with an action "show" the scope should be ['pictures', 'show'] which happens automatically.
|
60
|
+
#
|
61
|
+
# The key and options parameters follow the same rules as the I18n library (they are passed through).
|
62
|
+
#
|
63
|
+
# The search order is from most specific scope to most general (and then using a default value, if provided).
|
64
|
+
# So continuing the previous example, if the key was "title" and options included :default => 'Some Picture'
|
65
|
+
# then it would continue searching until it found a value for:
|
66
|
+
# * pictures.show.title
|
67
|
+
# * pictures.title
|
68
|
+
# * title
|
69
|
+
# * use the default value (if provided)
|
70
|
+
#
|
71
|
+
# The key itself can contain a scope. For example, if there were a set of shared error messages within the
|
72
|
+
# Pictures controller, that could be found using a key like "errors.deleted_picture". The inital search with
|
73
|
+
# narrowest scope ('pictures.show.errors.deleted_picture') will not find a value, but the subsequent search with
|
74
|
+
# broader scope ('pictures.errors.deleted_picture') will find the string.
|
75
|
+
#
|
76
|
+
def self.translate_with_scope(scope, key, options={})
|
77
|
+
scope ||= [] # guard against nil scope
|
78
|
+
|
79
|
+
# Let Rails 2.3 handle keys starting with "."
|
80
|
+
raise SplendeoTranslatorError, "Skip keys with leading dot" if key.to_s.first == "."
|
81
|
+
|
82
|
+
# Keep the original options clean
|
83
|
+
original_scope = scope.dup
|
84
|
+
scoped_options = {}.merge(options)
|
85
|
+
|
86
|
+
# Raise to know if the key was found
|
87
|
+
scoped_options[:raise] = true
|
88
|
+
|
89
|
+
# Remove any default value when searching with scope
|
90
|
+
scoped_options.delete(:default)
|
91
|
+
|
92
|
+
str = nil # the string being looked for
|
93
|
+
|
94
|
+
# Loop through each scope until a string is found.
|
95
|
+
# Example: starts with scope of [:blog_posts :show] then tries scope [:blog_posts] then
|
96
|
+
# without any automatically added scope ("[]").
|
97
|
+
while str.nil?
|
98
|
+
# Set scope to use for search
|
99
|
+
scoped_options[:scope] = scope
|
100
|
+
|
101
|
+
begin
|
102
|
+
# try to find key within scope (dup the options because I18n modifies the hash)
|
103
|
+
str = I18n.translate(key, scoped_options.dup)
|
104
|
+
rescue I18n::MissingTranslationData => exc
|
105
|
+
# did not find the string, remove a layer of scoping.
|
106
|
+
# break when there are no more layers to remove (pop returns nil)
|
107
|
+
break if scope.pop.nil?
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# return the key itself if translator is on key_fallback mode
|
112
|
+
str ||= key if SplendeoTranslator.key_fallback?
|
113
|
+
|
114
|
+
# If a string is not yet found, potentially check the default locale if in fallback mode.
|
115
|
+
if str.nil? && SplendeoTranslator.fallback? && (I18n.locale != I18n.default_locale) && options[:locale].nil?
|
116
|
+
# Recurse original request, but in the context of the default locale
|
117
|
+
str ||= SplendeoTranslator.translate_with_scope(original_scope, key, options.merge({:locale => I18n.default_locale}))
|
118
|
+
end
|
119
|
+
|
120
|
+
# If a string was still not found, fall back to trying original request (gets default behavior)
|
121
|
+
str ||= I18n.translate(key, options)
|
122
|
+
|
123
|
+
# If pseudo-translating, prepend / append marker text
|
124
|
+
if SplendeoTranslator.pseudo_translate? && !str.nil?
|
125
|
+
str = SplendeoTranslator.pseudo_prepend + str + SplendeoTranslator.pseudo_append
|
126
|
+
end
|
127
|
+
|
128
|
+
str
|
129
|
+
end
|
130
|
+
|
131
|
+
class << SplendeoTranslator
|
132
|
+
|
133
|
+
# Generic translate method that mimics <tt>I18n.translate</tt> (e.g. no automatic scoping) but includes locale fallback
|
134
|
+
# and strict mode behavior.
|
135
|
+
def translate(key, options={})
|
136
|
+
SplendeoTranslator.translate_with_scope([], key, options)
|
137
|
+
end
|
138
|
+
|
139
|
+
alias :t :translate
|
140
|
+
end
|
141
|
+
|
142
|
+
# When fallback mode is enabled if a key cannot be found in the set locale,
|
143
|
+
# it uses the default locale. So, for example, if an app is mostly localized
|
144
|
+
# to Spanish (:es), but a new page is added then Spanish users will continue
|
145
|
+
# to see mostly Spanish content but the English version (assuming the <tt>default_locale</tt> is :en)
|
146
|
+
# for the new page that has not yet been translated to Spanish.
|
147
|
+
def self.fallback(enable = true)
|
148
|
+
@@fallback_mode = enable
|
149
|
+
end
|
150
|
+
|
151
|
+
# If fallback mode is enabled
|
152
|
+
def self.fallback?
|
153
|
+
@@fallback_mode
|
154
|
+
end
|
155
|
+
|
156
|
+
# When key_fallback mode is enabled, if a key cannot be found in the set locale,
|
157
|
+
# it will return the key
|
158
|
+
def self.key_fallback(enable = true)
|
159
|
+
@@key_fallback_mode = enable
|
160
|
+
end
|
161
|
+
|
162
|
+
# If key_fallback mode is enabled
|
163
|
+
def self.key_fallback?
|
164
|
+
@@key_fallback_mode
|
165
|
+
end
|
166
|
+
|
167
|
+
# Toggle whether to true an exception on *all* +MissingTranslationData+ exceptions
|
168
|
+
# Useful during testing to ensure all keys are found.
|
169
|
+
# Passing +true+ enables strict mode, +false+ installs the default exception handler which
|
170
|
+
# does not raise on +MissingTranslationData+
|
171
|
+
def self.strict_mode(enable_strict = true)
|
172
|
+
@@strict_mode = enable_strict
|
173
|
+
|
174
|
+
if enable_strict
|
175
|
+
# Switch to using contributed exception handler
|
176
|
+
I18n.exception_handler = :strict_i18n_exception_handler
|
177
|
+
else
|
178
|
+
I18n.exception_handler = :default_exception_handler
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
# Get if it is in strict mode
|
183
|
+
def self.strict_mode?
|
184
|
+
@@strict_mode
|
185
|
+
end
|
186
|
+
|
187
|
+
# Toggle a pseudo-translation mode that will prepend / append special text
|
188
|
+
# to all fetched strings. This is useful during testing to view pages and visually
|
189
|
+
# confirm that strings have been fully extracted into locale bundles.
|
190
|
+
def self.pseudo_translate(enable = true)
|
191
|
+
@@pseudo_translate = enable
|
192
|
+
end
|
193
|
+
|
194
|
+
# If pseudo-translated is enabled
|
195
|
+
def self.pseudo_translate?
|
196
|
+
@@pseudo_translate
|
197
|
+
end
|
198
|
+
|
199
|
+
# Pseudo-translation text to prepend to fetched strings.
|
200
|
+
# Used as a visible marker. Default is "[["
|
201
|
+
def self.pseudo_prepend
|
202
|
+
@@pseudo_prepend
|
203
|
+
end
|
204
|
+
|
205
|
+
# Set the pseudo-translation text to prepend to fetched strings.
|
206
|
+
# Used as a visible marker.
|
207
|
+
def self.pseudo_prepend=(v)
|
208
|
+
@@pseudo_prepend = v
|
209
|
+
end
|
210
|
+
|
211
|
+
# Pseudo-translation text to append to fetched strings.
|
212
|
+
# Used as a visible marker. Default is "]]"
|
213
|
+
def self.pseudo_append
|
214
|
+
@@pseudo_append
|
215
|
+
end
|
216
|
+
|
217
|
+
# Set the pseudo-translation text to append to fetched strings.
|
218
|
+
# Used as a visible marker.
|
219
|
+
def self.pseudo_append=(v)
|
220
|
+
@@pseudo_append = v
|
221
|
+
end
|
222
|
+
|
223
|
+
# Additions to TestUnit to make testing i18n easier
|
224
|
+
module Assertions
|
225
|
+
|
226
|
+
# Assert that within the block there are no missing translation keys.
|
227
|
+
# This can be used in a more tailored way that the global +strict_mode+
|
228
|
+
#
|
229
|
+
# Example:
|
230
|
+
# assert_translated do
|
231
|
+
# str = "Test will fail for #{I18n.t('a_missing_key')}"
|
232
|
+
# end
|
233
|
+
#
|
234
|
+
def assert_translated(msg = nil, &block)
|
235
|
+
|
236
|
+
# Enable strict mode to force raising of MissingTranslationData
|
237
|
+
SplendeoTranslator.strict_mode(true)
|
238
|
+
|
239
|
+
msg ||= "Expected no missing translation keys"
|
240
|
+
|
241
|
+
begin
|
242
|
+
yield
|
243
|
+
# Credtit for running the assertion
|
244
|
+
assert(true, msg)
|
245
|
+
rescue I18n::MissingTranslationData => e
|
246
|
+
# Fail!
|
247
|
+
assert_block(build_message(msg, "Exception raised:\n?", e)) {false}
|
248
|
+
ensure
|
249
|
+
# uninstall strict exception handler
|
250
|
+
SplendeoTranslator.strict_mode(false)
|
251
|
+
end
|
252
|
+
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
module I18nExtensions
|
257
|
+
# Add an strict exception handler for testing that will raise all exceptions
|
258
|
+
def strict_i18n_exception_handler(exception, locale, key, options)
|
259
|
+
# Raise *all* exceptions
|
260
|
+
raise exception
|
261
|
+
end
|
262
|
+
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
module ActionView #:nodoc:
|
267
|
+
class Base
|
268
|
+
# Redefine the +translate+ method in ActionView (contributed by TranslationHelper) that is
|
269
|
+
# context-aware of what view (or partial) is being rendered.
|
270
|
+
# Initial scoping will be scoped to [:controller_name :view_name]
|
271
|
+
def translate_with_context(key, options={})
|
272
|
+
# default to an empty scope
|
273
|
+
scope = []
|
274
|
+
|
275
|
+
# Use the template for scoping if there is a templ
|
276
|
+
unless self.template.nil?
|
277
|
+
# The outer scope will typically be the controller name ("blog_posts")
|
278
|
+
# but can also be a dir of shared partials ("shared").
|
279
|
+
outer_scope = self.template.base_path
|
280
|
+
|
281
|
+
# The template will be the view being rendered ("show.erb" or "_ad.erb")
|
282
|
+
inner_scope = self.template.name
|
283
|
+
|
284
|
+
# Partials template names start with underscore, which should be removed
|
285
|
+
inner_scope.sub!(/^_/, '')
|
286
|
+
|
287
|
+
scope = [outer_scope, inner_scope]
|
288
|
+
end
|
289
|
+
|
290
|
+
# In the case of a missing translation, fall back to letting TranslationHelper
|
291
|
+
# put in span tag for a translation_missing.
|
292
|
+
begin
|
293
|
+
SplendeoTranslator.translate_with_scope(scope, key, options.merge({:raise => true}))
|
294
|
+
rescue SplendeoTranslator::SplendeoTranslatorError, I18n::MissingTranslationData => exc
|
295
|
+
# Call the original translate method
|
296
|
+
str = translate_without_context(key, options)
|
297
|
+
|
298
|
+
# View helper adds the translation missing span like:
|
299
|
+
# In strict mode, do not allow TranslationHelper to add "translation missing" span like:
|
300
|
+
# <span class="translation_missing">en, missing_string</span>
|
301
|
+
if str =~ /span class\=\"translation_missing\"/
|
302
|
+
# In strict mode, do not allow TranslationHelper to add "translation missing"
|
303
|
+
raise if SplendeoTranslator.strict_mode?
|
304
|
+
|
305
|
+
# Invoke callback if it is defined
|
306
|
+
SplendeoTranslator.missing_translation_callback(exc, key, options)
|
307
|
+
end
|
308
|
+
|
309
|
+
str
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
alias_method_chain :translate, :context
|
314
|
+
alias :t :translate
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
module ActionController #:nodoc:
|
319
|
+
class Base
|
320
|
+
|
321
|
+
# Add a +translate+ (or +t+) method to ActionController that is context-aware of what controller and action
|
322
|
+
# is being invoked. Initial scoping will be [:controller_name :action_name] when looking up keys. Example would be
|
323
|
+
# +['posts' 'show']+ for the +PostsController+ and +show+ action.
|
324
|
+
def translate_with_context(key, options={})
|
325
|
+
SplendeoTranslator.translate_with_scope([self.controller_name, self.action_name], key, options)
|
326
|
+
end
|
327
|
+
|
328
|
+
alias_method_chain :translate, :context
|
329
|
+
alias :t :translate
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
module ActiveRecord #:nodoc:
|
334
|
+
class Base
|
335
|
+
# Add a +translate+ (or +t+) method to ActiveRecord that is context-aware of what model is being invoked.
|
336
|
+
# Initial scoping of [:model_name] where model name is like 'blog_post' (singular - *not* the table name)
|
337
|
+
def translate(key, options={})
|
338
|
+
SplendeoTranslator.translate_with_scope([self.class.name.underscore], key, options)
|
339
|
+
end
|
340
|
+
|
341
|
+
alias :t :translate
|
342
|
+
|
343
|
+
# Add translate as a class method as well so that it can be used in validate statements, etc.
|
344
|
+
class << Base
|
345
|
+
|
346
|
+
def translate(key, options={}) #:nodoc:
|
347
|
+
SplendeoTranslator.translate_with_scope([self.name.underscore], key, options)
|
348
|
+
end
|
349
|
+
|
350
|
+
alias :t :translate
|
351
|
+
end
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
module ActionMailer #:nodoc:
|
356
|
+
class Base
|
357
|
+
|
358
|
+
# Add a +translate+ (or +t+) method to ActionMailer that is context-aware of what mailer and action
|
359
|
+
# is being invoked. Initial scoping of [:mailer_name :action_name] where mailer_name is like 'comment_mailer'
|
360
|
+
# and action_name is 'comment_notification' (note: no "deliver_" or "create_")
|
361
|
+
def translate(key, options={})
|
362
|
+
SplendeoTranslator.translate_with_scope([self.mailer_name, self.action_name], key, options)
|
363
|
+
end
|
364
|
+
|
365
|
+
alias :t :translate
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
module I18n
|
370
|
+
# Install the strict exception handler for testing
|
371
|
+
extend SplendeoTranslator::I18nExtensions
|
372
|
+
end
|
373
|
+
|
374
|
+
module Test # :nodoc: all
|
375
|
+
module Unit
|
376
|
+
class TestCase
|
377
|
+
include SplendeoTranslator::Assertions
|
378
|
+
end
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
# In test environment, enable strict exception handling for missing translations
|
383
|
+
if (defined? RAILS_ENV) && (RAILS_ENV == "test")
|
384
|
+
SplendeoTranslator.strict_mode(true)
|
385
|
+
end
|