├── .gitignore ├── LICENSE ├── README.textile ├── Rakefile ├── VERSION ├── generators ├── db_backend.rb └── templates │ └── db_backend_migration.rb ├── globalize2.gemspec ├── init.rb ├── lib ├── globalize.rb ├── globalize │ ├── active_record.rb │ └── active_record │ │ ├── adapter.rb │ │ ├── attributes.rb │ │ └── migration.rb └── i18n │ ├── missing_translations_log_handler.rb │ └── missing_translations_raise_handler.rb └── test ├── active_record ├── fallbacks_test.rb ├── migration_test.rb ├── sti_translated_test.rb ├── translates_test.rb ├── translation_class_test.rb └── validation_tests.rb ├── active_record_test.rb ├── all.rb ├── data ├── models.rb ├── no_globalize_schema.rb └── schema.rb ├── i18n └── missing_translations_test.rb └── test_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | doc 2 | spec/spec/db/* 3 | vendor 4 | NOTES 5 | .DS_Store -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /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://guides.rubyonrails.org/i18n.html. and adds model translations to ActiveRecord. 6 | 7 | Globalize2 is much more lightweight and compatible than its predecessor was. Model translations in Globalize2 use default ActiveRecord features and do not limit any ActiveRecord functionality any more. 8 | 9 | h2. Requirements 10 | 11 | ActiveRecord 12 | I18n 13 | 14 | (or Rails > 2.2) 15 | 16 | h2. Installation 17 | 18 | To install Globalize2 with its default setup just use: 19 | 20 |

21 | script/plugin install git://github.com/joshmh/globalize2.git
22 | 
23 | 24 | h2. Model translations 25 | 26 | Model translations allow you to translate your models' attribute values. E.g. 27 | 28 |

29 | class Post < ActiveRecord::Base
30 |   translates :title, :text
31 | end
32 | 
33 | 34 | Allows you to values for the attributes :title and :text per locale: 35 | 36 |

37 | I18n.locale = :en
38 | post.title # => Globalize2 rocks!
39 | 
40 | I18n.locale = :he
41 | post.title # => גלובאלייז2 שולט!
42 | 
43 | 44 | 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: 45 | 46 |

47 | class CreatePosts < ActiveRecord::Migration
48 |   def self.up
49 |     create_table :posts do |t|
50 |       t.timestamps
51 |     end
52 |     Post.create_translation_table! :title => :string, :text => :text
53 |   end
54 |   def self.down
55 |     drop_table :posts
56 |     Post.drop_translation_table!
57 |   end
58 | end
59 | 
60 | 61 | Note that the ActiveRecord model @Post@ must already exist and have a @translates@ directive listing the translated fields. 62 | 63 | h2. Migration from Globalize 64 | 65 | See this script by Tomasz Stachewicz: http://gist.github.com/120867 66 | 67 | h2. Changes since Globalize2 v0.1.0 68 | 69 | * The association globalize_translations has been renamed to translations. 70 | 71 | h2. Alternative Solutions 72 | 73 | * "Veger's fork":http://github.com/veger/globalize2 - uses default AR schema for the default locale, delegates to the translations table for other locales only 74 | * "TranslatableColumns":http://github.com/iain/translatable_columns - have multiple languages of the same attribute in a model (Iain Hecker) 75 | * "localized_record":http://github.com/glennpow/localized_record - allows records to have localized attributes without any modifications to the database (Glenn Powell) 76 | * "model_translations":http://github.com/janne/model_translations - Minimal implementation of Globalize2 style model translations (Jan Andersson) 77 | 78 | h2. Related solutions 79 | 80 | * "globalize2_versioning":http://github.com/joshmh/globalize2_versioning - acts_as_versioned style versioning for Globalize2 (Joshua Harvey) 81 | * "i18n_multi_locales_validations":http://github.com/ZenCocoon/i18n_multi_locales_validations - multi-locales attributes validations to validates attributes from Globalize2 translations models (Sébastien Grosjean) 82 | * "Globalize2 Demo App":http://github.com/svenfuchs/globalize2-demo - demo application for Globalize2 (Sven Fuchs) 83 | * "migrate_from_globalize1":http://gist.github.com/120867 - migrate model translations from Globalize1 to Globalize2 (Tomasz Stachewicz) 84 | * "easy_globalize2_accessors":http://github.com/astropanic/easy_globalize2_accessors - easily access (read and write) globalize2-translated fields (astropanic, Tomasz Stachewicz) 85 | * "globalize2-easy-translate":http://github.com/bsamman/globalize2-easy-translate - adds methods to easily access or set translated attributes to your model (bsamman) 86 | * "batch_translations":http://github.com/alvarezrilla/batch_translations - allow saving multiple Globalize2 translations in the same request (Jose Alvarez Rilla) 87 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'rake/testtask' 3 | require 'rake/rdoctask' 4 | 5 | desc 'Default: run unit tests.' 6 | task :default => :test 7 | 8 | desc 'Test the globalize2 plugin.' 9 | Rake::TestTask.new(:test) do |t| 10 | t.libs << 'lib' 11 | t.pattern = 'test/**/*_test.rb' 12 | t.verbose = true 13 | end 14 | 15 | desc 'Generate documentation for the globalize2 plugin.' 16 | Rake::RDocTask.new(:rdoc) do |rdoc| 17 | rdoc.rdoc_dir = 'rdoc' 18 | rdoc.title = 'Globalize2' 19 | rdoc.options << '--line-numbers' << '--inline-source' 20 | rdoc.rdoc_files.include('README') 21 | rdoc.rdoc_files.include('lib/**/*.rb') 22 | end 23 | 24 | begin 25 | require 'jeweler' 26 | Jeweler::Tasks.new do |s| 27 | s.name = "globalize2" 28 | s.summary = "Rails I18n: de-facto standard library for ActiveRecord data translation" 29 | s.description = "Rails I18n: de-facto standard library for ActiveRecord data translation" 30 | s.email = "joshmh@gmail.com" 31 | s.homepage = "http://github.com/joshmh/globalize2" 32 | # s.rubyforge_project = '' 33 | s.authors = ["Sven Fuchs, Joshua Harvey, Clemens Kofler, John-Paul Bader"] 34 | # s.add_development_dependency '' 35 | end 36 | Jeweler::GemcutterTasks.new 37 | rescue LoadError 38 | puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com" 39 | end 40 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.2.1 2 | -------------------------------------------------------------------------------- /generators/db_backend.rb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshmh/globalize2/d4023fba3f4e0de42f2e911dae7a65862e49811c/generators/db_backend.rb -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /globalize2.gemspec: -------------------------------------------------------------------------------- 1 | # Generated by jeweler 2 | # DO NOT EDIT THIS FILE DIRECTLY 3 | # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command 4 | # -*- encoding: utf-8 -*- 5 | 6 | Gem::Specification.new do |s| 7 | s.name = %q{globalize2} 8 | s.version = "0.2.0" 9 | 10 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 11 | s.authors = ["Sven Fuchs, Joshua Harvey, Clemens Kofler, John-Paul Bader"] 12 | s.date = %q{2010-04-22} 13 | s.description = %q{Rails I18n: de-facto standard library for ActiveRecord data translation} 14 | s.email = %q{joshmh@gmail.com} 15 | s.extra_rdoc_files = [ 16 | "LICENSE", 17 | "README.textile" 18 | ] 19 | s.files = [ 20 | ".gitignore", 21 | "LICENSE", 22 | "README.textile", 23 | "Rakefile", 24 | "VERSION", 25 | "generators/db_backend.rb", 26 | "generators/templates/db_backend_migration.rb", 27 | "globalize2.gemspec", 28 | "init.rb", 29 | "lib/globalize.rb", 30 | "lib/globalize/active_record.rb", 31 | "lib/globalize/active_record/adapter.rb", 32 | "lib/globalize/active_record/attributes.rb", 33 | "lib/globalize/active_record/migration.rb", 34 | "lib/i18n/missing_translations_log_handler.rb", 35 | "lib/i18n/missing_translations_raise_handler.rb", 36 | "test/active_record/fallbacks_test.rb", 37 | "test/active_record/migration_test.rb", 38 | "test/active_record/sti_translated_test.rb", 39 | "test/active_record/translates_test.rb", 40 | "test/active_record/translation_class_test.rb", 41 | "test/active_record/validation_tests.rb", 42 | "test/active_record_test.rb", 43 | "test/all.rb", 44 | "test/data/models.rb", 45 | "test/data/no_globalize_schema.rb", 46 | "test/data/schema.rb", 47 | "test/i18n/missing_translations_test.rb", 48 | "test/test_helper.rb" 49 | ] 50 | s.homepage = %q{http://github.com/joshmh/globalize2} 51 | s.rdoc_options = ["--charset=UTF-8"] 52 | s.require_paths = ["lib"] 53 | s.rubygems_version = %q{1.3.6} 54 | s.summary = %q{Rails I18n: de-facto standard library for ActiveRecord data translation} 55 | s.test_files = [ 56 | "test/active_record/fallbacks_test.rb", 57 | "test/active_record/migration_test.rb", 58 | "test/active_record/sti_translated_test.rb", 59 | "test/active_record/translates_test.rb", 60 | "test/active_record/translation_class_test.rb", 61 | "test/active_record/validation_tests.rb", 62 | "test/active_record_test.rb", 63 | "test/all.rb", 64 | "test/data/models.rb", 65 | "test/data/no_globalize_schema.rb", 66 | "test/data/schema.rb", 67 | "test/i18n/missing_translations_test.rb", 68 | "test/test_helper.rb" 69 | ] 70 | 71 | if s.respond_to? :specification_version then 72 | current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION 73 | s.specification_version = 3 74 | 75 | if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then 76 | else 77 | end 78 | else 79 | end 80 | end 81 | 82 | -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 1 | require 'globalize' 2 | -------------------------------------------------------------------------------- /lib/globalize.rb: -------------------------------------------------------------------------------- 1 | module Globalize 2 | autoload :ActiveRecord, 'globalize/active_record' 3 | 4 | class << self 5 | def fallbacks? 6 | I18n.respond_to?(:fallbacks) 7 | end 8 | 9 | def fallbacks(locale) 10 | fallbacks? ? I18n.fallbacks[locale] : [locale.to_sym] 11 | end 12 | end 13 | end 14 | 15 | ActiveRecord::Base.send(:include, Globalize::ActiveRecord) 16 | -------------------------------------------------------------------------------- /lib/globalize/active_record.rb: -------------------------------------------------------------------------------- 1 | module Globalize 2 | class MigrationError < StandardError; end 3 | class MigrationMissingTranslatedField < MigrationError; end 4 | class BadMigrationFieldType < MigrationError; end 5 | 6 | module ActiveRecord 7 | autoload :Adapter, 'globalize/active_record/adapter' 8 | autoload :Attributes, 'globalize/active_record/attributes' 9 | autoload :Migration, 'globalize/active_record/migration' 10 | 11 | def self.included(base) 12 | base.extend ActMacro 13 | end 14 | 15 | class << self 16 | def build_translation_class(target, options) 17 | options[:table_name] ||= "#{target.table_name.singularize}_translations" 18 | 19 | klass = target.const_defined?(:Translation) ? 20 | target.const_get(:Translation) : 21 | target.const_set(:Translation, Class.new(::ActiveRecord::Base)) 22 | 23 | klass.class_eval do 24 | set_table_name(options[:table_name]) 25 | belongs_to target.name.underscore.gsub('/', '_') 26 | def locale; read_attribute(:locale).to_sym; end 27 | def locale=(locale); write_attribute(:locale, locale.to_s); end 28 | end 29 | 30 | klass 31 | end 32 | end 33 | 34 | module ActMacro 35 | def locale 36 | (defined?(@@locale) && @@locale) 37 | end 38 | 39 | def locale=(locale) 40 | @@locale = locale 41 | end 42 | 43 | def translates(*attr_names) 44 | return if translates? 45 | options = attr_names.extract_options! 46 | 47 | class_inheritable_accessor :translation_class, :translated_attribute_names 48 | class_inheritable_writer :required_attributes 49 | self.translation_class = ActiveRecord.build_translation_class(self, options) 50 | self.translated_attribute_names = attr_names.map(&:to_sym) 51 | 52 | include InstanceMethods 53 | extend ClassMethods, Migration 54 | 55 | after_save :save_translations! 56 | has_many :translations, :class_name => translation_class.name, 57 | :foreign_key => class_name.foreign_key, 58 | :dependent => :delete_all, 59 | :extend => HasManyExtensions 60 | 61 | named_scope :with_translations, lambda { |locale| 62 | conditions = required_attributes.map do |attribute| 63 | "#{quoted_translation_table_name}.#{attribute} IS NOT NULL" 64 | end 65 | conditions << "#{quoted_translation_table_name}.locale = ?" 66 | { :include => :translations, :conditions => [conditions.join(' AND '), locale] } 67 | } 68 | 69 | attr_names.each { |attr_name| translated_attr_accessor(attr_name) } 70 | end 71 | 72 | def translates? 73 | included_modules.include?(InstanceMethods) 74 | end 75 | end 76 | 77 | module HasManyExtensions 78 | def by_locale(locale) 79 | first(:conditions => { :locale => locale.to_s }) 80 | end 81 | 82 | def by_locales(locales) 83 | all(:conditions => { :locale => locales.map(&:to_s) }) 84 | end 85 | end 86 | 87 | module ClassMethods 88 | delegate :set_translation_table_name, :to => :translation_class 89 | 90 | def with_locale(locale) 91 | previous_locale, self.locale = self.locale, locale 92 | result = yield 93 | self.locale = previous_locale 94 | result 95 | end 96 | 97 | def translation_table_name 98 | translation_class.table_name 99 | end 100 | 101 | def quoted_translation_table_name 102 | translation_class.quoted_table_name 103 | end 104 | 105 | def required_attributes 106 | @required_attributes ||= reflect_on_all_validations.select do |validation| 107 | validation.macro == :validates_presence_of && translated_attribute_names.include?(validation.name) 108 | end.map(&:name) 109 | end 110 | 111 | def respond_to?(method, *args, &block) 112 | method.to_s =~ /^find_by_(\w+)$/ && translated_attribute_names.include?($1.to_sym) || super 113 | end 114 | 115 | def method_missing(method, *args) 116 | if method.to_s =~ /^find_by_(\w+)$/ && translated_attribute_names.include?($1.to_sym) 117 | find_first_by_translated_attr_and_locales($1, args.first) 118 | else 119 | super 120 | end 121 | end 122 | 123 | protected 124 | 125 | def find_first_by_translated_attr_and_locales(name, value) 126 | query = "#{translated_attr_name(name)} = ? AND #{translated_attr_name('locale')} IN (?)" 127 | locales = Globalize.fallbacks(locale || I18n.locale).map(&:to_s) 128 | find( 129 | :first, 130 | :joins => :translations, 131 | :conditions => [query, value, locales], 132 | :readonly => false 133 | ) 134 | end 135 | 136 | def translated_attr_accessor(name) 137 | define_method "#{name}=", lambda { |value| 138 | globalize.write(self.class.locale || I18n.locale, name, value) 139 | self[name] = value 140 | } 141 | define_method name, lambda { |*args| 142 | globalize.fetch(args.first || self.class.locale || I18n.locale, name) 143 | } 144 | alias_method "#{name}_before_type_cast", name 145 | end 146 | 147 | def translated_attr_name(name) 148 | "#{translation_class.table_name}.#{name}" 149 | end 150 | end 151 | 152 | module InstanceMethods 153 | def globalize 154 | @globalize ||= Adapter.new self 155 | end 156 | 157 | def attributes 158 | self.attribute_names.inject({}) do |attrs, name| 159 | attrs[name] = read_attribute(name) || 160 | (globalize.fetch(I18n.locale, name) rescue nil) 161 | attrs 162 | end 163 | end 164 | 165 | def attributes=(attributes, *args) 166 | if attributes.respond_to?(:delete) && locale = attributes.delete(:locale) 167 | self.class.with_locale(locale) { super } 168 | else 169 | super 170 | end 171 | end 172 | 173 | def attribute_names 174 | translated_attribute_names.map(&:to_s) + super 175 | end 176 | 177 | def available_locales 178 | translations.scoped(:select => 'DISTINCT locale').map(&:locale) 179 | end 180 | 181 | def translated_locales 182 | translations.map(&:locale) 183 | end 184 | 185 | def translated_attributes 186 | translated_attribute_names.inject({}) do |attributes, name| 187 | attributes.merge(name => send(name)) 188 | end 189 | end 190 | 191 | def set_translations(options) 192 | options.keys.each do |locale| 193 | translation = translations.find_by_locale(locale.to_s) || 194 | translations.build(:locale => locale.to_s) 195 | translation.update_attributes!(options[locale]) 196 | end 197 | end 198 | 199 | def reload(options = nil) 200 | translated_attribute_names.each { |name| @attributes.delete(name.to_s) } 201 | globalize.reset 202 | super(options) 203 | end 204 | 205 | protected 206 | 207 | def save_translations! 208 | globalize.save_translations! 209 | end 210 | end 211 | end 212 | end 213 | -------------------------------------------------------------------------------- /lib/globalize/active_record/adapter.rb: -------------------------------------------------------------------------------- 1 | module Globalize 2 | module ActiveRecord 3 | class Adapter 4 | # The cache caches attributes that already were looked up for read access. 5 | # The stash keeps track of new or changed values that need to be saved. 6 | attr_reader :record, :cache, :stash 7 | 8 | def initialize(record) 9 | @record = record 10 | @cache = Attributes.new 11 | @stash = Attributes.new 12 | end 13 | 14 | def fetch(locale, attr_name) 15 | cache.contains?(locale, attr_name) ? 16 | cache.read(locale, attr_name) : 17 | cache.write(locale, attr_name, fetch_attribute(locale, attr_name)) 18 | end 19 | 20 | def write(locale, attr_name, value) 21 | stash.write(locale, attr_name, value) 22 | cache.write(locale, attr_name, value) 23 | end 24 | 25 | def save_translations! 26 | stash.each do |locale, attrs| 27 | translation = record.translations.find_or_initialize_by_locale(locale.to_s) 28 | attrs.each { |attr_name, value| translation[attr_name] = value } 29 | translation.save! 30 | end 31 | stash.clear 32 | end 33 | 34 | def reset 35 | cache.clear 36 | # stash.clear 37 | end 38 | 39 | protected 40 | 41 | def fetch_translation(locale) 42 | locale = locale.to_sym 43 | record.translations.loaded? ? record.translations.detect { |t| t.locale == locale } : 44 | record.translations.by_locale(locale) 45 | end 46 | 47 | def fetch_translations(locale) 48 | # only query if not already included with :include => translations 49 | record.translations.loaded? ? record.translations : 50 | record.translations.by_locales(Globalize.fallbacks(locale)) 51 | end 52 | 53 | def fetch_attribute(locale, attr_name) 54 | translations = fetch_translations(locale) 55 | value, requested_locale = nil, locale 56 | 57 | Globalize.fallbacks(locale).each do |fallback| 58 | translation = translations.detect { |t| t.locale == fallback } 59 | value = translation && translation.send(attr_name) 60 | locale = fallback && break if value 61 | end 62 | 63 | set_metadata(value, :locale => locale, :requested_locale => requested_locale) 64 | value 65 | end 66 | 67 | def set_metadata(object, metadata) 68 | if object.respond_to?(:translation_metadata) 69 | object.translation_metadata.merge!(meta_data) 70 | end 71 | end 72 | 73 | def translation_metadata_accessor(object) 74 | return if obj.respond_to?(:translation_metadata) 75 | class << object; attr_accessor :translation_metadata end 76 | object.translation_metadata ||= {} 77 | end 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/globalize/active_record/attributes.rb: -------------------------------------------------------------------------------- 1 | # Helper class for storing values per locale. Used by Globalize::Adapter 2 | # to stash and cache attribute values. 3 | module Globalize 4 | module ActiveRecord 5 | class Attributes < Hash 6 | def [](locale) 7 | locale = locale.to_sym 8 | self[locale] = {} unless has_key?(locale) 9 | self.fetch(locale) 10 | end 11 | 12 | def contains?(locale, attr_name) 13 | self[locale].has_key?(attr_name) 14 | end 15 | 16 | def read(locale, attr_name) 17 | self[locale][attr_name] 18 | end 19 | 20 | def write(locale, attr_name, value) 21 | self[locale][attr_name] = value 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/globalize/active_record/migration.rb: -------------------------------------------------------------------------------- 1 | module Globalize 2 | module ActiveRecord 3 | module Migration 4 | def create_translation_table!(fields) 5 | translated_attribute_names.each do |f| 6 | raise MigrationMissingTranslatedField, "Missing translated field #{f}" unless fields[f] 7 | end 8 | 9 | fields.each do |name, type| 10 | if translated_attribute_names.include?(name) && ![:string, :text].include?(type) 11 | raise BadMigrationFieldType, "Bad field type for #{name}, should be :string or :text" 12 | end 13 | end 14 | 15 | self.connection.create_table(translation_table_name) do |t| 16 | t.references table_name.sub(/^#{table_name_prefix}/, "").singularize 17 | t.string :locale 18 | fields.each do |name, type| 19 | t.column name, type 20 | end 21 | t.timestamps 22 | end 23 | 24 | self.connection.add_index( 25 | translation_table_name, 26 | "#{table_name.sub(/^#{table_name_prefix}/, "").singularize}_id", 27 | :name => translation_index_name 28 | ) 29 | end 30 | 31 | def translation_index_name 32 | require 'digest/sha1' 33 | # FIXME what's the max size of an index name? 34 | index_name = "index_#{translation_table_name}_on_#{self.table_name.singularize}_id" 35 | index_name.size < 50 ? index_name : "index_#{Digest::SHA1.hexdigest(index_name)}" 36 | end 37 | 38 | def drop_translation_table! 39 | self.connection.remove_index(translation_table_name, :name => translation_index_name) rescue nil 40 | self.connection.drop_table(translation_table_name) 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/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 42 | -------------------------------------------------------------------------------- /lib/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 | end 17 | 18 | I18n.exception_handler = :missing_translations_raise_handler 19 | 20 | ActionView::Helpers::TranslationHelper.module_eval do 21 | def translate(key, options = {}) 22 | I18n.translate(key, options) 23 | end 24 | alias :t :translate 25 | end 26 | -------------------------------------------------------------------------------- /test/active_record/fallbacks_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/../test_helper') 2 | require File.expand_path(File.dirname(__FILE__) + '/../data/models') 3 | 4 | if I18n.respond_to?(:fallbacks) 5 | class TranslatedTest < ActiveSupport::TestCase 6 | def setup 7 | I18n.locale = :'en-US' 8 | I18n.fallbacks.clear 9 | reset_db! 10 | ActiveRecord::Base.locale = nil 11 | end 12 | 13 | def teardown 14 | I18n.fallbacks.clear 15 | end 16 | 17 | test "keeping one field in new locale when other field is changed" do 18 | I18n.fallbacks.map 'de-DE' => [ 'en-US' ] 19 | post = Post.create :subject => 'foo' 20 | I18n.locale = 'de-DE' 21 | post.content = 'bar' 22 | assert_equal 'foo', post.subject 23 | end 24 | 25 | test "modifying non-required field in a new locale" do 26 | I18n.fallbacks.map 'de-DE' => [ 'en-US' ] 27 | post = Post.create :subject => 'foo' 28 | I18n.locale = 'de-DE' 29 | post.content = 'bar' 30 | assert post.save 31 | end 32 | 33 | test "resolves a simple fallback" do 34 | I18n.locale = 'de-DE' 35 | post = Post.create :subject => 'foo' 36 | I18n.locale = 'de' 37 | post.subject = 'baz' 38 | post.content = 'bar' 39 | post.save 40 | I18n.locale = 'de-DE' 41 | assert_equal 'foo', post.subject 42 | assert_equal 'bar', post.content 43 | end 44 | 45 | test "resolves a simple fallback without reloading" do 46 | I18n.locale = 'de-DE' 47 | post = Post.new :subject => 'foo' 48 | I18n.locale = 'de' 49 | post.subject = 'baz' 50 | post.content = 'bar' 51 | I18n.locale = 'de-DE' 52 | assert_equal 'foo', post.subject 53 | assert_equal 'bar', post.content 54 | end 55 | 56 | test "resolves a complex fallback without reloading" do 57 | I18n.fallbacks.map 'de' => %w(en he) 58 | I18n.locale = 'de' 59 | post = Post.new 60 | I18n.locale = 'en' 61 | post.subject = 'foo' 62 | I18n.locale = 'he' 63 | post.subject = 'baz' 64 | post.content = 'bar' 65 | I18n.locale = 'de' 66 | assert_equal 'foo', post.subject 67 | assert_equal 'bar', post.content 68 | end 69 | 70 | test 'fallbacks with lots of locale switching' do 71 | I18n.fallbacks.map :'de-DE' => [ :'en-US' ] 72 | post = Post.create :subject => 'foo' 73 | 74 | I18n.locale = :'de-DE' 75 | assert_equal 'foo', post.subject 76 | 77 | I18n.locale = :'en-US' 78 | post.update_attribute :subject, 'bar' 79 | 80 | I18n.locale = :'de-DE' 81 | assert_equal 'bar', post.subject 82 | end 83 | 84 | test 'fallbacks with lots of locale switching' do 85 | I18n.fallbacks.map :'de-DE' => [ :'en-US' ] 86 | child = Child.create :content => 'foo' 87 | 88 | I18n.locale = :'de-DE' 89 | assert_equal 'foo', child.content 90 | 91 | I18n.locale = :'en-US' 92 | child.update_attribute :content, 'bar' 93 | 94 | I18n.locale = :'de-DE' 95 | assert_equal 'bar', child.content 96 | end 97 | end 98 | end 99 | 100 | # TODO should validate_presence_of take fallbacks into account? maybe we need 101 | # an extra validation call, or more options for validate_presence_of. 102 | 103 | -------------------------------------------------------------------------------- /test/active_record/migration_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/../test_helper') 2 | require File.expand_path(File.dirname(__FILE__) + '/../data/models') 3 | 4 | class MigrationTest < ActiveSupport::TestCase 5 | def setup 6 | reset_db! 7 | Post.drop_translation_table! 8 | end 9 | 10 | test 'globalize table added' do 11 | assert !Post.connection.table_exists?(:post_translations) 12 | assert !Post.connection.index_exists?(:post_translations, :post_id) 13 | 14 | Post.create_translation_table!(:subject => :string, :content => :text) 15 | assert Post.connection.table_exists?(:post_translations) 16 | assert Post.connection.index_exists?(:post_translations, :post_id) 17 | 18 | columns = Post.connection.columns(:post_translations) 19 | assert locale = columns.detect { |c| c.name == 'locale' } 20 | assert_equal :string, locale.type 21 | assert subject = columns.detect { |c| c.name == 'subject' } 22 | assert_equal :string, subject.type 23 | assert content = columns.detect { |c| c.name == 'content' } 24 | assert_equal :text, content.type 25 | assert post_id = columns.detect { |c| c.name == 'post_id' } 26 | assert_equal :integer, post_id.type 27 | assert created_at = columns.detect { |c| c.name == 'created_at' } 28 | assert_equal :datetime, created_at.type 29 | assert updated_at = columns.detect { |c| c.name == 'updated_at' } 30 | assert_equal :datetime, updated_at.type 31 | end 32 | 33 | test 'globalize table dropped' do 34 | assert !Post.connection.table_exists?( :post_translations ) 35 | assert !Post.connection.index_exists?( :post_translations, :post_id ) 36 | Post.create_translation_table! :subject => :string, :content => :text 37 | assert Post.connection.table_exists?( :post_translations ) 38 | assert Post.connection.index_exists?( :post_translations, :post_id ) 39 | Post.drop_translation_table! 40 | assert !Post.connection.table_exists?( :post_translations ) 41 | assert !Post.connection.index_exists?( :post_translations, :post_id ) 42 | end 43 | 44 | test 'exception on missing field inputs' do 45 | assert_raise Globalize::MigrationMissingTranslatedField do 46 | Post.create_translation_table! :content => :text 47 | end 48 | end 49 | 50 | test 'exception on bad input type' do 51 | assert_raise Globalize::BadMigrationFieldType do 52 | Post.create_translation_table! :subject => :string, :content => :integer 53 | end 54 | end 55 | 56 | test "exception on bad input type isn't raised for untranslated fields" do 57 | assert_nothing_raised do 58 | Post.create_translation_table! :subject => :string, :content => :string, :views_count => :integer 59 | end 60 | end 61 | 62 | test 'create_translation_table! should not be called on non-translated models' do 63 | assert_raise NoMethodError do 64 | Blog.create_translation_table! :name => :string 65 | end 66 | end 67 | 68 | test 'drop_translation_table! should not be called on non-translated models' do 69 | assert_raise NoMethodError do 70 | Blog.drop_translation_table! 71 | end 72 | end 73 | 74 | test "translation_index_name returns a readable index name when it's not longer than 50 characters" do 75 | assert_equal 'index_post_translations_on_post_id', Post.send(:translation_index_name) 76 | end 77 | 78 | test "translation_index_name returns a hashed index name when it's longer than 50 characters" do 79 | class UltraLongModelNameWithoutProper < ActiveRecord::Base 80 | translates :foo 81 | end 82 | name = UltraLongModelNameWithoutProper.send(:translation_index_name) 83 | assert_match /^index_[a-z0-9]{40}$/, name 84 | end 85 | 86 | test 'globalize table added when table has long name' do 87 | UltraLongModelNameWithoutProper.create_translation_table!( 88 | :subject => :string, :content => :text 89 | ) 90 | 91 | assert UltraLongModelNameWithoutProper.connection.table_exists?( 92 | :ultra_long_model_name_without_proper_translations 93 | ) 94 | assert UltraLongModelNameWithoutProper.connection.index_exists?( 95 | :ultra_long_model_name_without_proper_translations, 96 | :name => UltraLongModelNameWithoutProper.send( 97 | :translation_index_name 98 | ) 99 | ) 100 | end 101 | 102 | test 'globalize table dropped when table has long name' do 103 | UltraLongModelNameWithoutProper.drop_translation_table! 104 | UltraLongModelNameWithoutProper.create_translation_table!( 105 | :subject => :string, :content => :text 106 | ) 107 | UltraLongModelNameWithoutProper.drop_translation_table! 108 | 109 | assert !UltraLongModelNameWithoutProper.connection.table_exists?( 110 | :ultra_long_model_name_without_proper_translations 111 | ) 112 | assert !UltraLongModelNameWithoutProper.connection.index_exists?( 113 | :ultra_long_model_name_without_proper_translations, 114 | :ultra_long_model_name_without_proper_id 115 | ) 116 | end 117 | 118 | end 119 | -------------------------------------------------------------------------------- /test/active_record/sti_translated_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/../test_helper') 2 | require File.expand_path(File.dirname(__FILE__) + '/../data/models') 3 | 4 | class StiTranslatedTest < ActiveSupport::TestCase 5 | def setup 6 | I18n.locale = :'en-US' 7 | reset_db! 8 | end 9 | 10 | test "works with simple dynamic finders" do 11 | foo = Child.create :content => 'foo' 12 | Child.create :content => 'bar' 13 | child = Child.find_by_content('foo') 14 | assert_equal foo, child 15 | end 16 | 17 | test 'change attribute on globalized model' do 18 | child = Child.create :content => 'foo' 19 | assert_equal [], child.changed 20 | child.content = 'bar' 21 | assert_equal [ 'content' ], child.changed 22 | child.content = 'baz' 23 | assert_member 'content', child.changed 24 | end 25 | 26 | test 'change attribute on globalized model after locale switching' do 27 | child = Child.create :content => 'foo' 28 | assert_equal [], child.changed 29 | child.content = 'bar' 30 | I18n.locale = :de 31 | assert_equal [ 'content' ], child.changed 32 | end 33 | 34 | test "saves all locales, even after locale switching" do 35 | child = Child.new :content => 'foo' 36 | I18n.locale = 'de-DE' 37 | child.content = 'bar' 38 | I18n.locale = 'he-IL' 39 | child.content = 'baz' 40 | child.save 41 | I18n.locale = 'en-US' 42 | child = Child.first 43 | assert_equal 'foo', child.content 44 | I18n.locale = 'de-DE' 45 | assert_equal 'bar', child.content 46 | I18n.locale = 'he-IL' 47 | assert_equal 'baz', child.content 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /test/active_record/translates_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/../test_helper') 2 | require File.expand_path(File.dirname(__FILE__) + '/../data/models') 3 | 4 | class TranslatesTest < ActiveSupport::TestCase 5 | def setup 6 | I18n.locale = nil 7 | ActiveRecord::Base.locale = nil 8 | reset_db! 9 | end 10 | 11 | test 'defines a :locale accessors on ActiveRecord::Base' do 12 | ActiveRecord::Base.locale = :de 13 | assert_equal :de, ActiveRecord::Base.locale 14 | end 15 | 16 | test 'the :locale reader on ActiveRecord::Base does not default to I18n.locale (anymore)' do 17 | I18n.locale = :en 18 | assert_nil ActiveRecord::Base.locale 19 | end 20 | 21 | test 'ActiveRecord::Base.with_locale temporarily sets the given locale and yields the block' do 22 | I18n.locale = :en 23 | post = Post.with_locale(:de) do 24 | Post.create!(:subject => 'Titel', :content => 'Inhalt') 25 | end 26 | assert_nil Post.locale 27 | assert_equal :en, I18n.locale 28 | 29 | I18n.locale = :de 30 | assert_equal 'Titel', post.subject 31 | end 32 | 33 | test 'translation_class returns the Translation class' do 34 | assert_equal Post::Translation, Post.translation_class 35 | end 36 | 37 | test 'defines a has_many association on the model class' do 38 | assert_has_many Post, :translations 39 | end 40 | 41 | test 'defines a scope for retrieving locales that have complete translations' do 42 | post = Post.create!(:subject => 'subject', :content => 'content') 43 | assert_equal [:en], post.translated_locales 44 | end 45 | 46 | test 'sets the given attributes to translated_attribute_names' do 47 | assert_equal [:subject, :content], Post.translated_attribute_names 48 | end 49 | 50 | test 'defines accessors for the translated attributes' do 51 | post = Post.new 52 | assert post.respond_to?(:subject) 53 | assert post.respond_to?(:subject=) 54 | end 55 | 56 | test 'attribute reader without arguments will use the current locale on ActiveRecord::Base or I18n' do 57 | post = Post.with_locale(:de) do 58 | Post.create!(:subject => 'Titel', :content => 'Inhalt') 59 | end 60 | I18n.locale = :de 61 | assert_equal 'Titel', post.subject 62 | 63 | I18n.locale = :en 64 | ActiveRecord::Base.locale = :de 65 | assert_equal 'Titel', post.subject 66 | end 67 | 68 | test 'attribute reader when passed a locale will use the given locale' do 69 | post = Post.with_locale(:de) do 70 | Post.create!(:subject => 'Titel', :content => 'Inhalt') 71 | end 72 | assert_equal 'Titel', post.subject(:de) 73 | end 74 | 75 | test 'attribute reader will use the current locale on ActiveRecord::Base or I18n' do 76 | post = Post.with_locale(:en) do 77 | Post.create!(:subject => 'title', :content => 'content') 78 | end 79 | I18n.locale = :de 80 | post.subject = 'Titel' 81 | assert_equal 'Titel', post.subject 82 | 83 | ActiveRecord::Base.locale = :en 84 | post.subject = 'title' 85 | assert_equal 'title', post.subject 86 | end 87 | 88 | test "find_by_xx records have writable attributes" do 89 | Post.create :subject => "change me" 90 | p = Post.find_by_subject("change me") 91 | p.subject = "changed" 92 | assert_nothing_raised(ActiveRecord::ReadOnlyRecord) do 93 | p.save 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /test/active_record/translation_class_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/../test_helper') 2 | require File.expand_path(File.dirname(__FILE__) + '/../data/models') 3 | 4 | class TranlationClassTest < ActiveSupport::TestCase 5 | def setup 6 | reset_db! 7 | end 8 | 9 | test 'defines a Translation class nested in the model class' do 10 | assert Post.const_defined?(:Translation) 11 | end 12 | 13 | test 'defines a belongs_to association' do 14 | assert_belongs_to Post::Translation, :post 15 | end 16 | 17 | test 'defines a reader for :locale that always returns a symbol' do 18 | post = Post::Translation.new 19 | post.write_attribute('locale', 'de') 20 | assert_equal :de, post.locale 21 | end 22 | 23 | test 'defines a write for :locale that always writes a string' do 24 | post = Post::Translation.new 25 | post.locale = :de 26 | assert_equal 'de', post.read_attribute('locale') 27 | end 28 | end 29 | 30 | 31 | -------------------------------------------------------------------------------- /test/active_record/validation_tests.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/../test_helper') 2 | require File.expand_path(File.dirname(__FILE__) + '/../data/models') 3 | 4 | class ValidationTest < ActiveSupport::TestCase 5 | def setup 6 | reset_db! 7 | end 8 | 9 | def teardown 10 | Validatee.instance_variable_set(:@validate_callbacks, CallbackChain.new) 11 | end 12 | 13 | test "validates_presence_of" do 14 | Validatee.class_eval { validates_presence_of :string } 15 | assert !Validatee.new.valid? 16 | assert Validatee.new(:string => 'foo').valid? 17 | end 18 | 19 | test "validates_confirmation_of" do 20 | Validatee.class_eval { validates_confirmation_of :string } 21 | assert !Validatee.new(:string => 'foo', :string_confirmation => 'bar').valid? 22 | assert Validatee.new(:string => 'foo', :string_confirmation => 'foo').valid? 23 | end 24 | 25 | test "validates_acceptance_of" do 26 | Validatee.class_eval { validates_acceptance_of :string, :accept => '1' } 27 | assert !Validatee.new(:string => '0').valid? 28 | assert Validatee.new(:string => '1').valid? 29 | end 30 | 31 | test "validates_length_of (:is)" do 32 | Validatee.class_eval { validates_length_of :string, :is => 1 } 33 | assert !Validatee.new(:string => 'aa').valid? 34 | assert Validatee.new(:string => 'a').valid? 35 | end 36 | 37 | test "validates_format_of" do 38 | Validatee.class_eval { validates_format_of :string, :with => /^\d+$/ } 39 | assert !Validatee.new(:string => 'a').valid? 40 | assert Validatee.new(:string => '1').valid? 41 | end 42 | 43 | test "validates_inclusion_of" do 44 | Validatee.class_eval { validates_inclusion_of :string, :in => %(a) } 45 | assert !Validatee.new(:string => 'b').valid? 46 | assert Validatee.new(:string => 'a').valid? 47 | end 48 | 49 | test "validates_exclusion_of" do 50 | Validatee.class_eval { validates_exclusion_of :string, :in => %(b) } 51 | assert !Validatee.new(:string => 'b').valid? 52 | assert Validatee.new(:string => 'a').valid? 53 | end 54 | 55 | test "validates_numericality_of" do 56 | Validatee.class_eval { validates_numericality_of :string } 57 | assert !Validatee.new(:string => 'a').valid? 58 | assert Validatee.new(:string => '1').valid? 59 | end 60 | 61 | # This doesn't pass and Rails' validates_uniqueness_of implementation doesn't 62 | # seem to be extensible easily. One can work around that by either defining 63 | # a custom validation on the Validatee model itself, or by using validates_uniqueness_of 64 | # on Validatee::Translation. 65 | # 66 | # test "validates_uniqueness_of" do 67 | # Validatee.class_eval { validates_uniqueness_of :string } 68 | # Validatee.create!(:string => 'a') 69 | # assert !Validatee.new(:string => 'a').valid? 70 | # assert Validatee.new(:string => 'b').valid? 71 | # end 72 | 73 | # test "validates_associated" do 74 | # end 75 | end -------------------------------------------------------------------------------- /test/active_record_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/test_helper') 2 | require File.expand_path(File.dirname(__FILE__) + '/data/models') 3 | 4 | # Higher level tests. 5 | 6 | class ActiveRecordTest < ActiveSupport::TestCase 7 | def setup 8 | I18n.locale = :en 9 | reset_db! 10 | ActiveRecord::Base.locale = nil 11 | end 12 | 13 | def assert_translated(locale, record, names, expected) 14 | I18n.locale = locale 15 | assert_equal Array(expected), Array(names).map { |name| record.send(name) } 16 | end 17 | 18 | test "a translated record has translations" do 19 | assert_equal [], Post.new.translations 20 | end 21 | 22 | test "saves a translated version of the record for each locale" do 23 | post = Post.create(:subject => 'title') 24 | I18n.locale = :de 25 | post.update_attributes(:subject => 'Titel') 26 | 27 | assert_equal 2, post.translations.size 28 | assert_equal %w(de en), post.translations.map(&:locale).map(&:to_s).sort 29 | assert_equal %w(Titel title), post.translations.map(&:subject).sort 30 | end 31 | 32 | test "a translated record has German translations" do 33 | I18n.locale = :de 34 | post = Post.create(:subject => 'foo') 35 | assert_equal 1, post.translations.size 36 | assert_equal [:de], post.translations.map { |t| t.locale } 37 | end 38 | 39 | test "modifiying translated fields while switching locales" do 40 | post = Post.create(:subject => 'title', :content => 'content') 41 | assert_equal %w(title content), [post.subject, post.content] 42 | 43 | I18n.locale = :de 44 | post.subject, post.content = 'Titel', 'Inhalt' 45 | 46 | assert_translated(:de, post, [:subject, :content], %w(Titel Inhalt)) 47 | assert_translated(:en, post, [:subject, :content], %w(title content)) 48 | assert_translated(:de, post, [:subject, :content], %w(Titel Inhalt)) 49 | 50 | post.save 51 | post.reload 52 | 53 | assert_translated(:en, post, [:subject, :content], %w(title content)) 54 | assert_translated(:de, post, [:subject, :content], %w(Titel Inhalt)) 55 | end 56 | 57 | test "attribute writers do return their argument" do 58 | value = Post.new.subject = 'foo' 59 | assert_equal 'foo', value 60 | end 61 | 62 | test "update_attribute succeeds with valid values" do 63 | post = Post.create(:subject => 'foo', :content => 'bar') 64 | post.update_attribute(:subject, 'baz') 65 | assert_equal 'baz', Post.first.subject 66 | end 67 | 68 | test "update_attributes fails with invalid values" do 69 | post = Post.create(:subject => 'foo', :content => 'bar') 70 | assert !post.update_attributes(:subject => '') 71 | assert_not_nil post.reload.attributes['subject'] 72 | assert_equal 'foo', post.subject 73 | end 74 | 75 | test "passing the locale to create uses the given locale" do 76 | post = Post.create(:subject => 'Titel', :content => 'Inhalt', :locale => :de) 77 | assert_equal :en, I18n.locale 78 | assert_nil ActiveRecord::Base.locale 79 | 80 | I18n.locale = :de 81 | assert_equal 'Titel', post.subject 82 | end 83 | 84 | test "passing the locale to attributes= uses the given locale" do 85 | post = Post.create(:subject => 'title', :content => 'content') 86 | post.update_attributes(:subject => 'Titel', :content => 'Inhalt', :locale => :de) 87 | post.reload 88 | 89 | assert_equal :en, I18n.locale 90 | assert_nil ActiveRecord::Base.locale 91 | 92 | assert_equal 'title', post.subject 93 | I18n.locale = :de 94 | assert_equal 'Titel', post.subject 95 | end 96 | 97 | test 'reload works' do 98 | post = Post.create(:subject => 'foo', :content => 'bar') 99 | post.subject = 'baz' 100 | post.reload 101 | assert_equal 'foo', post.subject 102 | end 103 | 104 | test "returns nil if no translations are found (unsaved record)" do 105 | post = Post.new(:subject => 'foo') 106 | assert_equal 'foo', post.subject 107 | assert_nil post.content 108 | end 109 | 110 | test "returns nil if no translations are found (saved record)" do 111 | post = Post.create(:subject => 'foo') 112 | post.reload 113 | assert_equal 'foo', post.subject 114 | assert_nil post.content 115 | end 116 | 117 | test "finds a German post" do 118 | post = Post.create(:subject => 'foo (en)', :content => 'bar') 119 | I18n.locale = :de 120 | post = Post.first 121 | post.subject = 'baz (de)' 122 | post.save 123 | assert_equal 'baz (de)', Post.first.subject 124 | I18n.locale = :en 125 | assert_equal 'foo (en)', Post.first.subject 126 | end 127 | 128 | test "saves an English post and loads correctly" do 129 | post = Post.create(:subject => 'foo', :content => 'bar') 130 | assert post.save 131 | post = Post.first 132 | assert_equal 'foo', post.subject 133 | assert_equal 'bar', post.content 134 | end 135 | 136 | test "returns the value for the correct locale, after locale switching" do 137 | post = Post.create(:subject => 'foo') 138 | I18n.locale = :de 139 | post.subject = 'bar' 140 | post.save 141 | I18n.locale = :en 142 | post = Post.first 143 | assert_equal 'foo', post.subject 144 | I18n.locale = :de 145 | assert_equal 'bar', post.subject 146 | end 147 | 148 | test "returns the value for the correct locale, after locale switching, without saving" do 149 | post = Post.create :subject => 'foo' 150 | I18n.locale = :de 151 | post.subject = 'bar' 152 | I18n.locale = :en 153 | assert_equal 'foo', post.subject 154 | I18n.locale = :de 155 | assert_equal 'bar', post.subject 156 | end 157 | 158 | test "saves all locales, even after locale switching" do 159 | post = Post.new :subject => 'foo' 160 | I18n.locale = :de 161 | post.subject = 'bar' 162 | I18n.locale = :he 163 | post.subject = 'baz' 164 | post.save 165 | I18n.locale = :en 166 | post = Post.first 167 | assert_equal 'foo', post.subject 168 | I18n.locale = :de 169 | assert_equal 'bar', post.subject 170 | I18n.locale = :he 171 | assert_equal 'baz', post.subject 172 | end 173 | 174 | test "works with associations" do 175 | blog = Blog.create 176 | post1 = blog.posts.create(:subject => 'foo') 177 | 178 | I18n.locale = :de 179 | post2 = blog.posts.create(:subject => 'bar') 180 | assert_equal 2, blog.posts.size 181 | 182 | I18n.locale = :en 183 | assert_equal 'foo', blog.posts.first.subject 184 | assert_nil blog.posts.last.subject 185 | 186 | I18n.locale = :de 187 | assert_equal 'bar', blog.posts.last.subject 188 | end 189 | 190 | test "works with simple dynamic finders" do 191 | foo = Post.create(:subject => 'foo') 192 | Post.create(:subject => 'bar') 193 | post = Post.find_by_subject('foo') 194 | assert_equal foo, post 195 | end 196 | 197 | test 'change attribute on globalized model' do 198 | post = Post.create(:subject => 'foo', :content => 'bar') 199 | assert_equal [], post.changed 200 | post.subject = 'baz' 201 | assert_equal ['subject'], post.changed 202 | post.content = 'quux' 203 | assert_member 'subject', post.changed 204 | assert_member 'content', post.changed 205 | end 206 | 207 | test 'change attribute on globalized model after locale switching' do 208 | post = Post.create(:subject => 'foo', :content => 'bar') 209 | assert_equal [], post.changed 210 | post.subject = 'baz' 211 | I18n.locale = :de 212 | assert_equal ['subject'], post.changed 213 | end 214 | 215 | test 'complex writing and stashing' do 216 | post = Post.create(:subject => 'foo', :content => 'bar') 217 | post.subject = nil 218 | assert_nil post.subject 219 | assert !post.valid? 220 | post.subject = 'stashed_foo' 221 | assert_equal 'stashed_foo', post.subject 222 | end 223 | 224 | test 'translated class locale setting' do 225 | assert ActiveRecord::Base.respond_to?(:locale) 226 | assert_equal :en, I18n.locale 227 | assert_nil ActiveRecord::Base.locale 228 | 229 | I18n.locale = :de 230 | assert_equal :de, I18n.locale 231 | assert_nil ActiveRecord::Base.locale 232 | 233 | ActiveRecord::Base.locale = :es 234 | assert_equal :de, I18n.locale 235 | assert_equal :es, ActiveRecord::Base.locale 236 | 237 | I18n.locale = :fr 238 | assert_equal :fr, I18n.locale 239 | assert_equal :es, ActiveRecord::Base.locale 240 | end 241 | 242 | test "untranslated class responds to locale" do 243 | assert Blog.respond_to?(:locale) 244 | end 245 | 246 | test "to ensure locales in different classes are the same" do 247 | ActiveRecord::Base.locale = :de 248 | assert_equal :de, ActiveRecord::Base.locale 249 | assert_equal :de, Parent.locale 250 | 251 | Parent.locale = :es 252 | assert_equal :es, ActiveRecord::Base.locale 253 | assert_equal :es, Parent.locale 254 | end 255 | 256 | test "attribute saving goes by content locale and not global locale" do 257 | ActiveRecord::Base.locale = :de 258 | assert_equal :en, I18n.locale 259 | Post.create :subject => 'foo' 260 | assert_equal :de, Post.first.translations.first.locale 261 | end 262 | 263 | test "attribute loading goes by content locale and not global locale" do 264 | post = Post.create(:subject => 'foo') 265 | assert_nil ActiveRecord::Base.locale 266 | 267 | ActiveRecord::Base.locale = :de 268 | assert_equal :en, I18n.locale 269 | post.update_attribute(:subject, 'foo [de]') 270 | assert_equal 'foo [de]', Post.first.subject 271 | 272 | ActiveRecord::Base.locale = :en 273 | assert_equal 'foo', Post.first.subject 274 | end 275 | 276 | test "access content locale before setting" do 277 | Globalize::ActiveRecord::ActMacro.class_eval "remove_class_variable(:@@locale)" 278 | assert_nothing_raised { ActiveRecord::Base.locale } 279 | end 280 | 281 | test "available_locales" do 282 | Post.locale = :de 283 | post = Post.create(:subject => 'foo') 284 | Post.locale = :es 285 | post.update_attribute(:subject, 'bar') 286 | Post.locale = :fr 287 | post.update_attribute(:subject, 'baz') 288 | assert_equal [:de, :es, :fr], post.available_locales 289 | assert_equal [:de, :es, :fr], Post.first.available_locales 290 | end 291 | 292 | test "saving record correctly after post-save reload" do 293 | reloader = Reloader.create(:content => 'foo') 294 | assert_equal 'foo', reloader.content 295 | end 296 | 297 | test "including translations" do 298 | I18n.locale = :de 299 | Post.create(:subject => "Foo1", :content => "Bar1") 300 | Post.create(:subject => "Foo2", :content => "Bar2") 301 | 302 | class << Post 303 | def translations_included 304 | self.all(:include => :translations) 305 | end 306 | end 307 | 308 | default = Post.all.map { |x| [x.subject, x.content] } 309 | with_include = Post.translations_included.map { |x| [x.subject, x.content] } 310 | assert_equal default, with_include 311 | end 312 | 313 | test "setting multiple translations at once with options hash" do 314 | Post.locale = :de 315 | post = Post.create(:subject => "foo1", :content => "foo1") 316 | Post.locale = :en 317 | post.update_attributes(:subject => "bar1", :content => "bar1") 318 | 319 | options = { :de => {:subject => "foo2", :content => "foo2"}, 320 | :en => {:subject => "bar2", :content => "bar2"} } 321 | post.set_translations options 322 | post.reload 323 | 324 | assert ["bar2", "bar2"], [post.subject, post.content] 325 | Post.locale = :de 326 | assert ["foo2", "foo2"], [post.subject, post.content] 327 | end 328 | 329 | test "setting only one translation with set_translations" do 330 | Post.locale = :de 331 | post = Post.create(:subject => "foo1", :content => "foo1") 332 | Post.locale = :en 333 | post.update_attributes(:subject => "bar1", :content => "bar1") 334 | 335 | options = { :en => { :subject => "bar2", :content => "bar2" } } 336 | post.set_translations options 337 | post.reload 338 | 339 | assert ["bar2", "bar2"], [post.subject, post.content] 340 | Post.locale = :de 341 | assert ["foo1", "foo1"], [post.subject, post.content] 342 | end 343 | 344 | test "setting only selected attributes with set_translations" do 345 | Post.locale = :de 346 | post = Post.create(:subject => "foo1", :content => "foo1") 347 | Post.locale = :en 348 | post.update_attributes(:subject => "bar1", :content => "bar1") 349 | 350 | options = { :de => { :content => "foo2" }, :en => { :subject => "bar2" } } 351 | post.set_translations options 352 | post.reload 353 | 354 | assert ["bar2", "bar1"], [post.subject, post.content] 355 | Post.locale = :de 356 | assert ["foo1", "foo2"], [post.subject, post.content] 357 | end 358 | 359 | test "setting invalid attributes raises ArgumentError" do 360 | Post.locale = :de 361 | post = Post.create(:subject => "foo1", :content => "foo1") 362 | Post.locale = :en 363 | post.update_attributes(:subject => "bar1", :content => "bar1") 364 | 365 | options = { :de => {:fake => "foo2"} } 366 | exception = assert_raise(ActiveRecord::UnknownAttributeError) do 367 | post.set_translations options 368 | end 369 | assert_equal "unknown attribute: fake", exception.message 370 | end 371 | 372 | test "reload accepting find options" do 373 | p = Post.create(:subject => "Foo", :content => "Bar") 374 | assert p.reload(:readonly => true, :lock => true) 375 | assert_raise(ArgumentError) { p.reload(:foo => :bar) } 376 | end 377 | 378 | test "dependent destroy of translation" do 379 | p = Post.create(:subject => "Foo", :content => "Bar") 380 | assert_equal 1, PostTranslation.count 381 | p.destroy 382 | assert_equal 0, PostTranslation.count 383 | end 384 | 385 | test "translating subclass of untranslated comment model" do 386 | translated_comment = TranslatedComment.create(:post => @post) 387 | assert_nothing_raised { translated_comment.translations } 388 | end 389 | 390 | test "modifiying translated comments works as expected" do 391 | I18n.locale = :en 392 | translated_comment = TranslatedComment.create(:post => @post, :content => 'foo') 393 | assert_equal 'foo', translated_comment.content 394 | 395 | I18n.locale = :de 396 | translated_comment.content = 'bar' 397 | assert translated_comment.save 398 | assert_equal 'bar', translated_comment.content 399 | 400 | I18n.locale = :en 401 | assert_equal 'foo', translated_comment.content 402 | 403 | assert_equal 2, translated_comment.translations.size 404 | end 405 | 406 | test "can create a proxy class for a namespaced model" do 407 | assert_nothing_raised do 408 | module Foo 409 | module Bar 410 | class Baz < ActiveRecord::Base 411 | translates :bumm 412 | end 413 | end 414 | end 415 | end 416 | end 417 | 418 | test "attribute translated before type cast" do 419 | Post.locale = :en 420 | post = Post.create(:subject => 'foo', :content => 'bar') 421 | Post.locale = :de 422 | post.update_attribute(:subject, "German foo") 423 | assert_equal 'German foo', post.subject_before_type_cast 424 | Post.locale = :en 425 | assert_equal 'foo', post.subject_before_type_cast 426 | end 427 | 428 | test "don't override existing translation class" do 429 | assert PostTranslation.new.respond_to?(:existing_method) 430 | end 431 | 432 | test "has_many and named scopes work with globalize" do 433 | blog = Blog.create 434 | assert_nothing_raised { blog.posts.foobar } 435 | end 436 | 437 | test "required_attribuets don't include non-translated attributes" do 438 | validations = [ 439 | stub(:name => :name, :macro => :validates_presence_of), 440 | stub(:name => :email, :macro => :validates_presence_of) 441 | ] 442 | User.expects(:reflect_on_all_validations => validations) 443 | assert_equal [:name], User.required_attributes 444 | end 445 | 446 | test "attribute_names returns translated and regular attribute names" do 447 | Post.create :subject => "foo", :content => "bar" 448 | assert_equal Post.last.attribute_names.sort, %w[blog_id content id subject] 449 | end 450 | 451 | test "attributes returns translated and regular attributes" do 452 | Post.create :subject => "foo", :content => "bar" 453 | assert_equal Post.last.attributes.keys.sort, %w[blog_id content id subject] 454 | end 455 | 456 | test "to_xml includes translated fields" do 457 | Post.create :subject => "foo", :content => "bar" 458 | assert Post.last.to_xml =~ /subject/ 459 | assert Post.last.to_xml =~ /content/ 460 | end 461 | end 462 | 463 | # TODO error checking for fields that exist in main table, don't exist in 464 | # proxy table, aren't strings or text 465 | # 466 | # TODO allow finding by translated attributes in conditions? 467 | # TODO generate advanced dynamic finders? 468 | -------------------------------------------------------------------------------- /test/all.rb: -------------------------------------------------------------------------------- 1 | files = Dir[File.dirname(__FILE__) + '/**/*_test.rb'] 2 | files.each { |file| require file } -------------------------------------------------------------------------------- /test/data/models.rb: -------------------------------------------------------------------------------- 1 | #require 'ruby2ruby' 2 | #require 'parse_tree' 3 | #require 'parse_tree_extensions' 4 | #require 'pp' 5 | 6 | class PostTranslation < ActiveRecord::Base 7 | def existing_method ; end 8 | end 9 | 10 | class Post < ActiveRecord::Base 11 | translates :subject, :content 12 | validates_presence_of :subject 13 | named_scope :foobar, :conditions => { :title => "foobar" } 14 | end 15 | 16 | class Blog < ActiveRecord::Base 17 | has_many :posts, :order => 'id ASC' 18 | end 19 | 20 | class Parent < ActiveRecord::Base 21 | translates :content 22 | end 23 | 24 | class Child < Parent 25 | end 26 | 27 | class Comment < ActiveRecord::Base 28 | validates_presence_of :content 29 | belongs_to :post 30 | end 31 | 32 | class TranslatedComment < Comment 33 | translates :content 34 | end 35 | 36 | class UltraLongModelNameWithoutProper < ActiveRecord::Base 37 | translates :subject, :content 38 | validates_presence_of :subject 39 | end 40 | 41 | class Reloader < Parent 42 | after_create :do_reload 43 | 44 | def do_reload 45 | reload 46 | end 47 | end 48 | 49 | class Validatee < ActiveRecord::Base 50 | translates :string 51 | end 52 | 53 | class User < ActiveRecord::Base 54 | translates :name 55 | validates_presence_of :name, :email 56 | end 57 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/data/schema.rb: -------------------------------------------------------------------------------- 1 | ActiveRecord::Schema.define do 2 | create_table :blogs, :force => true do |t| 3 | t.string :description 4 | end 5 | 6 | create_table :posts, :force => true do |t| 7 | t.references :blog 8 | end 9 | 10 | create_table :post_translations, :force => true do |t| 11 | t.string :locale 12 | t.references :post 13 | t.string :subject 14 | t.text :content 15 | end 16 | 17 | create_table :parents, :force => true do |t| 18 | end 19 | 20 | create_table :parent_translations, :force => true do |t| 21 | t.string :locale 22 | t.references :parent 23 | t.text :content 24 | t.string :type 25 | end 26 | 27 | create_table :comments, :force => true do |t| 28 | t.references :post 29 | end 30 | 31 | create_table :comment_translations, :force => true do |t| 32 | t.string :locale 33 | t.references :comment 34 | t.string :subject 35 | t.text :content 36 | end 37 | 38 | create_table :validatees, :force => true do |t| 39 | end 40 | 41 | create_table :validatee_translations, :force => true do |t| 42 | t.string :locale 43 | t.references :validatee 44 | t.string :string 45 | end 46 | 47 | create_table :users, :force => true do |t| 48 | t.string :email 49 | end 50 | 51 | create_table :users_translations, :force => true do |t| 52 | t.references :user 53 | t.string :name 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/i18n/missing_translations_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../test_helper' 2 | require '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 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH << File.expand_path( File.dirname(__FILE__) + '/../lib' ) 2 | 3 | require 'rubygems' 4 | require 'test/unit' 5 | require 'active_record' 6 | require 'active_support' 7 | require 'active_support/test_case' 8 | require 'mocha' 9 | require 'globalize' 10 | # require 'validation_reflection' 11 | 12 | config = { :adapter => 'sqlite3', :database => ':memory:' } 13 | ActiveRecord::Base.establish_connection(config) 14 | 15 | class ActiveSupport::TestCase 16 | def reset_db!(schema_path = nil) 17 | schema_path ||= File.expand_path(File.dirname(__FILE__) + '/data/schema.rb') 18 | ActiveRecord::Migration.verbose = false 19 | ActiveRecord::Base.silence { load(schema_path) } 20 | end 21 | 22 | def assert_member(item, array) 23 | assert_block "Item #{item} is not in array #{array}" do 24 | array.member?(item) 25 | end 26 | end 27 | 28 | def assert_belongs_to(model, associated) 29 | assert model.reflect_on_all_associations(:belongs_to).detect { |association| 30 | association.name.to_s == associated.to_s 31 | } 32 | end 33 | 34 | def assert_has_many(model, associated) 35 | assert model.reflect_on_all_associations(:has_many).detect { |association| 36 | association.name.to_s == associated.to_s 37 | } 38 | end 39 | end 40 | 41 | module ActiveRecord 42 | module ConnectionAdapters 43 | class AbstractAdapter 44 | def index_exists?(table_name, column_name) 45 | indexes(table_name).any? { |index| index.name == index_name(table_name, column_name) } 46 | end 47 | end 48 | end 49 | end 50 | 51 | # module ActiveRecord 52 | # class BaseWithoutTable < Base 53 | # self.abstract_class = true 54 | # 55 | # def create_or_update 56 | # errors.empty? 57 | # end 58 | # 59 | # class << self 60 | # def columns() 61 | # @columns ||= [] 62 | # end 63 | # 64 | # def column(name, sql_type = nil, default = nil, null = true) 65 | # columns << ActiveRecord::ConnectionAdapters::Column.new(name.to_s, default, sql_type.to_s, null) 66 | # reset_column_information 67 | # end 68 | # 69 | # # Do not reset @columns 70 | # def reset_column_information 71 | # read_methods.each { |name| undef_method(name) } 72 | # @column_names = @columns_hash = @content_columns = @dynamic_methods_hash = @read_methods = nil 73 | # end 74 | # end 75 | # end 76 | # end --------------------------------------------------------------------------------