├── generators ├── db_backend.rb └── templates │ └── db_backend_migration.rb ├── test ├── data │ ├── locale │ │ ├── root.yml │ │ ├── all.yml │ │ ├── de-DE.yml │ │ ├── en-US.yml │ │ ├── fi-FI │ │ │ └── module.yml │ │ └── en-US │ │ │ └── module.yml │ ├── no_globalize_schema.rb │ ├── post.rb │ └── schema.rb ├── test_helper.rb ├── i18n │ └── missing_translations_test.rb ├── translation_test.rb ├── load_path_test.rb ├── model │ └── active_record │ │ ├── sti_translated_test.rb │ │ ├── migration_test.rb │ │ └── translated_test.rb ├── backends │ ├── pluralizing_test.rb │ ├── static_test.rb │ └── chained_test.rb └── locale │ ├── language_tag_test.rb │ └── fallbacks_test.rb ├── .gitignore ├── init.rb ├── lib ├── globalize.rb ├── locale │ └── root.yml ├── globalize │ ├── i18n │ │ ├── missing_translations_raise_handler.rb │ │ └── missing_translations_log_handler.rb │ ├── translation.rb │ ├── backend │ │ ├── pluralizing.rb │ │ ├── static.rb │ │ └── chain.rb │ ├── model │ │ ├── active_record.rb │ │ └── active_record │ │ │ ├── adapter.rb │ │ │ └── translated.rb │ ├── locale │ │ ├── fallbacks.rb │ │ └── language_tag.rb │ └── load_path.rb └── rails_edge_load_path_patch.rb ├── rails └── init.rb ├── LICENSE ├── notes.textile ├── globalize2.gemspec └── README.textile /generators/db_backend.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/data/locale/root.yml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | doc 2 | spec/spec/db/* 3 | vendor 4 | NOTES -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__)+'/rails/init' -------------------------------------------------------------------------------- /lib/globalize.rb: -------------------------------------------------------------------------------- 1 | 2 | module Globalize 3 | end 4 | -------------------------------------------------------------------------------- /lib/locale/root.yml: -------------------------------------------------------------------------------- 1 | root: 2 | bidi: 3 | direction: left-to-right -------------------------------------------------------------------------------- /test/data/locale/all.yml: -------------------------------------------------------------------------------- 1 | en-US: 2 | from-all-file: From the "all" file. 3 | -------------------------------------------------------------------------------- /test/data/locale/de-DE.yml: -------------------------------------------------------------------------------- 1 | de-DE: 2 | from-locale-file: Aus der Locale Datei. -------------------------------------------------------------------------------- /test/data/locale/en-US.yml: -------------------------------------------------------------------------------- 1 | en-US: 2 | from-locale-file: From the locale file. 3 | -------------------------------------------------------------------------------- /test/data/locale/fi-FI/module.yml: -------------------------------------------------------------------------------- 1 | fi-FI: 2 | from-locale-dir: Locale hakemistosta. -------------------------------------------------------------------------------- /test/data/locale/en-US/module.yml: -------------------------------------------------------------------------------- 1 | en-US: 2 | from-locale-dir: From the locale directory. 3 | -------------------------------------------------------------------------------- /test/data/no_globalize_schema.rb: -------------------------------------------------------------------------------- 1 | # This schema creates tables without columns for the translated fields 2 | ActiveRecord::Schema.define do 3 | create_table :blogs, :force => true do |t| 4 | t.string :name 5 | end 6 | 7 | create_table :posts, :force => true do |t| 8 | t.references :blog 9 | end 10 | end 11 | 12 | -------------------------------------------------------------------------------- /rails/init.rb: -------------------------------------------------------------------------------- 1 | require 'rails_edge_load_path_patch.rb' unless I18n.respond_to?(:load_path) 2 | 3 | ActiveRecord::Base.send :include, Globalize::Model::ActiveRecord::Translated 4 | 5 | I18n.backend = Globalize::Backend::Static.new 6 | 7 | I18n.load_path = Globalize::LoadPath.new I18n.load_path 8 | I18n.load_path << "#{File.dirname(__FILE__)}/lib/locale" 9 | I18n.load_path << "#{RAILS_ROOT}/lib/locale" -------------------------------------------------------------------------------- /test/data/post.rb: -------------------------------------------------------------------------------- 1 | class Post < ActiveRecord::Base 2 | translates :subject, :content 3 | validates_presence_of :subject 4 | end 5 | 6 | class Blog < ActiveRecord::Base 7 | has_many :posts, :order => 'id ASC' 8 | end 9 | 10 | class Parent < ActiveRecord::Base 11 | translates :content 12 | end 13 | 14 | class Child < Parent 15 | end 16 | 17 | class Comment < ActiveRecord::Base 18 | validates_presence_of :content 19 | belongs_to :post 20 | end 21 | 22 | class TranslatedComment < Comment 23 | translates :content 24 | end -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'test/unit' 3 | require 'active_support' 4 | require 'active_support/test_case' 5 | require 'mocha' 6 | 7 | $LOAD_PATH << File.expand_path( File.dirname(__FILE__) + '/../lib' ) 8 | 9 | class ActiveSupport::TestCase 10 | def reset_db!( schema_path ) 11 | ::ActiveRecord::Migration.verbose = false # Quiet down the migration engine 12 | ::ActiveRecord::Base.establish_connection({ 13 | :adapter => 'sqlite3', 14 | :dbfile => ':memory:' 15 | }) 16 | ::ActiveRecord::Base.silence do 17 | load schema_path 18 | end 19 | end 20 | 21 | def assert_member(item, arr) 22 | assert_block "Item #{item} is not in array #{arr}" do 23 | arr.member? item 24 | end 25 | end 26 | end -------------------------------------------------------------------------------- /generators/templates/db_backend_migration.rb: -------------------------------------------------------------------------------- 1 | class ActsAsTaggableMigration < ActiveRecord::Migration 2 | def self.up 3 | create_table :globalize_translations do |t| 4 | t.string :locale, :null => false 5 | t.string :key, :null => false 6 | t.string :translation 7 | t.timestamps 8 | end 9 | 10 | # TODO: FINISH DOING MIGRATION -- stopped in the middle 11 | 12 | create_table :globalize_translations_map do |t| 13 | t.string :key, :null => false 14 | t.integer :translation_id, :null => false 15 | end 16 | 17 | add_index :taggings, :tag_id 18 | add_index :taggings, [:taggable_id, :taggable_type] 19 | end 20 | 21 | def self.down 22 | drop_table :globalize_translations 23 | drop_table :tags 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/globalize/i18n/missing_translations_raise_handler.rb: -------------------------------------------------------------------------------- 1 | # A simple exception handler that behaves like the default exception handler 2 | # but also raises on missing translations. 3 | # 4 | # Useful for identifying missing translations during testing. 5 | # 6 | # E.g. 7 | # 8 | # require 'globalize/i18n/missing_translations_raise_handler 9 | # I18n.exception_handler = :missing_translations_raise_handler 10 | module I18n 11 | class << self 12 | def missing_translations_raise_handler(exception, locale, key, options) 13 | raise exception 14 | end 15 | end 16 | 17 | # self.exception_handler = :missing_translations_raise_handler 18 | end 19 | 20 | I18n.exception_handler = :missing_translations_raise_handler 21 | 22 | ActionView::Helpers::TranslationHelper.module_eval do 23 | def translate(key, options = {}) 24 | I18n.translate(key, options) 25 | end 26 | alias :t :translate 27 | end 28 | -------------------------------------------------------------------------------- /lib/globalize/translation.rb: -------------------------------------------------------------------------------- 1 | module Globalize 2 | # Translations are simple value objects that carry some context information 3 | # alongside the actual translation string. 4 | 5 | class Translation < String 6 | class Attribute < Translation 7 | attr_accessor :requested_locale, :locale, :key 8 | end 9 | 10 | class Static < Translation 11 | attr_accessor :requested_locale, :locale, :key, :options, :plural_key, :original 12 | 13 | def initialize(string, meta = nil) 14 | self.original = string 15 | super 16 | end 17 | end 18 | 19 | def initialize(string, meta = nil) 20 | set_meta meta 21 | super string 22 | end 23 | 24 | def fallback? 25 | locale.to_sym != requested_locale.to_sym 26 | end 27 | 28 | def set_meta(meta) 29 | meta.each {|name, value| send :"#{name}=", value } if meta 30 | end 31 | end 32 | end -------------------------------------------------------------------------------- /test/data/schema.rb: -------------------------------------------------------------------------------- 1 | ActiveRecord::Schema.define do 2 | 3 | create_table :blogs, :force => true do |t| 4 | t.string :description 5 | end 6 | 7 | create_table :posts, :force => true do |t| 8 | t.references :blog 9 | end 10 | 11 | create_table :post_translations, :force => true do |t| 12 | t.string :locale 13 | t.references :post 14 | t.string :subject 15 | t.text :content 16 | end 17 | 18 | create_table :parents, :force => true do |t| 19 | end 20 | 21 | create_table :parent_translations, :force => true do |t| 22 | t.string :locale 23 | t.references :parent 24 | t.text :content 25 | t.string :type 26 | end 27 | 28 | create_table :comments, :force => true do |t| 29 | t.references :post 30 | end 31 | 32 | create_table :translated_comment_translations, :force => true do |t| 33 | t.string :locale 34 | t.references :comment 35 | t.string :subject 36 | t.text :content 37 | end 38 | 39 | end -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2008, 2009 Joshua Harvey 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /lib/globalize/backend/pluralizing.rb: -------------------------------------------------------------------------------- 1 | require 'i18n/backend/simple' 2 | 3 | module Globalize 4 | module Backend 5 | class Pluralizing < I18n::Backend::Simple 6 | def pluralize(locale, entry, count) 7 | return entry unless entry.is_a?(Hash) and count 8 | key = :zero if count == 0 && entry.has_key?(:zero) 9 | key ||= pluralizer(locale).call(count) 10 | raise InvalidPluralizationData.new(entry, count) unless entry.has_key?(key) 11 | translation entry[key], :plural_key => key 12 | end 13 | 14 | def add_pluralizer(locale, pluralizer) 15 | pluralizers[locale.to_sym] = pluralizer 16 | end 17 | 18 | def pluralizer(locale) 19 | pluralizers[locale.to_sym] || default_pluralizer 20 | end 21 | 22 | protected 23 | def default_pluralizer 24 | pluralizers[:en] 25 | end 26 | 27 | def pluralizers 28 | @pluralizers ||= { :en => lambda{|n| n == 1 ? :one : :other } } 29 | end 30 | 31 | # Overwrite this method to return something other than a String 32 | def translation(string, attributes) 33 | string 34 | end 35 | end 36 | end 37 | end -------------------------------------------------------------------------------- /lib/rails_edge_load_path_patch.rb: -------------------------------------------------------------------------------- 1 | module I18n 2 | @@load_path = nil 3 | @@default_locale = :'en-US' 4 | 5 | class << self 6 | def load_path 7 | @@load_path ||= [] 8 | end 9 | 10 | def load_path=(load_path) 11 | @@load_path = load_path 12 | end 13 | end 14 | end 15 | 16 | I18n::Backend::Simple.module_eval do 17 | def initialized? 18 | @initialized ||= false 19 | end 20 | 21 | protected 22 | 23 | def init_translations 24 | load_translations(*I18n.load_path) 25 | @initialized = true 26 | end 27 | 28 | def lookup(locale, key, scope = []) 29 | return unless key 30 | init_translations unless initialized? 31 | keys = I18n.send :normalize_translation_keys, locale, key, scope 32 | keys.inject(translations){|result, k| result[k.to_sym] or return nil } 33 | end 34 | end 35 | 36 | rails_dir = File.expand_path "#{File.dirname(__FILE__)}/../../../rails/" 37 | paths = %w(actionpack/lib/action_view/locale/en-US.yml 38 | activerecord/lib/active_record/locale/en-US.yml 39 | activesupport/lib/active_support/locale/en-US.yml) 40 | paths.each{|path| I18n.load_path << "#{rails_dir}/#{path}" } 41 | -------------------------------------------------------------------------------- /lib/globalize/i18n/missing_translations_log_handler.rb: -------------------------------------------------------------------------------- 1 | # A simple exception handler that behaves like the default exception handler 2 | # but additionally logs missing translations to a given log. 3 | # 4 | # Useful for identifying missing translations during testing. 5 | # 6 | # E.g. 7 | # 8 | # require 'globalize/i18n/missing_translations_log_handler 9 | # I18n.missing_translations_logger = RAILS_DEFAULT_LOGGER 10 | # I18n.exception_handler = :missing_translations_log_handler 11 | # 12 | # To set up a different log file: 13 | # 14 | # logger = Logger.new("#{RAILS_ROOT}/log/missing_translations.log") 15 | # I18n.missing_translations_logger = logger 16 | 17 | module I18n 18 | @@missing_translations_logger = nil 19 | 20 | class << self 21 | def missing_translations_logger 22 | @@missing_translations_logger ||= begin 23 | require 'logger' unless defined?(Logger) 24 | Logger.new(STDOUT) 25 | end 26 | end 27 | 28 | def missing_translations_logger=(logger) 29 | @@missing_translations_logger = logger 30 | end 31 | 32 | def missing_translations_log_handler(exception, locale, key, options) 33 | if MissingTranslationData === exception 34 | missing_translations_logger.warn(exception.message) 35 | return exception.message 36 | else 37 | raise exception 38 | end 39 | end 40 | end 41 | end -------------------------------------------------------------------------------- /test/i18n/missing_translations_test.rb: -------------------------------------------------------------------------------- 1 | require File.join( File.dirname(__FILE__), '..', 'test_helper' ) 2 | require 'globalize/i18n/missing_translations_log_handler' 3 | 4 | class MissingTranslationsTest < ActiveSupport::TestCase 5 | test "defines I18n.missing_translations_logger accessor" do 6 | assert I18n.respond_to?(:missing_translations_logger) 7 | end 8 | 9 | test "defines I18n.missing_translations_logger= writer" do 10 | assert I18n.respond_to?(:missing_translations_logger=) 11 | end 12 | end 13 | 14 | class TestLogger < String 15 | def warn(msg) self.concat msg; end 16 | end 17 | 18 | class LogMissingTranslationsTest < ActiveSupport::TestCase 19 | def setup 20 | @locale, @key, @options = :en, :foo, {} 21 | @exception = I18n::MissingTranslationData.new(@locale, @key, @options) 22 | 23 | @logger = TestLogger.new 24 | I18n.missing_translations_logger = @logger 25 | end 26 | 27 | test "still returns the exception message for MissingTranslationData exceptions" do 28 | result = I18n.send(:missing_translations_log_handler, @exception, @locale, @key, @options) 29 | assert_equal 'translation missing: en, foo', result 30 | end 31 | 32 | test "logs the missing translation to I18n.missing_translations_logger" do 33 | I18n.send(:missing_translations_log_handler, @exception, @locale, @key, @options) 34 | assert_equal 'translation missing: en, foo', @logger 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/globalize/model/active_record.rb: -------------------------------------------------------------------------------- 1 | require 'globalize/translation' 2 | require 'globalize/locale/fallbacks' 3 | require 'globalize/model/active_record/adapter' 4 | require 'globalize/model/active_record/translated' 5 | 6 | module Globalize 7 | module Model 8 | module ActiveRecord 9 | class << self 10 | def create_proxy_class(klass) 11 | short_name = klass.name.gsub(/.+::/, '') 12 | klass.parent.const_set "#{short_name}Translation", Class.new(::ActiveRecord::Base){ 13 | set_table_name "#{klass.name}Translation".pluralize.underscore.gsub('/', '_') 14 | belongs_to "#{short_name.underscore}".intern, :class_name => klass.name, :foreign_key => "#{short_name.underscore}_id" 15 | 16 | def locale 17 | read_attribute(:locale).to_sym 18 | end 19 | 20 | def locale=(locale) 21 | write_attribute(:locale, locale.to_s) 22 | end 23 | } 24 | end 25 | 26 | def define_accessors(klass, attr_names) 27 | attr_names.each do |attr_name| 28 | klass.send :define_method, attr_name, lambda { 29 | globalize.fetch self.class.locale, attr_name 30 | } 31 | klass.send :define_method, "#{attr_name}=", lambda {|val| 32 | globalize.stash self.class.locale, attr_name, val 33 | self[attr_name] = val 34 | } 35 | end 36 | end 37 | end 38 | end 39 | end 40 | end -------------------------------------------------------------------------------- /test/translation_test.rb: -------------------------------------------------------------------------------- 1 | require File.join( File.dirname(__FILE__), 'test_helper' ) 2 | require 'globalize/translation' 3 | 4 | class TranslationTest < ActiveSupport::TestCase 5 | include Globalize 6 | 7 | def setup 8 | @translation = Translation::Static.new 'foo', :locale => :'en-US' 9 | end 10 | 11 | test "responds to fallback?" do 12 | assert @translation.respond_to?( :fallback? ) 13 | end 14 | 15 | test "returns true when :locale and :requested_locale are not equal" do 16 | @translation.requested_locale = :'de-DE' 17 | assert @translation.fallback? 18 | end 19 | 20 | test "returns false when :locale and :requested_locale are equal" do 21 | @translation.requested_locale = :'en-US' 22 | assert !@translation.fallback? 23 | end 24 | 25 | test "has the attribute :locale" do 26 | assert @translation.respond_to?( :locale ) 27 | end 28 | 29 | test "has the attribute :requested_locale" do 30 | assert @translation.respond_to?( :requested_locale ) 31 | end 32 | 33 | test "has the attribute :options" do 34 | assert @translation.respond_to?( :options ) 35 | end 36 | 37 | test "has the attribute :plural_key" do 38 | assert @translation.respond_to?( :plural_key ) 39 | end 40 | 41 | test "has the attribute :original" do 42 | assert @translation.respond_to?( :original ) 43 | end 44 | 45 | test "Translation::Attribute has the attribute :locale" do 46 | translation = Translation::Attribute.new 'foo' 47 | assert translation.respond_to?( :locale ) 48 | end 49 | 50 | test "Translation::Attribute has the attribute :requested_locale" do 51 | translation = Translation::Attribute.new 'foo' 52 | assert translation.respond_to?( :requested_locale ) 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/globalize/locale/fallbacks.rb: -------------------------------------------------------------------------------- 1 | require 'globalize/locale/language_tag' 2 | 3 | module I18n 4 | @@fallbacks = nil 5 | 6 | class << self 7 | # Returns the current fallbacks. Defaults to +Globalize::Locale::Fallbacks+. 8 | def fallbacks 9 | @@fallbacks ||= Globalize::Locale::Fallbacks.new 10 | end 11 | 12 | # Sets the current fallbacks. Used to set a custom fallbacks instance. 13 | def fallbacks=(fallbacks) 14 | @@fallbacks = fallbacks 15 | end 16 | end 17 | end 18 | 19 | module Globalize 20 | module Locale 21 | class Fallbacks < Hash 22 | def initialize(*defaults) 23 | @map = {} 24 | map defaults.pop if defaults.last.is_a?(Hash) 25 | 26 | defaults = [I18n.default_locale.to_sym] if defaults.empty? 27 | self.defaults = defaults 28 | end 29 | 30 | def defaults=(defaults) 31 | @defaults = defaults.map{|default| compute(default, false) }.flatten << :root 32 | end 33 | attr_reader :defaults 34 | 35 | def [](tag) 36 | tag = tag.to_sym 37 | has_key?(tag) ? fetch(tag) : store(tag, compute(tag)) 38 | end 39 | 40 | def map(mappings) 41 | mappings.each do |from, to| 42 | from, to = from.to_sym, Array(to) 43 | to.each do |to| 44 | @map[from] ||= [] 45 | @map[from] << to.to_sym 46 | end 47 | end 48 | end 49 | 50 | protected 51 | 52 | def compute(tags, include_defaults = true) 53 | result = Array(tags).collect do |tag| 54 | tags = LanguageTag::tag(tag.to_sym).parents(true).map! {|t| t.to_sym } 55 | tags.each{|tag| tags += compute(@map[tag]) if @map[tag] } 56 | tags 57 | end.flatten 58 | result.push *defaults if include_defaults 59 | result.uniq 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /test/load_path_test.rb: -------------------------------------------------------------------------------- 1 | require File.join( File.dirname(__FILE__), 'test_helper' ) 2 | require 'globalize/load_path' 3 | 4 | class LoadPathTest < ActiveSupport::TestCase 5 | def setup 6 | @plugin_dir = "#{File.dirname(__FILE__)}/.." 7 | @locale_dir = "#{File.dirname(__FILE__)}/data/locale" 8 | @load_path = Globalize::LoadPath.new 9 | end 10 | 11 | test "returns glob patterns for all locales and ruby + yaml files by default" do 12 | patterns = %w(locales/all.rb 13 | locales/*.rb 14 | locales/*/**/*.rb 15 | locales/all.yml 16 | locales/*.yml 17 | locales/*/**/*.yml) 18 | assert_equal patterns, @load_path.send(:patterns, 'locales') 19 | end 20 | 21 | test "returns the glob patterns for registered locales and extensions" do 22 | @load_path.locales = [:en, :de] 23 | @load_path.extensions = [:sql] 24 | patterns = %w(locales/all.sql 25 | locales/en.sql 26 | locales/en/**/*.sql 27 | locales/de.sql 28 | locales/de/**/*.sql) 29 | assert_equal patterns, @load_path.send(:patterns, 'locales') 30 | end 31 | 32 | test "expands paths using yml as a default file extension" do 33 | @load_path << @locale_dir 34 | expected = %w(all.yml de-DE.yml en-US.yml en-US/module.yml fi-FI/module.yml root.yml) 35 | assert_equal expected, @load_path.map{|path| path.sub("#{@locale_dir}\/", '')} 36 | end 37 | 38 | test "appends new paths to the collection so earlier collected paths preceed later collected ones" do 39 | @load_path.locales = [:root] 40 | @load_path << "#{@plugin_dir}/lib/locale" 41 | @load_path << @locale_dir 42 | 43 | expected = %W(#{@plugin_dir}/lib/locale/root.yml 44 | #{@locale_dir}/all.yml 45 | #{@locale_dir}/root.yml) 46 | assert_equal expected, @load_path 47 | end 48 | 49 | end 50 | -------------------------------------------------------------------------------- /lib/globalize/load_path.rb: -------------------------------------------------------------------------------- 1 | # Locale load_path and Locale loading support. 2 | # 3 | # To use this include the Globalize::LoadPath::I18n module to I18n like this: 4 | # 5 | # I18n.send :include, Globalize::LoadPath::I18n 6 | # 7 | # Clients can add load_paths using: 8 | # 9 | # I18n.load_path.add load_path, 'rb', 'yml' # pass any number of extensions like this 10 | # I18n.load_path << 'path/to/dir' # usage without an extension, defaults to 'yml' 11 | # 12 | # And load locale data using either of: 13 | # 14 | # I18n.load_locales 'en-US', 'de-DE' 15 | # I18n.load_locale 'en-US' 16 | # 17 | # This will lookup all files named like: 18 | # 19 | # 'path/to/dir/all.yml' 20 | # 'path/to/dir/en-US.yml' 21 | # 'path/to/dir/en-US/*.yml' 22 | # 23 | # The filenames will be passed to I18n.load_translations which delegates to 24 | # the backend. So the actual behaviour depends on the implementation of the 25 | # backend. I18n::Backend::Simple will be able to read YAML and plain Ruby 26 | # files. See the documentation for I18n.load_translations for details. 27 | 28 | module Globalize 29 | class LoadPath < Array 30 | def extensions 31 | @extensions ||= ['rb', 'yml'] 32 | end 33 | attr_writer :extensions 34 | 35 | def locales 36 | @locales ||= ['*'] 37 | end 38 | attr_writer :locales 39 | 40 | def <<(path) 41 | push path 42 | end 43 | 44 | def push(*paths) 45 | super(*paths.map{|path| filenames(path) }.flatten.uniq.sort) 46 | end 47 | 48 | protected 49 | 50 | def filenames(path) 51 | return [path] if File.file? path 52 | patterns(path).map{|pattern| Dir[pattern] } 53 | end 54 | 55 | def patterns(path) 56 | locales.map do |locale| 57 | extensions.map do |extension| 58 | %W(#{path}/all.#{extension} #{path}/#{locale}.#{extension} #{path}/#{locale}/**/*.#{extension}) 59 | end 60 | end.flatten.uniq 61 | end 62 | end 63 | end -------------------------------------------------------------------------------- /lib/globalize/backend/static.rb: -------------------------------------------------------------------------------- 1 | require 'globalize/backend/pluralizing' 2 | require 'globalize/locale/fallbacks' 3 | require 'globalize/translation' 4 | 5 | module Globalize 6 | module Backend 7 | class Static < Pluralizing 8 | def initialize(*args) 9 | add(*args) unless args.empty? 10 | end 11 | 12 | def translate(locale, key, options = {}) 13 | result, default, fallback = nil, options.delete(:default), nil 14 | I18n.fallbacks[locale].each do |fallback| 15 | begin 16 | result = super(fallback, key, options) and break 17 | rescue I18n::MissingTranslationData 18 | end 19 | end 20 | result ||= default locale, default, options 21 | 22 | attrs = {:requested_locale => locale, :locale => fallback, :key => key, :options => options} 23 | translation(result, attrs) || raise(I18n::MissingTranslationData.new(locale, key, options)) 24 | end 25 | 26 | protected 27 | 28 | alias :orig_interpolate :interpolate unless method_defined? :orig_interpolate 29 | def interpolate(locale, string, values = {}) 30 | result = orig_interpolate(locale, string, values) 31 | translation = translation(string) 32 | translation.nil? ? result : translation.replace(result) 33 | end 34 | 35 | def translation(result, meta = nil) 36 | return unless result 37 | 38 | case result 39 | when Numeric 40 | result 41 | when String 42 | result = Translation::Static.new(result) unless result.is_a? Translation::Static 43 | result.set_meta meta 44 | result 45 | when Hash 46 | Hash[*result.map do |key, value| 47 | [key, translation(value, meta)] 48 | end.flatten] 49 | when Array 50 | result.map do |value| 51 | translation(value, meta) 52 | end 53 | else 54 | result 55 | # raise "unexpected translation type: #{result.inspect}" 56 | end 57 | end 58 | end 59 | end 60 | end -------------------------------------------------------------------------------- /notes.textile: -------------------------------------------------------------------------------- 1 | Stopped DB Backend in the middle, here's where we left off: 2 | 3 | h1. Some Notes 4 | 5 | * Started doing the migration generator in generators/db_backend.rb 6 | * Translation keys will be in dotted string format 7 | * Question: Do we need a plural_key column, or can we build it in to the dotted key? 8 | * We will probably have to code the following methods from scratch, to optimize db calls: 9 | ** translate 10 | ** localize 11 | ** pluralize 12 | * We should refactor @interpolation@ code so that it can be included into backend code without inheriting SimpleBackend 13 | ** Rationale: interpolation is something done entirely after a string is fetched from the data store 14 | ** Alternately, it could be done from within the I18n module 15 | 16 | h1. Schema 17 | 18 | There will be two db tables. 19 | 20 | # globalize_translations will have: locale, key, translation, created_at, updated_at. 21 | # globalize_translations_map will have: key, translation_id. 22 | 23 | globalize_translations_map will let us easily fetch entire sub-trees of namespaces. 24 | However, this table may not be necessary, as it may be feasible to just use key LIKE "some.namespace.%". 25 | 26 | h1. Caching 27 | 28 | We'll almost certainly want to implement caching in the backend. Should probably be a customized 29 | implementation based on the Rails caching mechanism, to support memcached, etc. 30 | 31 | h1. Queries 32 | 33 | We'll want to pull in lots of stuff at once and return a single translation based on some 34 | quick Ruby selection. The query will look something like this: 35 | 36 |
37 |
38 | SELECT * FROM globalize_translations
39 | WHERE locale in () AND
40 | key IN (key, default_key)
41 |
42 |
43 |
44 | The Ruby code would then pick the first translation that satisfies a fallback, in fallback order.
45 | Of course, the records with the supplied key would take precedence of those with the default key.
46 |
47 | h1. Misc
48 |
49 | We should revisit the :zero plural code. On the one hand it's certainly useful for
50 | many apps in many languages. On the other hand it's not mentioned in CLDR, and not a real
51 | concept in language pluralization. Right now, I'm feeling it's still a good idea to keep it in.
52 |
--------------------------------------------------------------------------------
/test/model/active_record/sti_translated_test.rb:
--------------------------------------------------------------------------------
1 | require File.join( File.dirname(__FILE__), '..', '..', 'test_helper' )
2 | require 'active_record'
3 | require 'globalize/model/active_record'
4 |
5 | # Hook up model translation
6 | ActiveRecord::Base.send(:include, Globalize::Model::ActiveRecord::Translated)
7 |
8 | # Load Post model
9 | require File.join( File.dirname(__FILE__), '..', '..', 'data', 'post' )
10 |
11 | class StiTranslatedTest < ActiveSupport::TestCase
12 | def setup
13 | I18n.locale = :'en-US'
14 | I18n.fallbacks.clear
15 | reset_db! File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'data', 'schema.rb'))
16 | end
17 |
18 | def teardown
19 | I18n.fallbacks.clear
20 | end
21 |
22 | test "works with simple dynamic finders" do
23 | foo = Child.create :content => 'foo'
24 | Child.create :content => 'bar'
25 | child = Child.find_by_content('foo')
26 | assert_equal foo, child
27 | end
28 |
29 | test 'change attribute on globalized model' do
30 | child = Child.create :content => 'foo'
31 | assert_equal [], child.changed
32 | child.content = 'bar'
33 | assert_equal [ 'content' ], child.changed
34 | child.content = 'baz'
35 | assert_member 'content', child.changed
36 | end
37 |
38 | test 'change attribute on globalized model after locale switching' do
39 | child = Child.create :content => 'foo'
40 | assert_equal [], child.changed
41 | child.content = 'bar'
42 | I18n.locale = :de
43 | assert_equal [ 'content' ], child.changed
44 | end
45 |
46 | test 'fallbacks with lots of locale switching' do
47 | I18n.fallbacks.map :'de-DE' => [ :'en-US' ]
48 | child = Child.create :content => 'foo'
49 |
50 | I18n.locale = :'de-DE'
51 | assert_equal 'foo', child.content
52 |
53 | I18n.locale = :'en-US'
54 | child.update_attribute :content, 'bar'
55 |
56 | I18n.locale = :'de-DE'
57 | assert_equal 'bar', child.content
58 | end
59 |
60 | test "saves all locales, even after locale switching" do
61 | child = Child.new :content => 'foo'
62 | I18n.locale = 'de-DE'
63 | child.content = 'bar'
64 | I18n.locale = 'he-IL'
65 | child.content = 'baz'
66 | child.save
67 | I18n.locale = 'en-US'
68 | child = Child.first
69 | assert_equal 'foo', child.content
70 | I18n.locale = 'de-DE'
71 | assert_equal 'bar', child.content
72 | I18n.locale = 'he-IL'
73 | assert_equal 'baz', child.content
74 | end
75 | end
--------------------------------------------------------------------------------
/test/backends/pluralizing_test.rb:
--------------------------------------------------------------------------------
1 | require File.join( File.dirname(__FILE__), '..', 'test_helper' )
2 | require 'globalize/backend/pluralizing'
3 |
4 | class PluralizingTest < ActiveSupport::TestCase
5 | def setup
6 | @backend = Globalize::Backend::Pluralizing.new
7 | @cz_pluralizer = lambda{|c| c == 1 ? :one : (2..4).include?(c) ? :few : :other }
8 | end
9 |
10 | test "#pluralizer returns the pluralizer for a given locale if defined" do
11 | assert_instance_of Proc, @backend.pluralizer(:en)
12 | end
13 |
14 | test "#pluralizer returns the default pluralizer if no pluralizer is defined for the given locale" do
15 | assert_equal @backend.pluralizer(:en), @backend.pluralizer(:de)
16 | end
17 |
18 | test "#add_pluralizer allows to store a pluralizer per locale" do
19 | assert_nothing_raised { @backend.add_pluralizer(:cz, @cz_pluralizer) }
20 | assert_equal @cz_pluralizer, @backend.pluralizer(:cz)
21 | end
22 |
23 | end
24 |
25 | class PluralizePluralizingTest < ActiveSupport::TestCase
26 | def setup
27 | @backend = Globalize::Backend::Pluralizing.new
28 | @cz_pluralizer = lambda{|c| c == 1 ? :one : (2..4).include?(c) ? :few : :other }
29 | @backend.store_translations :en, :foo => {:one => 'one en foo', :other => 'many en foos'}
30 | @backend.store_translations :cz, :foo => {:one => 'one cz foo', :few => 'few cz foos', :other => 'many cz foos'}
31 | end
32 |
33 | test "looks up the :one translation when count is 1" do
34 | assert_equal 'one en foo', @backend.translate(:en, :foo, :count => 1)
35 | end
36 |
37 | test "looks up the :other translation when count is 2" do
38 | assert_equal 'many en foos', @backend.translate(:en, :foo, :count => 2)
39 | end
40 | end
41 |
42 | class CzPluralizingTest < ActiveSupport::TestCase
43 | def setup
44 | @backend = Globalize::Backend::Pluralizing.new
45 | @cz_pluralizer = lambda{|c| c == 1 ? :one : (2..4).include?(c) ? :few : :other }
46 | @backend.store_translations :en, :foo => {:one => 'one en foo', :other => 'many en foos'}
47 | @backend.store_translations :cz, :foo => {:one => 'one cz foo', :few => 'few cz foos', :other => 'many cz foos'}
48 | @backend.add_pluralizer(:cz, @cz_pluralizer)
49 | end
50 |
51 | test "looks up the :one translation when count is 1 (:cz)" do
52 | assert_equal 'one cz foo', @backend.translate(:cz, :foo, :count => 1)
53 | end
54 |
55 | test "looks up the :few translation when count is 2 (:cz)" do
56 | assert_equal 'few cz foos', @backend.translate(:cz, :foo, :count => 2)
57 | end
58 |
59 | test "looks up the :other translation when count is 5 (:cz)" do
60 | assert_equal 'many cz foos', @backend.translate(:cz, :foo, :count => 5)
61 | end
62 |
63 | end
64 |
--------------------------------------------------------------------------------
/lib/globalize/locale/language_tag.rb:
--------------------------------------------------------------------------------
1 | # for specifications see http://en.wikipedia.org/wiki/IETF_language_tag
2 | #
3 | # SimpleParser does not implement advanced usages such as grandfathered tags
4 |
5 | module Globalize
6 | module Locale
7 | module Rfc4646
8 | SUBTAGS = [:language, :script, :region, :variant, :extension, :privateuse, :grandfathered]
9 | FORMATS = {:language => :downcase, :script => :capitalize, :region => :upcase, :variant => :downcase}
10 | end
11 |
12 | class LanguageTag < Struct.new(*Rfc4646::SUBTAGS)
13 | class << self
14 | def parser
15 | @@parser ||= SimpleParser
16 | end
17 |
18 | def parser=(parser)
19 | @@parser = parser
20 | end
21 |
22 | def tag(tag)
23 | matches = parser.match(tag)
24 | new *matches if matches
25 | end
26 | end
27 |
28 | Rfc4646::FORMATS.each do |name, format|
29 | define_method(name) { self[name].send(format) unless self[name].nil? }
30 | end
31 |
32 | def to_sym
33 | to_s.to_sym
34 | end
35 |
36 | def to_s
37 | @tag ||= to_a.compact.join("-")
38 | end
39 |
40 | def to_a
41 | members.collect {|attr| self.send(attr) }
42 | end
43 |
44 | def parent
45 | segs = to_a.compact
46 | segs.length < 2 ? nil : LanguageTag.tag(segs[0..(segs.length-2)].join('-'))
47 | end
48 |
49 | def parents(include_self = true)
50 | result, parent = [], self.dup
51 | result << parent if include_self
52 | while parent = parent.parent
53 | result << parent
54 | end
55 | result
56 | end
57 |
58 | module SimpleParser
59 | PATTERN = %r{\A(?:
60 | ([a-z]{2,3}(?:(?:-[a-z]{3}){0,3})?|[a-z]{4}|[a-z]{5,8}) # language
61 | (?:-([a-z]{4}))? # script
62 | (?:-([a-z]{2}|\d{3}))? # region
63 | (?:-([0-9a-z]{5,8}|\d[0-9a-z]{3}))* # variant
64 | (?:-([0-9a-wyz](?:-[0-9a-z]{2,8})+))* # extension
65 | (?:-(x(?:-[0-9a-z]{1,8})+))?| # privateuse subtag
66 | (x(?:-[0-9a-z]{1,8})+)| # privateuse tag
67 | /* ([a-z]{1,3}(?:-[0-9a-z]{2,8}){1,2}) */ # grandfathered
68 | )\z}xi
69 |
70 | class << self
71 | def match(tag)
72 | c = PATTERN.match(tag.to_s).captures
73 | c[0..4] << (c[5].nil? ? c[6] : c[5]) << c[7] # TODO c[7] is grandfathered, throw a NotImplemented exception here?
74 | rescue
75 | false
76 | end
77 | end
78 | end
79 | end
80 | end
81 | end
82 |
--------------------------------------------------------------------------------
/globalize2.gemspec:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 |
3 | Gem::Specification.new do |s|
4 | s.name = %q{globalize2}
5 | s.version = "0.0.6"
6 |
7 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8 | s.authors = ["Josh Harvey"]
9 | s.date = %q{2009-06-30}
10 | s.description = %q{The second incarnation of Globalize for Rails}
11 | s.email = %q{joshmh@gmail.com}
12 | s.files = ["lib/globalize/backend/chain.rb", "lib/globalize/backend/pluralizing.rb", "lib/globalize/backend/static.rb", "lib/globalize/i18n/missing_translations_log_handler.rb", "lib/globalize/i18n/missing_translations_raise_handler.rb", "lib/globalize/load_path.rb", "lib/globalize/locale/fallbacks.rb", "lib/globalize/locale/language_tag.rb", "lib/globalize/model/active_record/adapter.rb", "lib/globalize/model/active_record/translated.rb", "lib/globalize/model/active_record.rb", "lib/globalize/translation.rb", "lib/globalize.rb", "lib/rails_edge_load_path_patch.rb", "rails/init.rb", "test/backends/chained_test.rb", "test/backends/pluralizing_test.rb", "test/backends/static_test.rb", "test/data/no_globalize_schema.rb", "test/data/post.rb", "test/data/schema.rb", "test/i18n/missing_translations_test.rb", "test/load_path_test.rb", "test/locale/fallbacks_test.rb", "test/locale/language_tag_test.rb", "test/model/active_record/migration_test.rb", "test/model/active_record/sti_translated_test.rb", "test/model/active_record/translated_test.rb", "test/test_helper.rb", "test/translation_test.rb", "generators/db_backend.rb", "generators/templates/db_backend_migration.rb", "LICENSE", "README.textile", "notes.textile", "init.rb", "lib/locale/root.yml", "test/data/locale/all.yml", "test/data/locale/de-DE.yml", "test/data/locale/en-US/module.yml", "test/data/locale/en-US.yml", "test/data/locale/fi-FI/module.yml", "test/data/locale/root.yml"]
13 | s.homepage = %q{http://github.com/joshmh/globalize2}
14 | s.require_paths = ["lib"]
15 | s.rubygems_version = %q{1.3.3}
16 | s.summary = %q{The second incarnation of Globalize for Rails}
17 | s.test_files = ["test/backends/chained_test.rb", "test/backends/pluralizing_test.rb", "test/backends/static_test.rb", "test/data/no_globalize_schema.rb", "test/data/post.rb", "test/data/schema.rb", "test/i18n/missing_translations_test.rb", "test/load_path_test.rb", "test/locale/fallbacks_test.rb", "test/locale/language_tag_test.rb", "test/model/active_record/migration_test.rb", "test/model/active_record/sti_translated_test.rb", "test/model/active_record/translated_test.rb", "test/test_helper.rb", "test/translation_test.rb", "test/data/locale/all.yml", "test/data/locale/de-DE.yml", "test/data/locale/en-US/module.yml", "test/data/locale/en-US.yml", "test/data/locale/fi-FI/module.yml", "test/data/locale/root.yml"]
18 |
19 | if s.respond_to? :specification_version then
20 | current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
21 | s.specification_version = 3
22 |
23 | if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
24 | else
25 | end
26 | else
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/test/model/active_record/migration_test.rb:
--------------------------------------------------------------------------------
1 | require File.join( File.dirname(__FILE__), '..', '..', 'test_helper' )
2 | require 'active_record'
3 | require 'globalize/model/active_record'
4 |
5 | # Hook up model translation
6 | ActiveRecord::Base.send(:include, Globalize::Model::ActiveRecord::Translated)
7 |
8 | # Load Post model
9 | require File.join( File.dirname(__FILE__), '..', '..', 'data', 'post' )
10 |
11 | class MigrationTest < ActiveSupport::TestCase
12 | def setup
13 | reset_db! File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'data', 'no_globalize_schema.rb'))
14 | end
15 |
16 | test 'globalize table added' do
17 | assert !Post.connection.table_exists?( :post_translations )
18 | Post.create_translation_table! :subject => :string, :content => :text
19 | assert Post.connection.table_exists?( :post_translations )
20 | columns = Post.connection.columns( :post_translations )
21 | assert locale = columns.detect {|c| c.name == 'locale' }
22 | assert_equal :string, locale.type
23 | assert subject = columns.detect {|c| c.name == 'subject' }
24 | assert_equal :string, subject.type
25 | assert content = columns.detect {|c| c.name == 'content' }
26 | assert_equal :text, content.type
27 | assert post_id = columns.detect {|c| c.name == 'post_id' }
28 | assert_equal :integer, post_id.type
29 | assert created_at = columns.detect {|c| c.name == 'created_at' }
30 | assert_equal :datetime, created_at.type
31 | assert updated_at = columns.detect {|c| c.name == 'updated_at' }
32 | assert_equal :datetime, updated_at.type
33 | end
34 |
35 | test 'globalize table dropped' do
36 | assert !Post.connection.table_exists?( :post_translations )
37 | Post.create_translation_table! :subject => :string, :content => :text
38 | assert Post.connection.table_exists?( :post_translations )
39 | Post.drop_translation_table!
40 | assert !Post.connection.table_exists?( :post_translations )
41 | end
42 |
43 | test 'exception on untranslated field inputs' do
44 | assert_raise Globalize::Model::UntranslatedMigrationField do
45 | Post.create_translation_table! :subject => :string, :content => :text, :bogus => :string
46 | end
47 | end
48 |
49 | test 'exception on missing field inputs' do
50 | assert_raise Globalize::Model::MigrationMissingTranslatedField do
51 | Post.create_translation_table! :content => :text
52 | end
53 | end
54 |
55 | test 'exception on bad input type' do
56 | assert_raise Globalize::Model::BadMigrationFieldType do
57 | Post.create_translation_table! :subject => :string, :content => :integer
58 | end
59 | end
60 |
61 | test 'create_translation_table! should not be called on non-translated models' do
62 | assert_raise NoMethodError do
63 | Blog.create_translation_table! :name => :string
64 | end
65 | end
66 |
67 | test 'drop_translation_table! should not be called on non-translated models' do
68 | assert_raise NoMethodError do
69 | Blog.drop_translation_table!
70 | end
71 | end
72 |
73 | end
--------------------------------------------------------------------------------
/lib/globalize/model/active_record/adapter.rb:
--------------------------------------------------------------------------------
1 | module Globalize
2 | module Model
3 | class AttributeStash < Hash
4 | def contains?(locale, attr_name)
5 | locale = locale.to_sym
6 | self[locale] ||= {}
7 | self[locale].has_key? attr_name
8 | end
9 |
10 | def read(locale, attr_name)
11 | locale = locale.to_sym
12 | self[locale] ||= {}
13 | self[locale][attr_name]
14 | end
15 |
16 | def write(locale, attr_name, value)
17 | locale = locale.to_sym
18 | self[locale] ||= {}
19 | self[locale][attr_name] = value
20 | end
21 | end
22 |
23 | class Adapter
24 | def initialize(record)
25 | @record = record
26 |
27 | # TODO what exactly are the roles of cache and stash
28 | @cache = AttributeStash.new
29 | @stash = AttributeStash.new
30 | end
31 |
32 | def fetch(locale, attr_name)
33 | # locale = I18n.locale
34 | is_cached = @cache.contains?(locale, attr_name)
35 | is_cached ? @cache.read(locale, attr_name) : begin
36 | value = fetch_attribute locale, attr_name
37 | @cache.write locale, attr_name, value if value && value.locale == locale
38 | value
39 | end
40 | end
41 |
42 | def stash(locale, attr_name, value)
43 | @stash.write locale, attr_name, value
44 | @cache.write locale, attr_name, value
45 | end
46 |
47 | def update_translations!
48 | @stash.each do |locale, attrs|
49 | translation = @record.globalize_translations.find_or_initialize_by_locale(locale.to_s)
50 | attrs.each{|attr_name, value| translation[attr_name] = value }
51 | translation.save!
52 | end
53 | @stash.clear
54 | end
55 |
56 | # Clears the cache
57 | def clear
58 | @cache.clear
59 | @stash.clear
60 | end
61 |
62 | private
63 |
64 | def fetch_attribute(locale, attr_name)
65 | fallbacks = I18n.fallbacks[locale].map{|tag| tag.to_s}.map(&:to_sym)
66 |
67 | # If the translations were included with
68 | # :include => globalize_translations
69 | # there is no need to query them again.
70 | unless @record.globalize_translations.loaded?
71 | translations = @record.globalize_translations.by_locales(fallbacks)
72 | else
73 | translations = @record.globalize_translations
74 | end
75 | result, requested_locale = nil, locale
76 |
77 | # Walk through the fallbacks, starting with the current locale itself, and moving
78 | # to the next best choice, until we find a match.
79 | # Check the @globalize_set_translations cache first to see if we've just changed the
80 | # attribute and not saved yet.
81 | fallbacks.each do |fallback|
82 | # TODO should we be checking stash or just cache?
83 | result = @stash.read(fallback, attr_name) || begin
84 | translation = translations.detect {|tr| tr.locale == fallback }
85 | translation && translation.send(attr_name)
86 | end
87 | if result
88 | locale = fallback
89 | break
90 | end
91 | end
92 | result && Translation::Attribute.new(result, :locale => locale, :requested_locale => requested_locale)
93 | end
94 | end
95 | end
96 | end
97 |
--------------------------------------------------------------------------------
/lib/globalize/backend/chain.rb:
--------------------------------------------------------------------------------
1 | module I18n
2 | class << self
3 | def chain_backends(*args)
4 | self.backend = Globalize::Backend::Chain.new(*args)
5 | end
6 | end
7 | end
8 |
9 | module Globalize
10 | module Backend
11 | class Chain
12 | def initialize(*args)
13 | add(*args) unless args.empty?
14 | end
15 |
16 | # Change this to a) accept any number of backends and b) accept classes.
17 | # When classes are passed instantiate them and add the instances as backends.
18 | # Return the added backends from #add.
19 | #
20 | # Add an initialize method that accepts the same arguments and passes them
21 | # to #add, so we could:
22 | # I18n.backend = Globalize::Backend::Chain.new(Globalize::Backend::Foo, Globalize::Backend::Bar)
23 | # Globalize::Backend::Chain.new(:foo, :bar)
24 | # Globalize.chain_backends :foo, :bar
25 | def add(*backends)
26 | backends.each do |backend|
27 | backend = Globalize::Backend.const_get(backend.to_s.capitalize) if backend.is_a? Symbol
28 | backend = backend.new if backend.is_a? Class
29 | self.backends << backend
30 | end
31 | end
32 |
33 | def load_translations(*args)
34 | backends.each{|backend| backend.load_translations(*args) }
35 | end
36 |
37 | # For defaults:
38 | # Never pass any default option to the backends but instead implement our own default
39 | # mechanism (e.g. symbols as defaults would need to be passed to the whole chain to
40 | # be translated).
41 | #
42 | # For namespace lookup:
43 | # Only return if the result is not a hash OR count is not present, otherwise merge them.
44 | # So in effect the count variable would control whether we have a namespace lookup or a
45 | # pluralization going on.
46 | #
47 | # Exceptions:
48 | # Make sure that we catch MissingTranslationData exceptions and raise
49 | # one in the end when no translation was found at all.
50 | #
51 | # For bulk translation:
52 | # If the key is an array we need to call #translate for each of the
53 | # keys and collect the results.
54 |
55 | def translate(locale, key, options = {})
56 | raise I18n::InvalidLocale.new(locale) if locale.nil?
57 | return key.map{|k| translate locale, k, options } if key.is_a? Array
58 |
59 | default = options.delete(:default)
60 | result = backends.inject({}) do |namespace, backend|
61 | begin
62 | translation = backend.translate(locale.to_sym, key, options)
63 | if namespace_lookup?(translation, options)
64 | namespace.merge! translation
65 | elsif translation
66 | return translation
67 | end
68 | rescue I18n::MissingTranslationData
69 | end
70 | end
71 | result || default(locale, default, options) || raise(I18n::MissingTranslationData.new(locale, key, options))
72 | end
73 |
74 | def localize(locale, object, format = :default)
75 | backends.each do |backend|
76 | result = backend.localize(locale, object, format) and return result
77 | end
78 | end
79 |
80 | protected
81 | def backends
82 | @backends ||= []
83 | end
84 |
85 | def default(locale, default, options = {})
86 | case default
87 | when String then default
88 | when Symbol then translate locale, default, options
89 | when Array then default.each do |obj|
90 | result = default(locale, obj, options.dup) and return result
91 | end and nil
92 | end
93 | rescue I18n::MissingTranslationData
94 | nil
95 | end
96 |
97 | def namespace_lookup?(result, options)
98 | result.is_a?(Hash) and not options.has_key?(:count)
99 | end
100 | end
101 | end
102 | end
--------------------------------------------------------------------------------
/test/locale/language_tag_test.rb:
--------------------------------------------------------------------------------
1 | require File.join( File.dirname(__FILE__), '..', 'test_helper' )
2 | require 'globalize/locale/language_tag'
3 |
4 | include Globalize::Locale
5 |
6 | class LanguageTagTest < ActiveSupport::TestCase
7 | test "given a valid tag 'de' returns an LanguageTag from #tag" do
8 | assert_instance_of LanguageTag, LanguageTag.tag('de')
9 | end
10 |
11 | test "given a valid tag 'de' returns an array of subtags" do
12 | assert_equal ['de', nil, nil, nil, nil, nil, nil], LanguageTag::SimpleParser.match('de')
13 | end
14 |
15 | test "given a valid tag 'de-DE' returns an array of subtags" do
16 | assert_equal ['de', nil, 'DE', nil, nil, nil, nil], LanguageTag::SimpleParser.match('de-DE')
17 | end
18 |
19 | test "given a valid lowercase tag 'de-latn-de-variant-x-phonebk' returns an array of subtags" do
20 | assert_equal ['de', 'latn', 'de', 'variant', nil, 'x-phonebk', nil],
21 | LanguageTag::SimpleParser.match('de-latn-de-variant-x-phonebk')
22 | end
23 |
24 | test "given a valid uppercase tag 'DE-LATN-DE-VARIANT-X-PHONEBK' returns an array of subtags" do
25 | assert_equal ['DE', 'LATN', 'DE', 'VARIANT', nil, 'X-PHONEBK', nil],
26 | LanguageTag::SimpleParser.match('DE-LATN-DE-VARIANT-X-PHONEBK')
27 | end
28 |
29 | test "given an invalid tag 'a-DE' test returns false" do
30 | assert !LanguageTag::SimpleParser.match('a-DE')
31 | end
32 |
33 | test "given an invalid tag 'de-419-DE' test returns false" do
34 | assert !LanguageTag::SimpleParser.match('de-419-DE')
35 | end
36 | end
37 |
38 | class DeLatnLanguageTagTest < ActiveSupport::TestCase
39 | def setup
40 | subtags = %w(de Latn DE variant a-ext x-phonebk i-klingon)
41 | @tag = LanguageTag.new *subtags
42 | end
43 |
44 | test "returns 'de' as the language subtag in lowercase" do
45 | assert_equal 'de', @tag.language
46 | end
47 |
48 | test "returns 'Latn' as the script subtag in titlecase" do
49 | assert_equal 'Latn', @tag.script
50 | end
51 |
52 | test "returns 'DE' as the region subtag in uppercase" do
53 | assert_equal 'DE', @tag.region
54 | end
55 |
56 | test "returns 'variant' as the variant subtag in lowercase" do
57 | assert_equal 'variant', @tag.variant
58 | end
59 |
60 | test "returns 'a-ext' as the extension subtag" do
61 | assert_equal 'a-ext', @tag.extension
62 | end
63 |
64 | test "returns 'x-phonebk' as the privateuse subtag" do
65 | assert_equal 'x-phonebk', @tag.privateuse
66 | end
67 |
68 | test "returns 'i-klingon' as the grandfathered subtag" do
69 | assert_equal 'i-klingon', @tag.grandfathered
70 | end
71 |
72 | test "returns a formatted tag string from #to_s" do
73 | assert_equal 'de-Latn-DE-variant-a-ext-x-phonebk-i-klingon', @tag.to_s
74 | end
75 |
76 | test "returns an array containing the formatted subtags from #to_a" do
77 | assert_equal %w(de Latn DE variant a-ext x-phonebk i-klingon), @tag.to_a
78 | end
79 | end
80 |
81 | class InheritanceLanguageTagTest < ActiveSupport::TestCase
82 | test "returns 'de-Latn-DE-variant-a-ext-x-phonebk' as the parent of 'de-Latn-DE-variant-a-ext-x-phonebk-i-klingon'" do
83 | tag = LanguageTag.new *%w(de Latn DE variant a-ext x-phonebk i-klingon)
84 | assert_equal 'de-Latn-DE-variant-a-ext-x-phonebk', tag.parent.to_s
85 | end
86 |
87 | test "returns 'de-Latn-DE-variant-a-ext' as the parent of 'de-Latn-DE-variant-a-ext-x-phonebk'" do
88 | tag = LanguageTag.new *%w(de Latn DE variant a-ext x-phonebk)
89 | assert_equal 'de-Latn-DE-variant-a-ext', tag.parent.to_s
90 | end
91 |
92 | test "returns 'de-Latn-DE-variant' as the parent of 'de-Latn-DE-variant-a-ext'" do
93 | tag = LanguageTag.new *%w(de Latn DE variant a-ext)
94 | assert_equal 'de-Latn-DE-variant', tag.parent.to_s
95 | end
96 |
97 | test "returns 'de-Latn-DE' as the parent of 'de-Latn-DE-variant'" do
98 | tag = LanguageTag.new *%w(de Latn DE variant)
99 | assert_equal 'de-Latn-DE', tag.parent.to_s
100 | end
101 |
102 | test "returns 'de-Latn' as the parent of 'de-Latn-DE'" do
103 | tag = LanguageTag.new *%w(de Latn DE)
104 | assert_equal 'de-Latn', tag.parent.to_s
105 | end
106 |
107 | test "returns 'de' as the parent of 'de-Latn'" do
108 | tag = LanguageTag.new *%w(de Latn)
109 | assert_equal 'de', tag.parent.to_s
110 | end
111 |
112 | # TODO RFC4647 says: "If no language tag matches the request, the "default" value is returned."
113 | # where should we set the default language?
114 | # test "returns '' as the parent of 'de'" do
115 | # tag = LanguageTag.new *%w(de)
116 | # assert_equal '', tag.parent.to_s
117 | # end
118 |
119 | test "returns an array of 5 parents for 'de-Latn-DE-variant-a-ext-x-phonebk-i-klingon'" do
120 | parents = %w(de-Latn-DE-variant-a-ext-x-phonebk-i-klingon
121 | de-Latn-DE-variant-a-ext-x-phonebk
122 | de-Latn-DE-variant-a-ext
123 | de-Latn-DE-variant
124 | de-Latn-DE
125 | de-Latn
126 | de)
127 | tag = LanguageTag.new *%w(de Latn DE variant a-ext x-phonebk i-klingon)
128 | assert_equal parents, tag.parents.map{|tag| tag.to_s}
129 | end
130 | end
131 |
--------------------------------------------------------------------------------
/test/backends/static_test.rb:
--------------------------------------------------------------------------------
1 | require File.join( File.dirname(__FILE__), '..', 'test_helper' )
2 | require 'globalize/backend/static'
3 | require 'globalize/translation'
4 | require 'action_view'
5 | include ActionView::Helpers::NumberHelper
6 |
7 | I18n.locale = :'en-US' # Need to set this, since I18n defaults to 'en'
8 |
9 | class StaticTest < ActiveSupport::TestCase
10 | def setup
11 | I18n.backend = Globalize::Backend::Static.new
12 | translations = {:"en-US" => {:foo => "foo in en-US", :boz => 'boz', :buz => {:bum => 'bum'}},
13 | :"en" => {:bar => "bar in en"},
14 | :"de-DE" => {:baz => "baz in de-DE"},
15 | :"de" => {:boo => "boo in de", :number => { :currency => { :format => { :unit => '€', :format => '%n %u'}}}}}
16 | translations.each do |locale, data|
17 | I18n.backend.store_translations locale, data
18 | end
19 | I18n.fallbacks.map :"de-DE" => :"en-US", :he => :en
20 | end
21 |
22 | test "returns an instance of Translation:Static" do
23 | translation = I18n.translate :foo
24 | assert_instance_of Globalize::Translation::Static, translation
25 | end
26 |
27 | test "returns the translation in en-US if present" do
28 | assert_equal "foo in en-US", I18n.translate(:foo, :locale => :"en-US")
29 | end
30 |
31 | test "returns the translation in en if en-US is not present" do
32 | assert_equal "bar in en", I18n.translate(:bar, :locale => :"en-US")
33 | end
34 |
35 | test "returns the translation in de-DE if present" do
36 | assert_equal "baz in de-DE", I18n.translate(:baz, :locale => :"de-DE")
37 | end
38 |
39 | test "returns the translation in de if de-DE is not present" do
40 | assert_equal "boo in de", I18n.translate(:boo, :locale => :"de-DE")
41 | end
42 |
43 | test "returns the translation in en-US if none of de-DE and de are present" do
44 | assert_equal "foo in en-US", I18n.translate(:foo, :locale => :"de-DE")
45 | end
46 |
47 | test "returns the translation in en if none of de-DE, de and en-US are present" do
48 | assert_equal "bar in en", I18n.translate(:bar, :locale => :"de-DE")
49 | end
50 |
51 | test "returns the translation in en if none in he is present" do
52 | assert_equal "bar in en", I18n.translate(:bar, :locale => :he)
53 | end
54 |
55 | test "returns the given default String when the key is not present for any locale" do
56 | assert_equal "default", I18n.translate(:missing, :default => "default")
57 | end
58 |
59 | test "returns the fallback translation for the key if present for a fallback locale" do
60 | I18n.backend.store_translations :de, :non_default => "non_default in de"
61 | assert_equal "non_default in de", I18n.translate(:non_default, :default => "default", :locale => :"de-DE")
62 | end
63 |
64 | test "returns an array of translations" do
65 | assert_instance_of Array, I18n.translate([:foo, :boz])
66 | end
67 |
68 | test "returns an array of instances of Translation::Static" do
69 | assert_equal [Globalize::Translation::Static], I18n.translate([:foo, :boz]).map(&:class).uniq
70 | end
71 |
72 | test "returns a hash of translations" do
73 | assert_instance_of Hash, I18n.translate(:"buz")
74 | end
75 |
76 | test "returns an array of translations 2" do
77 | assert_equal [Globalize::Translation::Static], I18n.translate(:"buz").values.map(&:class)
78 | end
79 |
80 | test "returns currency properly formated" do
81 | currency = number_to_currency(10)
82 | assert_equal "$10.00", currency
83 | end
84 |
85 | test "returns currency properly formated for locale" do
86 | currency = number_to_currency(10, :locale => :'de')
87 | assert_equal "10.000 €", currency
88 | end
89 |
90 | test "returns currency properly formated from parameters" do
91 | currency = number_to_currency(10, :format => "%n %u", :unit => '€')
92 | assert_equal "10.00 €", currency
93 | end
94 |
95 | test "makes sure interpolation does not break even with False as string" do
96 | assert_equal "translation missing: en, support, array, skip_last_comma", I18n.translate(:"support.array.skip_last_comma")
97 | end
98 | end
99 |
100 | class TranslationStaticTest < ActiveSupport::TestCase
101 | def setup
102 | I18n.backend = Globalize::Backend::Static.new
103 | translations = {
104 | :greeting => "Hi {{name}}",
105 | :messages => { :one => "You have one message.", :other => "You have {{count}} messages."}
106 | }
107 | I18n.backend.store_translations :"en", translations
108 | end
109 |
110 | def greeting
111 | I18n.translate :greeting, :locale => :"en-US", :name => "Joshua"
112 | end
113 |
114 | test "stores the actual locale" do
115 | assert_equal :en, greeting.locale
116 | end
117 |
118 | test "stores the requested locale" do
119 | assert_equal :'en-US', greeting.requested_locale
120 | end
121 |
122 | test "stores the requested key" do
123 | assert_equal :greeting, greeting.key
124 | end
125 |
126 | test "stores the options given to #translate" do
127 | assert_equal( {:name => "Joshua"}, greeting.options )
128 | end
129 |
130 | test "stores the original translation before test was interpolated" do
131 | assert_equal "Hi {{name}}", greeting.original
132 | end
133 |
134 | test "stores the plural_key :one if pluralized as such" do
135 | message = I18n.translate :messages, :locale => :"en-US", :count => 1
136 | assert_equal :one, message.plural_key
137 | end
138 |
139 | test "stores the plural_key :other if pluralized as such" do
140 | messages = I18n.translate :messages, :locale => :"en-US", :count => 2
141 | assert_equal :other, messages.plural_key
142 | end
143 | end
144 |
--------------------------------------------------------------------------------
/test/locale/fallbacks_test.rb:
--------------------------------------------------------------------------------
1 | require File.join( File.dirname(__FILE__), '..', 'test_helper' )
2 | require 'globalize/locale/fallbacks'
3 |
4 | include Globalize::Locale
5 | I18n.default_locale = :'en-US' # This has to be set explicitly, no longer default for I18n
6 |
7 | class FallbacksTest < ActiveSupport::TestCase
8 | def setup
9 | I18n.fallbacks = Fallbacks.new
10 | end
11 |
12 | def teardown
13 | I18n.default_locale = :'en-US'
14 | end
15 |
16 | test "#[] caches computed results" do
17 | I18n.fallbacks['en']
18 | assert_equal( { :en => [:en, :"en-US", :root] }, I18n.fallbacks )
19 | end
20 |
21 | test "#defaults always reflect the I18n.default_locale if no default has been set manually" do
22 | I18n.default_locale = :'en-US'
23 | assert_equal( [:'en-US', :en, :root], I18n.fallbacks.defaults )
24 | end
25 |
26 | test "#defaults always reflect a manually passed default locale if any" do
27 | I18n.fallbacks = Fallbacks.new(:'fi-FI')
28 | assert_equal( [:'fi-FI', :fi, :root], I18n.fallbacks.defaults )
29 | I18n.default_locale = :'de-DE'
30 | assert_equal( [:'fi-FI', :fi, :root], I18n.fallbacks.defaults )
31 | end
32 |
33 | test "#defaults allows to set multiple defaults" do
34 | I18n.fallbacks = Fallbacks.new(:'fi-FI', :'se-FI')
35 | assert_equal( [:'fi-FI', :fi, :'se-FI', :se, :root], I18n.fallbacks.defaults )
36 | end
37 | end
38 |
39 | class NoMappingFallbacksTest < ActiveSupport::TestCase
40 | def setup
41 | @fallbacks = Fallbacks.new(:'en-US')
42 | end
43 |
44 | test "returns [:es, :en-US, :root] for :es" do
45 | assert_equal [:es, :"en-US", :en, :root], @fallbacks[:es]
46 | end
47 |
48 | test "returns [:es-ES, :es, :en-US, :root] for :es-ES" do
49 | assert_equal [:"es-ES", :es, :"en-US", :en, :root], @fallbacks[:"es-ES"]
50 | end
51 |
52 | test "returns [:es-MX, :es, :en-US, :root] for :es-MX" do
53 | assert_equal [:"es-MX", :es, :"en-US", :en, :root], @fallbacks[:"es-MX"]
54 | end
55 |
56 | test "returns [:es-Latn-ES, :es-Latn, :es, :en-US, :root] for :es-Latn-ES" do
57 | assert_equal [:"es-Latn-ES", :"es-Latn", :es, :"en-US", :en, :root], @fallbacks[:'es-Latn-ES']
58 | end
59 |
60 | test "returns [:en, :en-US, :root] for :en" do
61 | assert_equal [:en, :"en-US", :root], @fallbacks[:en]
62 | end
63 |
64 | test "returns [:en-US, :en, :root] for :en-US (special case: locale == default)" do
65 | assert_equal [:"en-US", :en, :root], @fallbacks[:"en-US"]
66 | end
67 | end
68 |
69 | class CaMappingFallbacksTest < ActiveSupport::TestCase
70 | # Most people who speak Catalan also live in Spain, so test is safe to assume
71 | # that they also speak Spanish as spoken in Spain.
72 | def setup
73 | @fallbacks = Fallbacks.new(:'en-US')
74 | @fallbacks.map :ca => :"es-ES"
75 | end
76 |
77 | test "returns [:ca, :es-ES, :es, :en-US, :root] for :ca" do
78 | assert_equal [:ca, :"es-ES", :es, :"en-US", :en, :root], @fallbacks[:ca]
79 | end
80 |
81 | test "returns [:ca-ES, :ca, :es-ES, :es, :en-US, :root] for :ca-ES" do
82 | assert_equal [:"ca-ES", :ca, :"es-ES", :es, :"en-US", :en, :root], @fallbacks[:"ca-ES"]
83 | end
84 | end
85 |
86 | class ArMappingFallbacksTest < ActiveSupport::TestCase
87 | # People who speak Arabic as spoken in Palestine often times also speak
88 | # Hebrew as spoken in Israel. However test is in no way safe to assume that
89 | # everybody who speaks Arabic also speaks Hebrew.
90 | def setup
91 | @fallbacks = Fallbacks.new(:'en-US')
92 | @fallbacks.map :"ar-PS" => :"he-IL"
93 | end
94 |
95 | test "returns [:ar, :en-US, :root] for :ar" do
96 | assert_equal [:ar, :"en-US", :en, :root], @fallbacks[:ar]
97 | end
98 |
99 | test "returns [:ar-EG, :ar, :en-US, :root] for :ar-EG" do
100 | assert_equal [:"ar-EG", :ar, :"en-US", :en, :root], @fallbacks[:"ar-EG"]
101 | end
102 |
103 | test "returns [:ar-PS, :ar, :he-IL, :he, :en-US, :root] for :ar-PS" do
104 | assert_equal [:"ar-PS", :ar, :"he-IL", :he, :"en-US", :en, :root], @fallbacks[:"ar-PS"]
105 | end
106 | end
107 |
108 | class SmsMappingFallbacksTest < ActiveSupport::TestCase
109 | # Sami people live in several scandinavian countries. In Finnland many people
110 | # know Swedish and Finnish. Thus, test can be assumed that Sami living in
111 | # Finnland also speak Swedish and Finnish.
112 | def setup
113 | @fallbacks = Fallbacks.new(:'en-US')
114 | @fallbacks.map :sms => [:"se-FI", :"fi-FI"]
115 | end
116 |
117 | test "returns [:sms-FI, :sms, :se-FI, :se, :fi-FI, :fi, :en-US, :root] for :sms-FI" do
118 | assert_equal [:"sms-FI", :sms, :"se-FI", :se, :"fi-FI", :fi, :"en-US", :en, :root], @fallbacks[:"sms-FI"]
119 | end
120 | end
121 |
122 | class DeAtMappingFallbacksTest < ActiveSupport::TestCase
123 | def setup
124 | @fallbacks = Fallbacks.new(:'en-US')
125 | @fallbacks.map :"de-AT" => :"de-DE"
126 | end
127 |
128 | test "returns [:de, :en-US, :root] for de" do
129 | assert_equal [:de, :"en-US", :en, :root], @fallbacks[:"de"]
130 | end
131 |
132 | test "returns [:de-DE, :de, :en-US, :root] for de-DE" do
133 | assert_equal [:"de-DE", :de, :"en-US", :en, :root], @fallbacks[:"de-DE"]
134 | end
135 |
136 | test "returns [:de-AT, :de, :de-DE, :en-US, :root] for de-AT" do
137 | assert_equal [:"de-AT", :de, :"de-DE", :"en-US", :en, :root], @fallbacks[:"de-AT"]
138 | end
139 | end
140 |
141 | class DeMappingFallbacksTest < ActiveSupport::TestCase
142 | def setup
143 | @fallbacks = Fallbacks.new(:'en-US')
144 | @fallbacks.map :de => :en, :he => :en
145 | end
146 |
147 | test "returns [:de, :en, :root] for :de" do
148 | assert_equal [:de, :en, :"en-US", :root], @fallbacks[:de]
149 | end
150 |
151 | test "returns [:he, :en, :root] for :de" do
152 | assert_equal [:he, :en, :"en-US", :root], @fallbacks[:he]
153 | end
154 | end
155 |
--------------------------------------------------------------------------------
/lib/globalize/model/active_record/translated.rb:
--------------------------------------------------------------------------------
1 | module Globalize
2 | module Model
3 |
4 | class MigrationError < StandardError; end
5 | class UntranslatedMigrationField < MigrationError; end
6 | class MigrationMissingTranslatedField < MigrationError; end
7 | class BadMigrationFieldType < MigrationError; end
8 |
9 | module ActiveRecord
10 | module Translated
11 | def self.included(base)
12 | base.extend ActMethods
13 | end
14 |
15 | module ActMethods
16 | def translates(*attr_names)
17 | options = attr_names.extract_options!
18 | options[:translated_attributes] = attr_names
19 |
20 | # Only set up once per class
21 | unless included_modules.include? InstanceMethods
22 | class_inheritable_accessor :globalize_options, :globalize_proxy
23 |
24 | include InstanceMethods
25 | extend ClassMethods
26 |
27 | metaclass.alias_method_chain :find_every, :globalize2
28 | metaclass.alias_method_chain :construct_finder_sql, :globalize2
29 |
30 | self.globalize_proxy = Globalize::Model::ActiveRecord.create_proxy_class(self)
31 | has_many(
32 | :globalize_translations,
33 | :class_name => globalize_proxy.name,
34 | :extend => Extensions,
35 | :dependent => :delete_all,
36 | :foreign_key => self.name.gsub(/.+::/, '').foreign_key
37 | )
38 |
39 | after_save :update_globalize_record
40 | end
41 |
42 | self.globalize_options = options
43 | Globalize::Model::ActiveRecord.define_accessors(self, attr_names)
44 |
45 | # Import any callbacks that have been defined by extensions to Globalize2
46 | # and run them.
47 | extend Callbacks
48 | Callbacks.instance_methods.each {|cb| send cb }
49 | end
50 |
51 | def locale=(locale)
52 | @@locale = locale
53 | end
54 |
55 | def locale
56 | (defined?(@@locale) && @@locale) || I18n.locale
57 | end
58 | end
59 |
60 | # Dummy Callbacks module. Extensions to Globalize2 can insert methods into here
61 | # and they'll be called at the end of the translates class method.
62 | module Callbacks
63 | end
64 |
65 | # Extension to the has_many :globalize_translations association
66 | module Extensions
67 | def by_locales(locales)
68 | find :all, :conditions => { :locale => locales.map(&:to_s) }
69 | end
70 | end
71 |
72 | module ClassMethods
73 | def method_missing(method, *args)
74 | if method.to_s =~ /^find_by_(\w+)$/ && globalize_options[:translated_attributes].include?($1.to_sym)
75 | find(:first, :joins => :globalize_translations,
76 | :conditions => [ "#{i18n_attr($1)} = ? AND #{i18n_attr('locale')} IN (?)",
77 | args.first,I18n.fallbacks[I18n.locale].map{|tag| tag.to_s}])
78 | else
79 | super
80 | end
81 | end
82 |
83 | def create_translation_table!(fields)
84 | translated_fields = self.globalize_options[:translated_attributes]
85 | translated_fields.each do |f|
86 | raise MigrationMissingTranslatedField, "Missing translated field #{f}" unless fields[f]
87 | end
88 | fields.each do |name, type|
89 | unless translated_fields.member? name
90 | raise UntranslatedMigrationField, "Can't migrate untranslated field: #{name}"
91 | end
92 | unless [ :string, :text ].member? type
93 | raise BadMigrationFieldType, "Bad field type for #{name}, should be :string or :text"
94 | end
95 | end
96 | translation_table_name = self.name.underscore.gsub('/', '_') + '_translations'
97 | self.connection.create_table(translation_table_name) do |t|
98 | t.integer self.name.gsub(/.+::/, '').foreign_key
99 | t.string :locale
100 | fields.each do |name, type|
101 | t.column name, type
102 | end
103 | t.timestamps
104 | end
105 | end
106 |
107 | def drop_translation_table!
108 | translation_table_name = self.name.underscore.gsub('/', '_') + '_translations'
109 | self.connection.drop_table translation_table_name
110 | end
111 |
112 | def find_every_with_globalize2(options)
113 | locale = I18n.locale
114 | locales = I18n.fallbacks[locale].map{ |tag| tag.to_s }
115 | scope_options = { :joins => :globalize_translations,
116 | :conditions => [ "#{i18n_attr('locale')} IN (?)", locales ] }
117 | with_scope(:find => scope_options) do
118 | find_every_without_globalize2(options)
119 | end
120 | end
121 |
122 | def construct_finder_sql_with_globalize2(options)
123 | sql = construct_finder_sql_without_globalize2(options)
124 | sql.sub! /SELECT(\s+DISTINCT)?/, 'SELECT DISTINCT'
125 | sql
126 | end
127 |
128 | private
129 |
130 | def i18n_attr(attribute_name)
131 | self.base_class.name.underscore.gsub('/', '_') + "_translations.#{attribute_name}"
132 | end
133 | end
134 |
135 | module InstanceMethods
136 | def reload(options = nil)
137 | globalize.clear
138 |
139 | # clear all globalized attributes
140 | # TODO what's the best way to handle this?
141 | self.class.globalize_options[:translated_attributes].each do |attr|
142 | @attributes.delete attr.to_s
143 | end
144 |
145 | super options
146 | end
147 |
148 | def globalize
149 | @globalize ||= Adapter.new self
150 | end
151 |
152 | def update_globalize_record
153 | globalize.update_translations!
154 | end
155 |
156 | def translated_locales
157 | globalize_translations.scoped(:select => 'DISTINCT locale').map {|gt| gt.locale.to_sym }
158 | end
159 |
160 | def set_translations options
161 | options.keys.each do |key|
162 |
163 | translation = globalize_translations.find_by_locale(key.to_s) ||
164 | globalize_translations.build(:locale => key.to_s)
165 | translation.update_attributes!(options[key])
166 | end
167 | end
168 |
169 | end
170 | end
171 | end
172 | end
173 | end
--------------------------------------------------------------------------------
/test/backends/chained_test.rb:
--------------------------------------------------------------------------------
1 | require File.join( File.dirname(__FILE__), '..', 'test_helper' )
2 | require 'globalize/backend/chain'
3 |
4 | module Globalize
5 | module Backend
6 | class Dummy
7 | def translate(locale, key, options = {})
8 | end
9 | end
10 | end
11 | end
12 |
13 | class ChainedTest < ActiveSupport::TestCase
14 |
15 | test "instantiates a chained backend and sets test as backend" do
16 | assert_nothing_raised { I18n.chain_backends }
17 | assert_instance_of Globalize::Backend::Chain, I18n.backend
18 | end
19 |
20 | test "passes all given arguments to the chained backends #initialize method" do
21 | Globalize::Backend::Chain.expects(:new).with(:spec, :simple)
22 | I18n.chain_backends :spec, :simple
23 | end
24 |
25 | test "passes all given arguments to #add assuming that they are backends" do
26 | # no idea how to spec that
27 | end
28 | end
29 |
30 | class AddChainedTest < ActiveSupport::TestCase
31 | def setup
32 | I18n.backend = Globalize::Backend::Chain.new
33 | end
34 |
35 | test "accepts an instance of a backend" do
36 | assert_nothing_raised { I18n.backend.add Globalize::Backend::Dummy.new }
37 | assert_instance_of Globalize::Backend::Dummy, I18n.backend.send(:backends).first
38 | end
39 |
40 | test "accepts a class and instantiates the backend" do
41 | assert_nothing_raised { I18n.backend.add Globalize::Backend::Dummy }
42 | assert_instance_of Globalize::Backend::Dummy, I18n.backend.send(:backends).first
43 | end
44 |
45 | test "accepts a symbol, constantizes test as a backend class and instantiates the backend" do
46 | assert_nothing_raised { I18n.backend.add :dummy }
47 | assert_instance_of Globalize::Backend::Dummy, I18n.backend.send(:backends).first
48 | end
49 |
50 | test "accepts any number of backend instances, classes or symbols" do
51 | assert_nothing_raised { I18n.backend.add Globalize::Backend::Dummy.new, Globalize::Backend::Dummy, :dummy }
52 | assert_instance_of Globalize::Backend::Dummy, I18n.backend.send(:backends).first
53 | assert_equal [ Globalize::Backend::Dummy, Globalize::Backend::Dummy, Globalize::Backend::Dummy ],
54 | I18n.backend.send(:backends).map{|backend| backend.class }
55 | end
56 |
57 | end
58 |
59 | class TranslateChainedTest < ActiveSupport::TestCase
60 | def setup
61 | I18n.locale = :en
62 | I18n.backend = Globalize::Backend::Chain.new
63 | @first_backend = I18n::Backend::Simple.new
64 | @last_backend = I18n::Backend::Simple.new
65 | I18n.backend.add @first_backend
66 | I18n.backend.add @last_backend
67 | end
68 |
69 | test "delegates #translate to all backends in the order they were added" do
70 | @first_backend.expects(:translate).with(:en, :foo, {})
71 | @last_backend.expects(:translate).with(:en, :foo, {})
72 | I18n.translate :foo
73 | end
74 |
75 | test "returns the result from #translate from the first backend if test's not nil" do
76 | @first_backend.store_translations :en, {:foo => 'foo from first backend'}
77 | @last_backend.store_translations :en, {:foo => 'foo from last backend'}
78 | result = I18n.translate :foo
79 | assert_equal 'foo from first backend', result
80 | end
81 |
82 | test "returns the result from #translate from the second backend if the first one returned nil" do
83 | @first_backend.store_translations :en, {}
84 | @last_backend.store_translations :en, {:foo => 'foo from last backend'}
85 | result = I18n.translate :foo
86 | assert_equal 'foo from last backend', result
87 | end
88 |
89 | test "looks up a namespace from all backends and merges them (if a result is a hash and no count option is present)" do
90 | @first_backend.store_translations :en, {:foo => {:bar => 'bar from first backend'}}
91 | @last_backend.store_translations :en, {:foo => {:baz => 'baz from last backend'}}
92 | result = I18n.translate :foo
93 | assert_equal( {:bar => 'bar from first backend', :baz => 'baz from last backend'}, result )
94 | end
95 |
96 | test "raises a MissingTranslationData exception if no translation was found" do
97 | assert_raise( I18n::MissingTranslationData ) { I18n.translate :not_here, :raise => true }
98 | end
99 |
100 | test "raises an InvalidLocale exception if the locale is nil" do
101 | assert_raise( I18n::InvalidLocale ) { Globalize::Backend::Chain.new.translate nil, :foo }
102 | end
103 |
104 | test "bulk translates a number of keys from different backends" do
105 | @first_backend.store_translations :en, {:foo => 'foo from first backend'}
106 | @last_backend.store_translations :en, {:bar => 'bar from last backend'}
107 | result = I18n.translate [:foo, :bar]
108 | assert_equal( ['foo from first backend', 'bar from last backend'], result )
109 | end
110 |
111 | test "still calls #translate on all the backends" do
112 | @last_backend.expects :translate
113 | I18n.translate :not_here, :default => 'default'
114 | end
115 |
116 | test "returns a given default string when no backend returns a translation" do
117 | result = I18n.translate :not_here, :default => 'default'
118 | assert_equal 'default', result
119 | end
120 |
121 | end
122 |
123 | class CustomLocalizeBackend < I18n::Backend::Simple
124 | def localize(locale, object, format = :default)
125 | "result from custom localize backend" if locale == 'custom'
126 | end
127 | end
128 |
129 | class LocalizeChainedTest < ActiveSupport::TestCase
130 | def setup
131 | I18n.locale = :en
132 | I18n.backend = Globalize::Backend::Chain.new
133 | @first_backend = CustomLocalizeBackend.new
134 | @last_backend = I18n::Backend::Simple.new
135 | I18n.backend.add @first_backend
136 | I18n.backend.add @last_backend
137 | @time = Time.now
138 | end
139 |
140 | test "delegates #localize to all backends in the order they were added" do
141 | @first_backend.expects(:localize).with(:en, @time, :default)
142 | @last_backend.expects(:localize).with(:en, @time, :default)
143 | I18n.localize @time
144 | end
145 |
146 | test "returns the result from #localize from the first backend if test's not nil" do
147 | @last_backend.expects(:localize).never
148 | result = I18n.localize @time, :locale => 'custom'
149 | assert_equal 'result from custom localize backend', result
150 | end
151 |
152 | test "returns the result from #localize from the second backend if the first one returned nil" do
153 | @last_backend.expects(:localize).returns "value from last backend"
154 | result = I18n.localize @time
155 | assert_equal 'value from last backend', result
156 | end
157 | end
158 |
159 | class NamespaceChainedTest < ActiveSupport::TestCase
160 | def setup
161 | @backend = Globalize::Backend::Chain.new
162 | end
163 |
164 | test "returns false if the given result is not a Hash" do
165 | assert !@backend.send(:namespace_lookup?, 'foo', {})
166 | end
167 |
168 | test "returns false if a count option is present" do
169 | assert !@backend.send(:namespace_lookup?, {:foo => 'foo'}, {:count => 1})
170 | end
171 |
172 | test "returns true if the given result is a Hash AND no count option is present" do
173 | assert @backend.send(:namespace_lookup?, {:foo => 'foo'}, {})
174 | end
175 | end
176 |
--------------------------------------------------------------------------------
/README.textile:
--------------------------------------------------------------------------------
1 | h1. Globalize2
2 |
3 | Globalize2 is the successor of Globalize for Rails.
4 |
5 | It is compatible with and builds on the new "I18n api in Ruby on Rails":http://rails-i18n.org. and adds model translations as well as a bunch of other useful features, such as Locale fallbacks (RFC4647 compliant) and automatic loading of Locale data from defined directory/file locations.
6 |
7 | Globalize2 is much more lightweight and modular than its predecessor was. Content translations in Globalize2 use default ActiveRecord features and do not limit any functionality any more.
8 |
9 | All features and tools in Globalize2 are implemented in the most unobstrusive and loosely-coupled way possible, so you can pick whatever features or tools you need for your application and combine them with other tools from other libraries or plugins.
10 |
11 | h2. Requirements
12 |
13 | Rails 2.2 (currently Rails edge)
14 |
15 | h2. Installation
16 |
17 | To install Globalize2 with its default setup just use:
18 |
19 |
20 | script/plugin install git://github.com/joshmh/globalize2.git
21 |
22 |
23 | This will:
24 |
25 | * activate model translations
26 | * set I18n.load_path to an instance of Globalize::LoadPath
27 | * set I18n.backend to an instance of Globalize::Backend::Static
28 |
29 | h2. Configuration
30 |
31 | You might want to add additional configuration to an initializer, e.g. config/initializers/globalize.rb
32 |
33 | h2. Model translations
34 |
35 | Model translations (or content translations) allow you to translate your models' attribute values. E.g.
36 |
37 |
38 | class Post < ActiveRecord::Base
39 | translates :title, :text
40 | end
41 |
42 |
43 | Allows you to values for the attributes :title and :text per locale:
44 |
45 |
46 | I18n.locale = :en
47 | post.title # Globalize2 rocks!
48 |
49 | I18n.locale = :he
50 | post.title # גלובאלייז2 שולט!
51 |
52 |
53 | In order to make this work, you'll need to add the appropriate translation tables. Globalize2 comes with a handy helper method to help you do this. It's called @create_translation_table!@. Here's an example:
54 |
55 |
56 | class CreatePosts < ActiveRecord::Migration
57 | def self.up
58 | create_table :posts do |t|
59 | t.timestamps
60 | end
61 | Post.create_translation_table! :title => :string, :text => :text
62 | end
63 | def self.down
64 | drop_table :posts
65 | Post.drop_translation_table!
66 | end
67 | end
68 |
69 |
70 | Note that the ActiveRecord model @Post@ must already exist and have a @translates@ directive listing the translated fields.
71 |
72 | h2. Globalize::Backend::Static
73 |
74 | Globalize2 ships with a Static backend that builds on the Simple backend from the I18n library (which is shipped with Rails) and adds the following features:
75 |
76 | * It uses locale fallbacks when looking up translation data.
77 | * It returns an instance of Globalize::Translation::Static instead of a plain Ruby String as a translation.
78 | * It allows to hook in custom pluralization logic as lambdas.
79 |
80 | h2. Custom pluralization logic
81 |
82 | The Simple backend has its pluralization algorithm baked in hardcoded. This algorithm is only suitable for English and other languages that have the same pluralization rules. It is not suitable for, e.g., Czech though.
83 |
84 | To add custom pluralization logic to Globalize' Static backend you can do something like this:
85 |
86 |
87 | @backend.add_pluralizer :cz, lambda{|c|
88 | c == 1 ? :one : (2..4).include?(c) ? :few : :other
89 | }
90 |
91 |
92 | h2. Locale Fallbacks
93 |
94 | Globalize2 ships with a Locale fallback tool which extends the I18n module to hold a fallbacks instance which is set to an instance of Globalize::Locale::Fallbacks by default but can be swapped with a different implementation.
95 |
96 | Globalize2 fallbacks will compute a number of other locales for a given locale. For example:
97 |
98 |
99 | I18n.fallbacks[:"es-MX"] # => [:"es-MX", :es, :"en-US", :en]
100 |
101 |
102 | Globalize2 fallbacks always fall back to
103 |
104 | * all parents of a given locale (e.g. :es for :"es-MX"),
105 | * then to the fallbacks' default locales and all of their parents and
106 | * finally to the :root locale.
107 |
108 | The default locales are set to [:"en-US"] by default but can be set to something else. The root locale is a concept borrowed from "CLDR":http://unicode.org and makes sense for storing common locale data which works as a last default fallback (e.g. "ltr" for bidi directions).
109 |
110 | One can additionally add any number of additional fallback locales manually. These will be added before the default locales to the fallback chain. For example:
111 |
112 |
113 | fb = I18n.fallbacks
114 |
115 | fb.map :ca => :"es-ES"
116 | fb[:ca] # => [:ca, :"es-ES", :es, :"en-US", :en]
117 |
118 | fb.map :"ar-PS" => :"he-IL"
119 | fb[:"ar-PS"] # => [:"ar-PS", :ar, :"he-IL", :he, :"en-US", :en]
120 | fb[:"ar-EG"] # => [:"ar-EG", :ar, :"en-US", :en]
121 |
122 | fb.map :sms => [:"se-FI", :"fi-FI"]
123 | fb[:sms] # => [:sms, :"se-FI", :se, :"fi-FI", :fi, :"en-US", :en]
124 |
125 |
126 | h2. Globalize::LoadPath
127 |
128 | Globalize2 replaces the plain Ruby array that is set to I18n.load_path by default through an instance of Globalize::LoadPath.
129 |
130 | This object can be populated with both paths to files and directories. If a path to a directory is added to it it will look up all locale data files present in that directory enforcing the following convention:
131 |
132 |
133 | I18n.load_path << "#{RAILS_ROOT}/lib/locales"
134 |
135 | # will load all the following files if present:
136 | lib/locales/all.yml
137 | lib/locales/fr.yml
138 | lib/locales/fr/*.yaml
139 | lib/locales/ru.yml
140 | lib/locales/ru/*.yaml
141 | ...
142 |
143 |
144 | One can also specify which locales are used. By default this is set to "*" meaning that files for all locales are added. To define that only files for the locale :es are added one can specify:
145 |
146 |
147 | I18n.load_path.locales = [:es]
148 |
149 |
150 | One can also specify which file extensions are used. By default this is set to ['rb', 'yml'] so plain Ruby and YAML files are added if found. To define that only *.sql files are added one can specify:
151 |
152 |
153 | I18n.load_path.extensions = ['sql']
154 |
155 |
156 | Note that Globalize::LoadPath "expands" a directory to its contained file paths immediately when you add it to the load_path. Thus, if you change the locales or extensions settings in the middle of your application the change won't be applied to already added file paths.
157 |
158 |
159 | h2. Globalize::Translation classes
160 |
161 | Globalize2's Static backend as well as Globalize2 model translations return instances of Globalize::Translation classes (instead of plain Ruby Strings). These are simple and lightweight value objects that carry some additional meta data about the translation and how it was looked up.
162 |
163 | Model translations return instances of Globalize::Translation::Attribute, the Static backend returns instances of Globalize::Translation::Static.
164 |
165 | For example:
166 |
167 |
168 | I18n.locale = :de
169 |
170 | # Translation::Attribute
171 | title = Post.first.title # assuming that no translation can be found:
172 | title.locale # => :en
173 | title.requested_locale # => :de
174 | title.fallback? # => true
175 |
176 | # Translation::Static
177 | rails = I18n.t :rails # assuming that no translation can be found:
178 | rails.locale # => :en
179 | rails.requested_locale # => :de
180 | rails.fallback? # => true
181 | rails.options # returns the options passed to #t
182 | rails.plural_key # returns the plural_key (e.g. :one, :other)
183 | rails.original # returns the original translation with no values
184 | # interpolated to it (e.g. "Hi {{name}}!")
185 |
186 |
187 | h2. Missing Translations Log Handler
188 |
189 | A simple exception handler that behaves like the default exception handler but additionally logs missing translations to a given log.
190 |
191 | Useful for identifying missing translations during testing.
192 |
193 | E.g.
194 |
195 | require 'globalize/i18n/missing_translations_log_handler
196 | I18n.missing_translations_logger = RAILS_DEFAULT_LOGGER
197 | I18n.exception_handler = :missing_translations_log_handler
198 |
199 | To set up a different log file:
200 |
201 | logger = Logger.new("#{RAILS_ROOT}/log/missing_translations.log")
202 | I18n.missing_translations_logger = logger
203 |
--------------------------------------------------------------------------------
/test/model/active_record/translated_test.rb:
--------------------------------------------------------------------------------
1 | require File.join( File.dirname(__FILE__), '..', '..', 'test_helper' )
2 | require 'active_record'
3 | require 'globalize/model/active_record'
4 |
5 | # Hook up model translation
6 | ActiveRecord::Base.send(:include, Globalize::Model::ActiveRecord::Translated)
7 |
8 | # Load Post model
9 | require File.join( File.dirname(__FILE__), '..', '..', 'data', 'post' )
10 |
11 | class TranslatedTest < ActiveSupport::TestCase
12 | def setup
13 | I18n.locale = :'en-US'
14 | I18n.fallbacks.clear
15 | reset_db! File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'data', 'schema.rb'))
16 | ActiveRecord::Base.locale = nil
17 | end
18 |
19 | def teardown
20 | I18n.fallbacks.clear
21 | end
22 |
23 | test "modifiying translated fields" do
24 | post = Post.create :subject => 'foo'
25 | assert_equal 'foo', post.subject
26 | post.subject = 'bar'
27 | assert_equal 'bar', post.subject
28 | end
29 |
30 | test "modifiying translated fields while switching locales" do
31 | post = Post.create :subject => 'foo'
32 | assert_equal 'foo', post.subject
33 | I18n.locale = :'de-DE'
34 | post.subject = 'bar'
35 | assert_equal 'bar', post.subject
36 | I18n.locale = :'en-US'
37 | assert_equal 'foo', post.subject
38 | I18n.locale = :'de-DE'
39 | post.subject = 'bar'
40 | end
41 |
42 | test "has post_translations" do
43 | post = Post.create
44 | assert_nothing_raised { post.globalize_translations }
45 | end
46 |
47 | test "has German post_translations" do
48 | I18n.locale = :de
49 | post = Post.create :subject => 'foo'
50 | assert_equal 1, post.globalize_translations.size
51 | I18n.locale = :en
52 | assert_equal 1, post.globalize_translations.size
53 | end
54 |
55 | test "returns the value passed to :subject" do
56 | post = Post.new
57 | assert_equal 'foo', (post.subject = 'foo')
58 | end
59 |
60 | test "translates subject and content into en-US" do
61 | post = Post.create :subject => 'foo', :content => 'bar'
62 | assert_equal 'foo', post.subject
63 | assert_equal 'bar', post.content
64 | assert post.save
65 | post.reload
66 | assert_equal 'foo', post.subject
67 | assert_equal 'bar', post.content
68 | end
69 |
70 | test "finds a German post" do
71 | post = Post.create :subject => 'foo (en)', :content => 'bar'
72 | I18n.locale = 'de-DE'
73 | post = Post.first
74 | post.subject = 'baz (de)'
75 | post.save
76 | assert_equal 'baz (de)', Post.first.subject
77 | I18n.locale = :'en-US'
78 | assert_equal 'foo (en)', Post.first.subject
79 | end
80 |
81 | test "saves an English post and loads test correctly" do
82 | assert_nil Post.first
83 | post = Post.create :subject => 'foo', :content => 'bar'
84 | assert post.save
85 | post = Post.first
86 | assert_equal 'foo', post.subject
87 | assert_equal 'bar', post.content
88 | end
89 |
90 | test "updates an attribute" do
91 | post = Post.create :subject => 'foo', :content => 'bar'
92 | post.update_attribute :subject, 'baz'
93 | assert_equal 'baz', Post.first.subject
94 | end
95 |
96 | test "update_attributes failure" do
97 | post = Post.create :subject => 'foo', :content => 'bar'
98 | assert !post.update_attributes( { :subject => '' } )
99 | assert_nil post.reload.attributes['subject']
100 | assert_equal 'foo', post.subject
101 | end
102 |
103 | test "validates presence of :subject" do
104 | post = Post.new
105 | assert !post.save
106 |
107 | post = Post.new :subject => 'foo'
108 | assert post.save
109 | end
110 |
111 | test "returns the value for the correct locale, after locale switching" do
112 | post = Post.create :subject => 'foo'
113 | I18n.locale = 'de-DE'
114 | post.subject = 'bar'
115 | post.save
116 | I18n.locale = 'en-US'
117 | post = Post.first
118 | assert_equal 'foo', post.subject
119 | I18n.locale = 'de-DE'
120 | assert_equal 'bar', post.subject
121 | end
122 |
123 | test "keeping one field in new locale when other field is changed" do
124 | I18n.fallbacks.map 'de-DE' => [ 'en-US' ]
125 | post = Post.create :subject => 'foo'
126 | I18n.locale = 'de-DE'
127 | post.content = 'bar'
128 | assert_equal 'foo', post.subject
129 | end
130 |
131 | test "modifying non-required field in a new locale" do
132 | I18n.fallbacks.map 'de-DE' => [ 'en-US' ]
133 | post = Post.create :subject => 'foo'
134 | I18n.locale = 'de-DE'
135 | post.content = 'bar'
136 | assert post.save
137 | end
138 |
139 | test "returns the value for the correct locale, after locale switching, without saving" do
140 | post = Post.create :subject => 'foo'
141 | I18n.locale = 'de-DE'
142 | post.subject = 'bar'
143 | I18n.locale = 'en-US'
144 | assert_equal 'foo', post.subject
145 | I18n.locale = 'de-DE'
146 | assert_equal 'bar', post.subject
147 | end
148 |
149 | test "saves all locales, even after locale switching" do
150 | post = Post.new :subject => 'foo'
151 | I18n.locale = 'de-DE'
152 | post.subject = 'bar'
153 | I18n.locale = 'he-IL'
154 | post.subject = 'baz'
155 | post.save
156 | I18n.locale = 'en-US'
157 | post = Post.first
158 | assert_equal 'foo', post.subject
159 | I18n.locale = 'de-DE'
160 | assert_equal 'bar', post.subject
161 | I18n.locale = 'he-IL'
162 | assert_equal 'baz', post.subject
163 | end
164 |
165 | test "resolves a simple fallback" do
166 | I18n.locale = 'de-DE'
167 | post = Post.create :subject => 'foo'
168 | I18n.locale = 'de'
169 | post.subject = 'baz'
170 | post.content = 'bar'
171 | post.save
172 | I18n.locale = 'de-DE'
173 | assert_equal 'foo', post.subject
174 | assert_equal 'bar', post.content
175 | end
176 |
177 | test "resolves a simple fallback without reloading" do
178 | I18n.locale = 'de-DE'
179 | post = Post.new :subject => 'foo'
180 | I18n.locale = 'de'
181 | post.subject = 'baz'
182 | post.content = 'bar'
183 | I18n.locale = 'de-DE'
184 | assert_equal 'foo', post.subject
185 | assert_equal 'bar', post.content
186 | end
187 |
188 | test "resolves a complex fallback without reloading" do
189 | I18n.fallbacks.map 'de' => %w(en he)
190 | I18n.locale = 'de'
191 | post = Post.new
192 | I18n.locale = 'en'
193 | post.subject = 'foo'
194 | I18n.locale = 'he'
195 | post.subject = 'baz'
196 | post.content = 'bar'
197 | I18n.locale = 'de'
198 | assert_equal 'foo', post.subject
199 | assert_equal 'bar', post.content
200 | end
201 |
202 | test "returns nil if no translations are found" do
203 | post = Post.new :subject => 'foo'
204 | assert_equal 'foo', post.subject
205 | assert_nil post.content
206 | end
207 |
208 | test "returns nil if no translations are found; reloaded" do
209 | post = Post.create :subject => 'foo'
210 | post = Post.first
211 | assert_equal 'foo', post.subject
212 | assert_nil post.content
213 | end
214 |
215 | test "works with associations" do
216 | blog = Blog.create
217 | post1 = blog.posts.create :subject => 'foo'
218 | I18n.locale = 'de-DE'
219 | post2 = blog.posts.create :subject => 'bar'
220 | assert_equal 2, blog.posts.size
221 | I18n.locale = 'en-US'
222 | assert_equal 'foo', blog.posts.first.subject
223 | assert_nil blog.posts.last.subject
224 | I18n.locale = 'de-DE'
225 | assert_equal 'bar', blog.posts.last.subject
226 | end
227 |
228 | test "works with simple dynamic finders" do
229 | foo = Post.create :subject => 'foo'
230 | Post.create :subject => 'bar'
231 | post = Post.find_by_subject('foo')
232 | assert_equal foo, post
233 | end
234 |
235 | test 'change attribute on globalized model' do
236 | post = Post.create :subject => 'foo', :content => 'bar'
237 | assert_equal [], post.changed
238 | post.subject = 'baz'
239 | assert_equal [ 'subject' ], post.changed
240 | post.content = 'quux'
241 | assert_member 'subject', post.changed
242 | assert_member 'content', post.changed
243 | end
244 |
245 | test 'change attribute on globalized model after locale switching' do
246 | post = Post.create :subject => 'foo', :content => 'bar'
247 | assert_equal [], post.changed
248 | post.subject = 'baz'
249 | I18n.locale = :de
250 | assert_equal [ 'subject' ], post.changed
251 | end
252 |
253 | test 'fallbacks with lots of locale switching' do
254 | I18n.fallbacks.map :'de-DE' => [ :'en-US' ]
255 | post = Post.create :subject => 'foo'
256 |
257 | I18n.locale = :'de-DE'
258 | assert_equal 'foo', post.subject
259 |
260 | I18n.locale = :'en-US'
261 | post.update_attribute :subject, 'bar'
262 |
263 | I18n.locale = :'de-DE'
264 | assert_equal 'bar', post.subject
265 | end
266 |
267 | test 'reload' do
268 | post = Post.create :subject => 'foo', :content => 'bar'
269 | post.subject = 'baz'
270 | assert_equal 'foo', post.reload.subject
271 | end
272 |
273 | test 'complex writing and stashing' do
274 | post = Post.create :subject => 'foo', :content => 'bar'
275 | post.subject = nil
276 | assert_nil post.subject
277 | assert !post.valid?
278 | end
279 |
280 | test 'translated class locale setting' do
281 | assert ActiveRecord::Base.respond_to?(:locale)
282 | assert_equal :'en-US', I18n.locale
283 | assert_equal :'en-US', ActiveRecord::Base.locale
284 | I18n.locale = :de
285 | assert_equal :de, I18n.locale
286 | assert_equal :de, ActiveRecord::Base.locale
287 | ActiveRecord::Base.locale = :es
288 | assert_equal :de, I18n.locale
289 | assert_equal :es, ActiveRecord::Base.locale
290 | I18n.locale = :fr
291 | assert_equal :fr, I18n.locale
292 | assert_equal :es, ActiveRecord::Base.locale
293 | end
294 |
295 | test "untranslated class responds to locale" do
296 | assert Blog.respond_to?(:locale)
297 | end
298 |
299 | test "to ensure locales in different classes are the same" do
300 | ActiveRecord::Base.locale = :de
301 | assert_equal :de, ActiveRecord::Base.locale
302 | assert_equal :de, Parent.locale
303 | Parent.locale = :es
304 | assert_equal :es, ActiveRecord::Base.locale
305 | assert_equal :es, Parent.locale
306 | end
307 |
308 | test "attribute saving goes by content locale and not global locale" do
309 | ActiveRecord::Base.locale = :de
310 | assert_equal :'en-US', I18n.locale
311 | Post.create :subject => 'foo'
312 | assert_equal :de, Post.first.globalize_translations.first.locale
313 | end
314 |
315 | test "attribute loading goes by content locale and not global locale" do
316 | post = Post.create :subject => 'foo'
317 | assert_equal :'en-US', ActiveRecord::Base.locale
318 | ActiveRecord::Base.locale = :de
319 | assert_equal :'en-US', I18n.locale
320 | post.update_attribute :subject, 'foo [de]'
321 | assert_equal 'foo [de]', Post.first.subject
322 | ActiveRecord::Base.locale = :'en-US'
323 | assert_equal 'foo', Post.first.subject
324 | end
325 |
326 | test "access content locale before setting" do
327 | Globalize::Model::ActiveRecord::Translated::ActMethods.class_eval "remove_class_variable(:@@locale)"
328 | assert_nothing_raised { ActiveRecord::Base.locale }
329 | end
330 |
331 | test "translated_locales" do
332 | Post.locale = :de
333 | post = Post.create :subject => 'foo'
334 | Post.locale = :es
335 | post.update_attribute :subject, 'bar'
336 | Post.locale = :fr
337 | post.update_attribute :subject, 'baz'
338 | assert_equal [ :de, :es, :fr ], post.translated_locales
339 | assert_equal [ :de, :es, :fr ], Post.first.translated_locales
340 | end
341 |
342 | test "including globalize_translations" do
343 | I18n.locale = :de
344 | Post.create :subject => "Foo1", :content => "Bar1"
345 | Post.create :subject => "Foo2", :content => "Bar2"
346 |
347 | class << Post
348 | def tranlsations_included
349 | self.all(:include => :globalize_translations)
350 | end
351 | end
352 |
353 | default = Post.all.map {|x| [x.subject, x.content]}
354 | with_include = Post.tranlsations_included.map {|x| [x.subject, x.content]}
355 | assert_equal default, with_include
356 | end
357 |
358 | test "setting multiple translations at once with options hash" do
359 | Post.locale = :de
360 | post = Post.create :subject => "foo1", :content => "foo1"
361 | Post.locale = :en
362 | post.update_attributes( :subject => "bar1", :content => "bar1" )
363 |
364 | options = { :de => {:subject => "foo2", :content => "foo2"},
365 | :en => {:subject => "bar2", :content => "bar2"} }
366 | post.set_translations options
367 | post.reload
368 |
369 | assert ["bar2", "bar2"], [post.subject, post.content]
370 | Post.locale = :de
371 | assert ["foo2", "foo2"], [post.subject, post.content]
372 | end
373 |
374 | test "setting only one translation with set_translations" do
375 | Post.locale = :de
376 | post = Post.create :subject => "foo1", :content => "foo1"
377 | Post.locale = :en
378 | post.update_attributes( :subject => "bar1", :content => "bar1" )
379 |
380 | options = { :en => {:subject => "bar2", :content => "bar2"} }
381 | post.set_translations options
382 | post.reload
383 |
384 | assert ["bar2", "bar2"], [post.subject, post.content]
385 | Post.locale = :de
386 | assert ["foo1", "foo1"], [post.subject, post.content]
387 | end
388 |
389 | test "setting only selected attributes with set_translations" do
390 | Post.locale = :de
391 | post = Post.create :subject => "foo1", :content => "foo1"
392 | Post.locale = :en
393 | post.update_attributes( :subject => "bar1", :content => "bar1" )
394 |
395 | options = { :de => {:content => "foo2"}, :en => {:subject => "bar2"} }
396 | post.set_translations options
397 | post.reload
398 |
399 | assert ["bar2", "bar1"], [post.subject, post.content]
400 | Post.locale = :de
401 | assert ["foo1", "foo2"], [post.subject, post.content]
402 | end
403 |
404 | test "setting invalid attributes raises ArgumentError" do
405 | Post.locale = :de
406 | post = Post.create :subject => "foo1", :content => "foo1"
407 | Post.locale = :en
408 | post.update_attributes( :subject => "bar1", :content => "bar1" )
409 |
410 | options = { :de => {:fake => "foo2"} }
411 | exception = assert_raise(ActiveRecord::UnknownAttributeError) do
412 | post.set_translations options
413 | end
414 | assert_equal "unknown attribute: fake", exception.message
415 | end
416 |
417 | test "reload accepting find options" do
418 | p = Post.create :subject => "Foo", :content => "Bar"
419 | assert p.reload(:readonly => true, :lock => true)
420 | assert_raise(ArgumentError) { p.reload(:foo => :bar) }
421 | end
422 |
423 | test "dependent destroy of translation" do
424 | p = Post.create :subject => "Foo", :content => "Bar"
425 | assert_equal 1, PostTranslation.count
426 | p.destroy
427 | assert_equal 0, PostTranslation.count
428 | end
429 |
430 | test "translating subclass of untranslated comment model" do
431 | translated_comment = TranslatedComment.create(:post => @post)
432 | assert_nothing_raised { translated_comment.globalize_translations }
433 | end
434 |
435 | test "modifiying translated comments works as expected" do
436 | I18n.locale = :en
437 | translated_comment = TranslatedComment.create(:post => @post, :content => 'foo')
438 | assert_equal 'foo', translated_comment.content
439 |
440 | I18n.locale = :de
441 | translated_comment.content = 'bar'
442 | assert translated_comment.save
443 | assert_equal 'bar', translated_comment.content
444 |
445 | I18n.locale = :en
446 | assert_equal 'foo', translated_comment.content
447 |
448 | assert_equal 2, translated_comment.globalize_translations.size
449 | end
450 | end
451 |
452 | # TODO should validate_presence_of take fallbacks into account? maybe we need
453 | # an extra validation call, or more options for validate_presence_of.
454 | # TODO error checking for fields that exist in main table, don't exist in
455 | # proxy table, aren't strings or text
456 | #
457 | # TODO allow finding by translated attributes in conditions?
458 | # TODO generate advanced dynamic finders?
459 |
--------------------------------------------------------------------------------