├── .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
--------------------------------------------------------------------------------