Commit ecd3f31242bfecf770ad76d9cb1613fc29900188
1 parent
6d904169
Exists in
master
and in
22 other branches
Replacing Ruby-GetText with fast_gettext
Besides being faster, consumming less memory, and being thread-safe, fast_gettext's approach is cleaner than Ruby-GetText's because it does not mess with the Rails internals. That's probably due to the fact that fast_gettext was designed after Rails had proper I18N support, so that's not exactly Ruby-GetText's fault. Current versions of Ruby-GetText are claimed to be thread-safe as well, but I decided to go with fast_gettext regardless. I am messing with the Rails internals myself by copying some code from Ruby-Gettext, but that code will be dropped when we upgrade to a more recent Rails version with proper I18N. Code was copied from Ruby-GetText to implement: * per-language cache * validation error messages translation During initialization, the needed .mo files installed system-wide are symlinked locally. By doing this we can take "similar" locales locally since fast_gettext does not seem to support loading of files from similar locales (e.g. loading pt_BR/LC_MESSAGES/domain.mo when pt/LC_MESSAGES/domain.mo is not available). This hopefully will fix the long-standing bug with messed up translations due to high concurrency and non-thread-safety of the version of Ruby-GetText in Debian Lenny. (ActionItem1315)
Showing
24 changed files
with
226 additions
and
80 deletions
Show diff stats
app/controllers/application.rb
... | ... | @@ -70,20 +70,15 @@ class ApplicationController < ActionController::Base |
70 | 70 | end |
71 | 71 | end |
72 | 72 | |
73 | - include GetText | |
74 | - before_init_gettext :maybe_save_locale, :default_locale | |
75 | - def maybe_save_locale | |
73 | + before_filter :set_locale | |
74 | + def set_locale | |
75 | + FastGettext.available_locales = Noosfero.available_locales | |
76 | + FastGettext.default_locale = Noosfero.default_locale | |
77 | + FastGettext.set_locale(params[:lang] || session[:lang] || Noosfero.default_locale || request.env['HTTP_ACCEPT_LANGUAGE'] || 'en') | |
76 | 78 | if params[:lang] |
77 | - cookies[:lang] = params[:lang] | |
79 | + session[:lang] = params[:lang] | |
78 | 80 | end |
79 | 81 | end |
80 | - def default_locale | |
81 | - if Noosfero.default_locale && cookies[:lang].blank? | |
82 | - cookies[:lang] = params[:lang] = Noosfero.default_locale | |
83 | - end | |
84 | - end | |
85 | - protected :maybe_save_locale, :default_locale | |
86 | - init_gettext 'noosfero' | |
87 | 82 | |
88 | 83 | include NeedsProfile |
89 | 84 | ... | ... |
app/helpers/account_helper.rb
app/helpers/application_helper.rb
... | ... | @@ -26,6 +26,10 @@ module ApplicationHelper |
26 | 26 | |
27 | 27 | include AccountHelper |
28 | 28 | |
29 | + def locale | |
30 | + FastGettext.locale | |
31 | + end | |
32 | + | |
29 | 33 | def load_web2_conf |
30 | 34 | if File.exists?( RAILS_ROOT + '/config/web2.0.yml') |
31 | 35 | YAML.load_file( RAILS_ROOT + '/config/web2.0.yml' ) |
... | ... | @@ -664,7 +668,6 @@ module ApplicationHelper |
664 | 668 | |
665 | 669 | # Should be on the forms_helper file but when its there the translation of labels doesn't work |
666 | 670 | class NoosferoFormBuilder < ActionView::Helpers::FormBuilder |
667 | - include GetText | |
668 | 671 | extend ActionView::Helpers::TagHelper |
669 | 672 | |
670 | 673 | def self.output_field(text, field_html, field_id = nil) |
... | ... | @@ -687,13 +690,7 @@ module ApplicationHelper |
687 | 690 | (field_helpers - %w(hidden_field)).each do |selector| |
688 | 691 | src = <<-END_SRC |
689 | 692 | def #{selector}(field, *args, &proc) |
690 | - column = object.class.columns_hash[field.to_s] | |
691 | - text = | |
692 | - ( column ? | |
693 | - column.human_name : | |
694 | - field.to_s.humanize | |
695 | - ) | |
696 | - | |
693 | + text = object.class.human_attribute_name(field.to_s) | |
697 | 694 | NoosferoFormBuilder::output_field(text, super) |
698 | 695 | end |
699 | 696 | END_SRC | ... | ... |
app/helpers/categories_helper.rb
app/helpers/content_viewer_helper.rb
app/helpers/countries_helper.rb
app/helpers/dates_helper.rb
app/helpers/language_helper.rb
app/helpers/profile_editor_helper.rb
app/models/task.rb
app/views/layouts/application-ng.rhtml
config/environment.rb
... | ... | @@ -10,17 +10,6 @@ RAILS_GEM_VERSION = '2.1.0' unless defined? RAILS_GEM_VERSION |
10 | 10 | # Bootstrap the Rails environment, frameworks, and default configuration |
11 | 11 | require File.join(File.dirname(__FILE__), 'boot') |
12 | 12 | |
13 | -# locally-developed modules | |
14 | -require 'locale' | |
15 | -require 'gettext/rails' | |
16 | -require 'acts_as_filesystem' | |
17 | -require 'acts_as_having_settings' | |
18 | -require 'acts_as_searchable' | |
19 | -require 'acts_as_having_boxes' | |
20 | -require 'acts_as_having_image' | |
21 | -require 'will_paginate' | |
22 | -require 'route_if' | |
23 | - | |
24 | 13 | # extra directories for controllers organization |
25 | 14 | extra_controller_dirs = %w[ |
26 | 15 | app/controllers/my_profile | ... | ... |
... | ... | @@ -0,0 +1,10 @@ |
1 | +# locally-developed modules | |
2 | +require 'acts_as_filesystem' | |
3 | +require 'acts_as_having_settings' | |
4 | +require 'acts_as_searchable' | |
5 | +require 'acts_as_having_boxes' | |
6 | +require 'acts_as_having_image' | |
7 | +require 'route_if' | |
8 | + | |
9 | +# third-party libraries | |
10 | +require 'will_paginate' | ... | ... |
... | ... | @@ -0,0 +1 @@ |
1 | +require 'noosfero/i18n' | ... | ... |
features/step_definitions/internationalization_steps.rb
1 | -def language_to_header(name) | |
1 | +def language_to_code(name) | |
2 | 2 | { |
3 | 3 | 'Brazilian Portuguese' => 'pt-br', |
4 | 4 | 'European Portuguese' => 'pt-pt', |
... | ... | @@ -17,29 +17,25 @@ def native_name(name) |
17 | 17 | }[name] || name |
18 | 18 | end |
19 | 19 | |
20 | -def language_to_code(name) | |
21 | - language_to_header(name) | |
22 | -end | |
23 | - | |
24 | 20 | Given /^Noosfero is configured to use (.+) as default$/ do |lang| |
25 | 21 | Noosfero.default_locale = language_to_code(lang) |
26 | 22 | end |
27 | 23 | |
28 | -After('@default_locale_config') do | |
24 | +After do | |
25 | + # reset everything back to normal | |
29 | 26 | Noosfero.default_locale = nil |
27 | + FastGettext.locale = 'en' | |
30 | 28 | end |
31 | 29 | |
32 | 30 | Given /^a user accessed in (.*) before$/ do |lang| |
33 | 31 | session = Webrat::Session.new(Webrat.adapter_class.new(self)) |
34 | 32 | session.extend(Webrat::Matchers) |
35 | 33 | session.visit('/') |
36 | - session.should have_selector('html[lang=en]') | |
34 | + session.should have_selector("html[lang=#{language_to_code(lang)}]") | |
37 | 35 | end |
38 | 36 | |
39 | 37 | Given /^my browser prefers (.*)$/ do |lang| |
40 | - @n ||= 0 | |
41 | - header 'Accept-Language', language_to_header(lang) | |
42 | - | |
38 | + header 'Accept-Language', language_to_code(lang) | |
43 | 39 | end |
44 | 40 | |
45 | 41 | Then /^the site should be in (.*)$/ do |lang| | ... | ... |
lib/noosfero.rb
... | ... | @@ -12,12 +12,15 @@ module Noosfero |
12 | 12 | attr_accessor :locales |
13 | 13 | attr_accessor :default_locale |
14 | 14 | def available_locales |
15 | - @available_locales ||= locales.keys | |
16 | - end | |
17 | - def each_locale | |
18 | - locales.keys.sort.each do |key| | |
19 | - yield(key, locales[key]) | |
20 | - end | |
15 | + @available_locales ||= | |
16 | + begin | |
17 | + locales_list = locales.keys | |
18 | + # move English to the beginning | |
19 | + if locales_list.include?('en') | |
20 | + locales_list = ['en'] + (locales_list - ['en']).sort | |
21 | + end | |
22 | + locales_list | |
23 | + end | |
21 | 24 | end |
22 | 25 | end |
23 | 26 | ... | ... |
... | ... | @@ -0,0 +1,183 @@ |
1 | +require 'fast_gettext' | |
2 | + | |
3 | +class Object | |
4 | + include FastGettext::Translation | |
5 | + alias :gettext :_ | |
6 | + alias :ngettext :n_ | |
7 | +end | |
8 | + | |
9 | +class ActiveRecord::Errors | |
10 | + default_error_messages.update( | |
11 | + :inclusion => N_("%{fn} is not included in the list"), | |
12 | + :exclusion => N_("%{fn} is reserved"), | |
13 | + :invalid => N_("%{fn} is invalid"), | |
14 | + :confirmation => N_("%{fn} doesn't match confirmation"), | |
15 | + :accepted => N_("%{fn} must be accepted"), | |
16 | + :empty => N_("%{fn} can't be empty"), | |
17 | + :blank => N_("%{fn} can't be blank"), | |
18 | + :too_long => N_("%{fn} is too long (maximum is %d characters)"), | |
19 | + :too_short => N_("%{fn} is too short (minimum is %d characters)"), | |
20 | + :wrong_length => N_("%{fn} is the wrong length (should be %d characters)"), | |
21 | + :taken => N_("%{fn} has already been taken"), | |
22 | + :not_a_number => N_("%{fn} is not a number") | |
23 | + ) | |
24 | + | |
25 | + def localize_error_messages | |
26 | + errors = {} | |
27 | + each do |attr,msg| | |
28 | + next if msg.nil? | |
29 | + errors[attr] ||= [] | |
30 | + errors[attr] << _(msg).sub('%{fn}', @base.class.human_attribute_name(attr)) | |
31 | + end | |
32 | + errors | |
33 | + end | |
34 | + def on_with_gettext(attribute) | |
35 | + errors = localize_error_messages[attribute.to_s] | |
36 | + return nil if errors.nil? | |
37 | + errors.size == 1 ? errors.first : errors | |
38 | + end | |
39 | + alias_method_chain :on, :gettext | |
40 | + | |
41 | + def full_messages_with_gettext | |
42 | + full_messages = [] | |
43 | + errors = localize_error_messages | |
44 | + errors.each_key do |attr| | |
45 | + errors[attr].each do |msg| | |
46 | + next if msg.nil? | |
47 | + full_messages << msg | |
48 | + end | |
49 | + end | |
50 | + full_messages | |
51 | + end | |
52 | + alias_method_chain :full_messages, :gettext | |
53 | +end | |
54 | + | |
55 | + | |
56 | +module ActionView::Helpers::ActiveRecordHelper | |
57 | + module L10n | |
58 | + @error_message_title = Nn_("%{num} error prohibited this %{record} from being saved", "%{num} errors prohibited this %{record} from being saved").flatten | |
59 | + @error_message_explanation = Nn_("There was a problem with the following field:", "There were problems with the following fields:").flatten | |
60 | + module_function | |
61 | + def error_messages_for(instance, objects, object_names, count, options) | |
62 | + record = _(options[:object_name] || object_names[0].to_s) | |
63 | + | |
64 | + html = {} | |
65 | + [:id, :class].each do |key| | |
66 | + if options.include?(key) | |
67 | + value = options[key] | |
68 | + html[key] = value unless value.blank? | |
69 | + else | |
70 | + html[key] = 'errorExplanation' | |
71 | + end | |
72 | + end | |
73 | + | |
74 | + if options[:message_title] | |
75 | + header_message = instance.error_message(options[:message_title], count) % {:num => count, :record => record} | |
76 | + else | |
77 | + header_message = ((count == 1) ? _(@error_message_title[0]) : _(@error_message_title[1])) % {:num => count, :record => record} | |
78 | + end | |
79 | + if options[:message_explanation] | |
80 | + message_explanation = instance.error_message(options[:message_explanation], count) % {:num => count} | |
81 | + else | |
82 | + message_explanation = (count == 1 ? _(@error_message_explanation[0]) : _(@error_message_explanation[1])) % {:num => count} | |
83 | + end | |
84 | + | |
85 | + error_messages = objects.map {|object| object.errors.full_messages.map {|msg| instance.content_tag(:li, msg) } } | |
86 | + | |
87 | + instance.content_tag( | |
88 | + :div, | |
89 | + instance.content_tag(options[:header_tag] || :h2, header_message) << | |
90 | + instance.content_tag(:p, message_explanation) << | |
91 | + instance.content_tag(:ul, error_messages), | |
92 | + html | |
93 | + ) | |
94 | + end | |
95 | + end | |
96 | + | |
97 | + alias error_messages_for_without_localize error_messages_for #:nodoc: | |
98 | + | |
99 | + # error_messages_for overrides original method with localization. | |
100 | + # And also it extends to be able to replace the title/explanation of the header of the error dialog. (Since 1.90) | |
101 | + # If you want to override these messages in the whole application, | |
102 | + # use ActionView::Helpers::ActiveRecordHelper::L10n.set_error_message_(title|explanation) instead. | |
103 | + # * :message_title - the title of message. Use Nn_() to path the strings for singular/plural. | |
104 | + # e.g. Nn_("%{num} error prohibited this %{record} from being saved", | |
105 | + # "%{num} errors prohibited this %{record} from being saved") | |
106 | + # * :message_explanation - the explanation of message | |
107 | + # e.g. Nn_("There was a problem with the following field:", | |
108 | + # "There were %{num} problems with the following fields:") | |
109 | + def error_messages_for(*params) | |
110 | + options = params.last.is_a?(Hash) ? params.pop.symbolize_keys : {} | |
111 | + objects = params.collect {|object_name| instance_variable_get("@#{object_name}") }.compact | |
112 | + object_names = params.dup | |
113 | + count = objects.inject(0) {|sum, object| sum + object.errors.count } | |
114 | + if count.zero? | |
115 | + '' | |
116 | + else | |
117 | + L10n.error_messages_for(self, objects, object_names, count, options) | |
118 | + end | |
119 | + end | |
120 | + | |
121 | +end | |
122 | + | |
123 | +module ActionController::Caching::Fragments | |
124 | + def fragment_cache_key_with_fast_gettext(name) | |
125 | + ret = fragment_cache_key_without_fast_gettext(name) | |
126 | + if ret.is_a? String | |
127 | + ret.gsub(/:/, ".") << "_#{FastGettext.locale}" | |
128 | + else | |
129 | + ret | |
130 | + end | |
131 | + end | |
132 | + alias_method_chain :fragment_cache_key, :fast_gettext | |
133 | + | |
134 | + def expire_fragment_with_fast_gettext(name, options = nil) | |
135 | + return unless perform_caching | |
136 | + | |
137 | + key = fragment_cache_key_without_fast_gettext(name) | |
138 | + if key.is_a?(Regexp) | |
139 | + self.class.benchmark "Expired fragments matching: #{key.source}" do | |
140 | + fragment_cache_store.delete_matched(key, options) | |
141 | + end | |
142 | + else | |
143 | + key = key.gsub(/:/, ".") | |
144 | + self.class.benchmark "Expired fragment: #{key}, lang = #{FastGettext.available_locales.inspect}" do | |
145 | + if FastGettext.available_locales | |
146 | + FastGettext.available_locales.each do |lang| | |
147 | + fragment_cache_store.delete("#{key}_#{lang}", options) | |
148 | + end | |
149 | + end | |
150 | + end | |
151 | + end | |
152 | + end | |
153 | + alias_method_chain :expire_fragment, :fast_gettext | |
154 | +end | |
155 | + | |
156 | +FileUtils.mkdir_p(Rails.root + '/locale') | |
157 | +Dir.glob(Rails.root + '/locale/*').each do |dir| | |
158 | + lang = File.basename(dir) | |
159 | + FileUtils.mkdir_p("#{Rails.root}/locale/#{lang}/LC_MESSAGES") | |
160 | + ['iso_3166', 'rails'].each do |domain| | |
161 | + target = "#{Rails.root}/locale/#{lang}/LC_MESSAGES/#{domain}.mo" | |
162 | + if !File.exists?(target) | |
163 | + orig = "/usr/share/locale/#{lang}/LC_MESSAGES/#{domain}.mo" | |
164 | + if File.exists?(orig) | |
165 | + File.symlink(orig, target) | |
166 | + else | |
167 | + alternatives = Dir.glob("/usr/share/locale/#{lang}_*/LC_MESSAGES/#{domain}.mo") | |
168 | + unless alternatives.empty? | |
169 | + File.symlink(alternatives.first, target) | |
170 | + end | |
171 | + end | |
172 | + end | |
173 | + end | |
174 | +end | |
175 | + | |
176 | +repos = [ | |
177 | + FastGettext::TranslationRepository.build('noosfero', :type => 'mo', :path => Rails.root + '/locale'), | |
178 | + FastGettext::TranslationRepository.build('iso_3166', :type => 'mo', :path => Rails.root + '/locale'), | |
179 | + FastGettext::TranslationRepository.build('rails', :type => 'mo', :path => Rails.root + '/locale'), | |
180 | +] | |
181 | + | |
182 | +FastGettext.add_text_domain 'noosferofull', :type => :chain, :chain => repos | |
183 | +FastGettext.default_text_domain = 'noosferofull' | ... | ... |
lib/tasks/populate.rake
lib/unifreire_terminology.rb
lib/zen3_terminology.rb
test/unit/communities_block_test.rb
test/unit/enterprises_block_test.rb
test/unit/friends_block_test.rb
test/unit/language_helper_test.rb
... | ... | @@ -4,14 +4,9 @@ class LanguageHelperTest < Test::Unit::TestCase |
4 | 4 | |
5 | 5 | include LanguageHelper |
6 | 6 | |
7 | - | |
8 | 7 | should 'return current language' do |
9 | - locale = mock | |
10 | - locale.stubs(:to_s).returns('pt_BR') | |
11 | - locale.stubs(:language).returns('pt') | |
12 | - stubs(:locale).returns(locale) | |
13 | - | |
14 | - assert_equal 'pt', self.language | |
8 | + expects(:locale).returns('pt') | |
9 | + assert_equal 'pt', language | |
15 | 10 | end |
16 | 11 | |
17 | 12 | should 'remove country code for TinyMCE' do | ... | ... |