require 'iconv' module Zena module Use # On load this module changes ENV['LANG'] to 'C' in order to behave consitently without # strange bugs when the locale is changed. module I18n ::ENV['LANG'] = 'C' class TranslationDict attr_reader :last_error, :node_id include Zena::Acts::Secure include RubyLess # never returns nil safe_method [:get, String] => {:class => String, :accept_nil => true} def initialize(node_id) @node_id = node_id end def get(key, use_global = true) load! get_without_loading(key, use_global) end def get_without_loading(key, use_global = true) @dict[key] || (use_global && ApplicationController.send(:_, key)) end def load!(text = nil) unless text unless dict = secure(Node) { Node.find(:first, :conditions => {:id => @node_id}) } @dict = {} error("missing 'dictionary'") return false end text = dict.prop['text'] end begin definitions = YAML::load(text) if translations = definitions['translations'] if translations.kind_of?(Hash) # ok @dict = translations true else return error("bad 'translations' content (should be a dictionary)") end else return error("missing 'translations' top-level key in dictionary") end rescue return error("invalid dictionary content #{dict.inspect}") end class << self alias get get_without_loading end true end private def error(message) @last_error = message nil end end module FormatDate # display the time with the format provided by the translation of 'long_time' def long_time(atime) format_date(atime, :format => _("long_time")) end # display the time with the format provided by the translation of 'short_time' def short_time(atime) format_date(atime, :format => _("short_time")) end # display the time with the format provided by the translation of 'full_date' def full_date(adate) format_date(adate, :format => _("full_date")) end # display the time with the format provided by the translation of 'long_date' def long_date(adate) format_date(adate, :format => _("long_date")) end # display the time with the format provided by the translation of 'short_date' def short_date(adate) format_date(adate, :format => _("short_date")) end # format a date with the given format. Translate month and day names. def tformat_date(thedate, fmt) format_date(thedate, :format => _(fmt)) end end module ControllerMethods def self.included(base) FastGettext.add_text_domain 'zena', :path => "#{Zena::ROOT}/locale" base.prepend_before_filter { FastGettext.text_domain = 'zena' } base.before_filter :set_lang, :check_lang base.after_filter :set_encoding end # Choose best language to display content. # 1. 'test.host/oo?lang=en' use 'lang', redirect without lang # 2. 'test.host/nodes/12?node[v_lang]=es # 3. 'test.host/oo' use visitor[:lang] # 4. 'test.host/es' use prefix ONLY ENABLED FOR html format (see below) # 5. 'test.host/' use session[:lang] # 6. 'test.host/oo' use visitor lang # 7. 'test.host/' use HTTP_ACCEPT_LANGUAGE # 8. 'test.host/' use default language # # 4. 'test.host/es' the redirect for this rule is called once we are sure the request is # not for document data (lang in this case can be different from what the visitor is # visiting due to caching optimization). def set_lang if params[:prefix] =~ /^\d+$/ # this has nothing to do with set_lang... # 'test.host/34' --> /en/node34.html redirect_to "/#{prefix}/#{params[:prefix]}" return false end chosen_lang = nil [ params[:lang], # Avoid redirects for static assets (cached documents). request.format == Mime::HTML ? params[:prefix] : nil, visitor.is_anon? ? session[:lang] : visitor.lang, (request.headers['HTTP_ACCEPT_LANGUAGE'] || '').split(',').sort {|a,b| (b.split(';q=')[1] || 1.0).to_f <=> (a.split(';q=')[1] || 1.0).to_f }.map {|l| l.split(';')[0].split('-')[0] }, (visitor.is_anon? ? visitor.lang : nil), # anonymous user's lang comes last ].compact.flatten.uniq.each do |l| if current_site.lang_list.include?(l) chosen_lang = l break end end set_visitor_lang(chosen_lang || current_site[:default_lang]) true end def set_visitor_lang(l) return unless current_site.lang_list.include?(l) session[:lang] = l if visitor.lang != l && !visitor.is_anon? visitor.update_attribute('lang', l) else visitor.lang = l end if File.exist?("#{Zena::ROOT}/locale/#{l}/LC_MESSAGES/zena.mo") ::I18n.locale = l else ::I18n.locale = 'en' end end # Redirect on lang change "...?lang=de" def check_lang if params[:lang] # redirects other controllers (users controller, etc) redirect_url = params redirect_url.delete(:lang) if params[:controller] == 'nodes' redirect_to redirect_url.merge(:prefix => prefix) and return false else redirect_to redirect_url and return false end end true end def set_encoding headers['Content-Type'] ||= 'text/html' if headers['Content-Type'].starts_with?('text/') and !headers['Content-Type'].include?('charset=') headers['Content-Type'] += '; charset=utf-8' end end end module ViewMethods include RubyLess safe_method [:lang_links, {:wrap => String, :join => String}] => String def self.included(base) base.send(:alias_method_chain, :will_paginate, :i18n) if base.respond_to?(:will_paginate) end include FormatDate # Enable translations for will_paginate def will_paginate_with_i18n(collection, options = {}) will_paginate_without_i18n(collection, options.merge(:prev_label => _('img_prev_page'), :next_label => _('img_next_page'))) end # translation of static text using gettext # FIXME: I do not know why this is needed in order to have <%= _('blah') %> find the translations on some servers def _(str) ApplicationController.send(:_, str) end def trans(str) ApplicationController.send(:_, str) end def load_dictionary(node_id) Zena::Use::I18n::TranslationDict.new(node_id) end # show language selector def lang_links(opts={}) if opts[:wrap] tag_in = "<#{opts[:wrap]}>" tag_out = "" else tag_in = tag_out = '' end res = [] visitor.site.lang_list.each do |l| if l == visitor.lang if opts[:wrap] res << "<#{opts[:wrap]} class='on'>#{l}" + tag_out else res << "#{l}" end else if params[:controller] == 'nodes' res << tag_in + "#{l}" else res << tag_in + link_to(l, params.merge(:lang => l)) + tag_out end end end res.join(opts[:join] || '') end end # ViewMethods module ZafuMethods include RubyLess safe_method [:trans, String] => :translate safe_method [:t, String] => :translate # Show a little [xx] next to the title if the desired language could not be found. You can # use a :text => '(lang)' option. The word 'lang' will be replaced by the real value. def r_wrong_lang(params = @params) if @blocks.empty? || @method != 'wrong_lang' text = params[:text] || %q{[#{v.lang}] } "<%= #{node}.version.lang == lang ? '' : #{::RubyLess.translate_string(self, text)} %>" else expand_if("#{node}.version.lang != lang") end end def r_iconv return parser_error("missing 'to' parameter") unless to = @params[:to] begin Iconv.iconv(to, 'utf8', 'éaïl') rescue return parser_error("invalid encoding #{to.inspect}") end data_name = get_var_name('iconv', 'data') out "<% #{data_name} = capture do %>" out expand_with out "<% end %>" out "<%= Iconv.iconv(#{to.inspect}, 'utf8', #{data_name}) %>" end def r_load if dict = @params[:dictionary] # FIXME: replace @options[:base_path] by @options[:skin_id] dict_content, absolute_url, base_path, doc = @options[:helper].send(:get_template_text, dict, @options[:base_path]) if base_path # Lazy dictionary used for literal resolution dict = TranslationDict.new(doc.id) if dict.load!(dict_content) # Save dictionary in template for dynamic uses dict_name = get_var_name('dictionary', 'dict') # This is to use in RubyLess translations and static translations in Zafu set_context_var('set_var', 'dictionary', RubyLess::TypedString.new(dict_name, :class => TranslationDict, :literal => dict)) # Lazy loading (loads file on first request) out "<% #{dict_name} = load_dictionary(#{doc.id}) %>" else return parser_error(dict.last_error) end else # not found if @params[:missing] == 'ignore' # ignore else return parser_error("dictionary #{dict.inspect} not found") unless base_path end end else return parser_error("missing 'dictionary'") end expand_with end # Resolve RubyLess 't' and 'trans' methods def translate(signature, receiver = nil) return nil unless signature.size == 2 && signature[1] <= String dict = get_context_var('set_var', 'dictionary') if dict && dict.klass <= TranslationDict { :class => String, :method => "#{dict}.get", :accept_nil => true, :pre_processor => Proc.new {|this, str| trans(str)} } else { :class => String, :method => 'trans', :accept_nil => true, :pre_processor => Proc.new {|this, str| trans(str)} } end end def trans(text, use_global = true) dictionary = get_context_var('set_var', 'dictionary') if dictionary && dictionary.klass <= TranslationDict && dict = dictionary.literal # will call ApplicationController(:_) if key is not found dict.get(text, use_global) elsif use_global helper.send(:_, text) else nil end end # Translate a string representing a list of values separated with a comma ('dog,cat,house') # to a list of strings. def translate_list(str) if trad = trans(str, false) trad.split(',').map(&:strip) else str.split(',').map(&:strip).map{|v| trans(v)} end end def r_trans # _1 ==> insert this param ==> trans(@params[:text]) return nil unless method = get_attribute_or_eval klass = method.klass return parser_error("Cannot translate a '#{klass}'.") unless klass <= String dict = get_context_var('set_var', 'dictionary') if method.literal erb_escape trans(method.literal) elsif dict && dict.klass <= TranslationDict "<%= #{dict}.get(#{method}) %>" else "<%= trans(#{method}) %>" end end alias r_t r_trans protected # Overwriten from Zafu to insert dictionary in partial if there is one def context_for_partial(cont) cleared_context, prefix = super(cont) prefix = prefix.to_s dict = get_context_var('set_var', 'dictionary') if dict && dict.klass <= TranslationDict # Lazy loading (loads file on first request) dict_name = get_var_name('dictionary', 'dict', cleared_context) set_context_var('set_var', 'dictionary', dict, cleared_context) prefix += "<% #{dict_name} = load_dictionary(#{dict.literal.node_id}) %>" return cleared_context, prefix else return cleared_context, nil end end end end # I18n end # Use end # Zena