├── Gemfile ├── lib ├── has_translations │ ├── version.rb │ ├── railtie.rb │ └── model_additions.rb ├── generators │ ├── templates │ │ ├── model.rb.erb │ │ └── migration.rb.erb │ └── translation_for_generator.rb └── has_translations.rb ├── .gitignore ├── gemfiles ├── 3.2.gemfile ├── 4.0.gemfile ├── 4.1.gemfile ├── 4.2.gemfile ├── 5.0.gemfile ├── 4.0.gemfile.lock ├── 4.1.gemfile.lock ├── 3.2.gemfile.lock ├── 4.2.gemfile.lock └── 5.0.gemfile.lock ├── Appraisals ├── Rakefile ├── .travis.yml ├── test ├── test_helper.rb ├── schema.rb └── has_translations_test.rb ├── Gemfile.lock ├── MIT-LICENSE ├── has_translations.gemspec └── README.md /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /lib/has_translations/version.rb: -------------------------------------------------------------------------------- 1 | module HasTranslations 2 | VERSION = '1.1.2' 3 | end 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | pkg/* 4 | .idea 5 | test.log 6 | test/has_translations_plugin.sqlite3.db 7 | test/debug.log 8 | Gemfile.lock 9 | -------------------------------------------------------------------------------- /gemfiles/3.2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rails", "~> 3.2.22" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /gemfiles/4.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rails", "4.0.13" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /gemfiles/4.1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rails", "4.1.13" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /gemfiles/4.2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rails", "4.2.4" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /gemfiles/5.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rails", "5.0.0" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /lib/generators/templates/model.rb.erb: -------------------------------------------------------------------------------- 1 | class <%= name.camelcase %> < ActiveRecord::Base 2 | 3 | attr_accessor <%= attributes.map { |attribute| ":#{attribute.name}" }.join(', ') %> 4 | 5 | end 6 | -------------------------------------------------------------------------------- /lib/has_translations/railtie.rb: -------------------------------------------------------------------------------- 1 | module HasTranslations 2 | class Railtie < Rails::Railtie 3 | initializer 'has_translations.model_additions' do 4 | ActiveSupport.on_load :active_record do 5 | include ModelAdditions 6 | end 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | appraise '3.2' do 2 | gem 'rails', '~> 3.2.22' 3 | end 4 | 5 | appraise '4.0' do 6 | gem 'rails', '4.0.13' 7 | end 8 | 9 | appraise '4.1' do 10 | gem 'rails', '4.1.13' 11 | end 12 | 13 | appraise '4.2' do 14 | gem 'rails', '4.2.4' 15 | end 16 | 17 | 18 | appraise '5.0' do 19 | gem 'rails', '5.0.0' 20 | end 21 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rake/testtask' 3 | require 'appraisal' 4 | 5 | Rake::TestTask.new do |t| 6 | t.libs = %w(lib test) 7 | t.test_files = Dir.glob('test/**/*_test.rb').sort 8 | t.verbose = true 9 | # t.warning = false 10 | end 11 | 12 | task :default => :test 13 | 14 | desc 'Setup Appraisal.' 15 | task 'appraisal:setup' do 16 | Rake::Task['appraisal:cleanup'].invoke 17 | Rake::Task['appraisal:gemfiles'].invoke 18 | Rake::Task['appraisal:install'].invoke 19 | end -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | rvm: 2 | - 2.0.0 3 | - 2.1.10 4 | - 2.2.5 5 | - 2.3.1 6 | before_install: 7 | - "gem install bundler" 8 | before_script: 9 | - "bundle install" 10 | script: "bundle exec rake test" 11 | gemfile: 12 | - gemfiles/3.2.gemfile 13 | - gemfiles/4.0.gemfile 14 | - gemfiles/4.1.gemfile 15 | - gemfiles/4.2.gemfile 16 | - gemfiles/5.0.gemfile 17 | cache: bundler 18 | matrix: 19 | exclude: 20 | - rvm: 2.0.0 21 | gemfile: gemfiles/5.0.gemfile 22 | - rvm: 2.1.10 23 | gemfile: gemfiles/5.0.gemfile 24 | -------------------------------------------------------------------------------- /lib/generators/templates/migration.rb.erb: -------------------------------------------------------------------------------- 1 | class Create<%= table_name.camelcase %> < ActiveRecord::Migration 2 | 3 | def <%= old_active_record? ? "self.up" : "change" %> 4 | create_table :<%= table_name %> do |t| 5 | t.integer :<%= foreign_key_name %>, :null => false 6 | t.string :locale, :null => false, :limit => 2 7 | <%- attributes.each do |attribute| -%> 8 | t.<%= attribute.type %> :<%= attribute.name %>, :null => false 9 | <%- end -%> 10 | end 11 | 12 | add_index :<%= table_name %>, [:<%= foreign_key_name %>, :locale], :unique => true 13 | end 14 | <% if old_active_record? %> 15 | def self.down 16 | drop_table :<%= table_name %> 17 | end 18 | <% end %> 19 | end 20 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'test/unit' 3 | 4 | require 'active_record' 5 | require 'logger' 6 | 7 | require 'has_translations' 8 | 9 | begin 10 | I18n.available_locales = :ru, :en, :es 11 | rescue 12 | p '[WARNING]: This test should have the I18n.available_locales= method, which were included in versions ~> 0.3.0' 13 | end 14 | 15 | puts "Using Rails version: #{ActiveRecord::VERSION::STRING}" 16 | 17 | ActiveRecord::Base.logger = Logger.new('test.log') 18 | #ActiveRecord::Base.logger = nil 19 | ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => ':memory:') 20 | 21 | def setup_db 22 | ActiveRecord::Migration.verbose = false 23 | load 'schema.rb' 24 | end 25 | 26 | def teardown_db 27 | ActiveRecord::Base.connection.public_send(ActiveRecord::VERSION::MAJOR >= 5 ? :data_sources : :tables).each do |table| 28 | ActiveRecord::Base.connection.drop_table(table) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | has_translations (1.1.2) 5 | activerecord (>= 3.2.0) 6 | activesupport (>= 3.2.0) 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | activemodel (5.0.0) 12 | activesupport (= 5.0.0) 13 | activerecord (5.0.0) 14 | activemodel (= 5.0.0) 15 | activesupport (= 5.0.0) 16 | arel (~> 7.0) 17 | activesupport (5.0.0) 18 | concurrent-ruby (~> 1.0, >= 1.0.2) 19 | i18n (~> 0.7) 20 | minitest (~> 5.1) 21 | tzinfo (~> 1.1) 22 | appraisal (2.1.0) 23 | bundler 24 | rake 25 | thor (>= 0.14.0) 26 | arel (7.0.0) 27 | concurrent-ruby (1.0.2) 28 | i18n (0.7.0) 29 | minitest (5.9.0) 30 | power_assert (0.2.5) 31 | rake (10.4.2) 32 | sqlite3 (1.3.11) 33 | test-unit (3.1.5) 34 | power_assert 35 | thor (0.19.1) 36 | thread_safe (0.3.5) 37 | tzinfo (1.2.2) 38 | thread_safe (~> 0.1) 39 | 40 | PLATFORMS 41 | ruby 42 | 43 | DEPENDENCIES 44 | appraisal 45 | has_translations! 46 | rake 47 | sqlite3 48 | test-unit 49 | 50 | BUNDLED WITH 51 | 1.12.5 52 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 [name of plugin creator] 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /has_translations.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path('../lib', __FILE__) 3 | require 'has_translations/version' 4 | 5 | Gem::Specification.new do |s| 6 | s.name = 'has_translations' 7 | s.version = HasTranslations::VERSION 8 | s.authors = ['Dmitry Polushkin'] 9 | s.email = %w(dmitry.polushkin@gmail.com) 10 | s.homepage = 'http://github.com/dmitry/has_translations' 11 | s.summary = %q{Create translations for your ActiveRecord models.} 12 | s.description = %q{Create translations for your ActiveRecord models. Uses delegate pattern. Fully tested and used in a several production sites.} 13 | 14 | s.rubyforge_project = 'has_translations' 15 | 16 | s.files = `git ls-files`.split("\n") 17 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 18 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 19 | s.require_paths = %w(lib) 20 | 21 | s.license = 'MIT' 22 | 23 | s.add_dependency 'activesupport', '>= 3.2.0' 24 | s.add_dependency 'activerecord', '>= 3.2.0' 25 | s.add_development_dependency 'sqlite3' 26 | s.add_development_dependency 'appraisal' 27 | s.add_development_dependency 'rake' 28 | s.add_development_dependency 'test-unit' 29 | end 30 | -------------------------------------------------------------------------------- /lib/generators/translation_for_generator.rb: -------------------------------------------------------------------------------- 1 | require 'rails/generators/active_record' 2 | 3 | class TranslationForGenerator < ActiveRecord::Generators::Base 4 | 5 | desc "Description:\n Generate translation for ActiveRecord model" 6 | source_root File.expand_path("../templates", __FILE__) 7 | 8 | argument :attributes, :type => :array, :default => [], :banner => "field_1:text field_2:string field_3:another_type" 9 | 10 | def check_attributes 11 | puts table_name 12 | if attributes.blank? 13 | puts "Define at least one translated attribute" 14 | exit 15 | end 16 | end 17 | 18 | def create_model 19 | template 'model.rb.erb', File.join('app/models', class_path, "#{file_name}.rb") 20 | end 21 | 22 | def create_translations_migration 23 | migration_template 'migration.rb.erb', "db/migrate/create_#{table_name}" 24 | end 25 | 26 | private 27 | 28 | def name_with_translation 29 | "#{name_without_translation.underscore}_translation" 30 | end 31 | 32 | alias_method_chain :name, :translation 33 | 34 | def old_active_record? 35 | (ActiveRecord::VERSION::MAJOR < 3) || (ActiveRecord::VERSION::MAJOR == 3 && ActiveRecord::VERSION::MINOR == 0) 36 | end 37 | 38 | def foreign_key_name 39 | "#{name_without_translation.underscore}_id" 40 | end 41 | 42 | end 43 | -------------------------------------------------------------------------------- /test/schema.rb: -------------------------------------------------------------------------------- 1 | ActiveRecord::Schema.define(:version => 0) do 2 | create_table :articles, :force => true do |t| 3 | t.string :title 4 | end 5 | 6 | create_table :article_translations, :force => true do |t| 7 | t.references :article, :null => false 8 | t.string :locale, :null => false, :limit => 2 9 | t.string :description 10 | t.text :text 11 | end 12 | 13 | create_table :teams, :force => true do |t| 14 | t.string :title 15 | end 16 | 17 | create_table :team_translations, :force => true do |t| 18 | t.references :team, :null => false 19 | t.string :locale, :null => false, :limit => 2 20 | t.text :text 21 | end 22 | 23 | create_table :organizations, :force => true do |t| 24 | t.string :title 25 | end 26 | 27 | create_table :organization_translations, :force => true do |t| 28 | t.integer :company_id, :null => false 29 | t.string :locale, :null => false, :limit => 2 30 | t.text :text 31 | end 32 | end 33 | 34 | class ArticleTranslation < ActiveRecord::Base 35 | #attr_accessible :description, :text 36 | end 37 | class Article < ActiveRecord::Base 38 | include HasTranslations::ModelAdditions 39 | 40 | has_translations :description, :text, :writer => true 41 | end 42 | 43 | class TeamTranslation < ActiveRecord::Base 44 | end 45 | class Team < ActiveRecord::Base 46 | include HasTranslations::ModelAdditions 47 | 48 | has_translations :text, :fallback => true, :nil => nil 49 | end 50 | 51 | class OrganizationTranslation < ActiveRecord::Base 52 | end 53 | class Organization < ActiveRecord::Base 54 | include HasTranslations::ModelAdditions 55 | 56 | has_translations :text, :writer => true, :foreign_key => 'company_id' 57 | end 58 | -------------------------------------------------------------------------------- /gemfiles/4.0.gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: ../ 3 | specs: 4 | has_translations (1.1.0) 5 | activerecord (>= 3.2.0) 6 | activesupport (>= 3.2.0) 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | actionmailer (4.0.13) 12 | actionpack (= 4.0.13) 13 | mail (~> 2.5, >= 2.5.4) 14 | actionpack (4.0.13) 15 | activesupport (= 4.0.13) 16 | builder (~> 3.1.0) 17 | erubis (~> 2.7.0) 18 | rack (~> 1.5.2) 19 | rack-test (~> 0.6.2) 20 | activemodel (4.0.13) 21 | activesupport (= 4.0.13) 22 | builder (~> 3.1.0) 23 | activerecord (4.0.13) 24 | activemodel (= 4.0.13) 25 | activerecord-deprecated_finders (~> 1.0.2) 26 | activesupport (= 4.0.13) 27 | arel (~> 4.0.0) 28 | activerecord-deprecated_finders (1.0.4) 29 | activesupport (4.0.13) 30 | i18n (~> 0.6, >= 0.6.9) 31 | minitest (~> 4.2) 32 | multi_json (~> 1.3) 33 | thread_safe (~> 0.1) 34 | tzinfo (~> 0.3.37) 35 | appraisal (2.1.0) 36 | bundler 37 | rake 38 | thor (>= 0.14.0) 39 | arel (4.0.2) 40 | builder (3.1.4) 41 | concurrent-ruby (1.0.2) 42 | erubis (2.7.0) 43 | i18n (0.7.0) 44 | mail (2.6.4) 45 | mime-types (>= 1.16, < 4) 46 | mime-types (3.1) 47 | mime-types-data (~> 3.2015) 48 | mime-types-data (3.2016.0521) 49 | minitest (4.7.5) 50 | multi_json (1.12.1) 51 | power_assert (0.3.0) 52 | rack (1.5.5) 53 | rack-test (0.6.3) 54 | rack (>= 1.0) 55 | rails (4.0.13) 56 | actionmailer (= 4.0.13) 57 | actionpack (= 4.0.13) 58 | activerecord (= 4.0.13) 59 | activesupport (= 4.0.13) 60 | bundler (>= 1.3.0, < 2.0) 61 | railties (= 4.0.13) 62 | sprockets-rails (~> 2.0) 63 | railties (4.0.13) 64 | actionpack (= 4.0.13) 65 | activesupport (= 4.0.13) 66 | rake (>= 0.8.7) 67 | thor (>= 0.18.1, < 2.0) 68 | rake (11.2.2) 69 | sprockets (3.6.3) 70 | concurrent-ruby (~> 1.0) 71 | rack (> 1, < 3) 72 | sprockets-rails (2.3.3) 73 | actionpack (>= 3.0) 74 | activesupport (>= 3.0) 75 | sprockets (>= 2.8, < 4.0) 76 | sqlite3 (1.3.11) 77 | test-unit (3.2.0) 78 | power_assert 79 | thor (0.19.1) 80 | thread_safe (0.3.5) 81 | tzinfo (0.3.51) 82 | 83 | PLATFORMS 84 | ruby 85 | 86 | DEPENDENCIES 87 | appraisal 88 | has_translations! 89 | rails (= 4.0.13) 90 | rake 91 | sqlite3 92 | test-unit 93 | 94 | BUNDLED WITH 95 | 1.12.4 96 | -------------------------------------------------------------------------------- /gemfiles/4.1.gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: ../ 3 | specs: 4 | has_translations (1.1.0) 5 | activerecord (>= 3.2.0) 6 | activesupport (>= 3.2.0) 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | actionmailer (4.1.13) 12 | actionpack (= 4.1.13) 13 | actionview (= 4.1.13) 14 | mail (~> 2.5, >= 2.5.4) 15 | actionpack (4.1.13) 16 | actionview (= 4.1.13) 17 | activesupport (= 4.1.13) 18 | rack (~> 1.5.2) 19 | rack-test (~> 0.6.2) 20 | actionview (4.1.13) 21 | activesupport (= 4.1.13) 22 | builder (~> 3.1) 23 | erubis (~> 2.7.0) 24 | activemodel (4.1.13) 25 | activesupport (= 4.1.13) 26 | builder (~> 3.1) 27 | activerecord (4.1.13) 28 | activemodel (= 4.1.13) 29 | activesupport (= 4.1.13) 30 | arel (~> 5.0.0) 31 | activesupport (4.1.13) 32 | i18n (~> 0.6, >= 0.6.9) 33 | json (~> 1.7, >= 1.7.7) 34 | minitest (~> 5.1) 35 | thread_safe (~> 0.1) 36 | tzinfo (~> 1.1) 37 | appraisal (2.1.0) 38 | bundler 39 | rake 40 | thor (>= 0.14.0) 41 | arel (5.0.1.20140414130214) 42 | builder (3.2.2) 43 | concurrent-ruby (1.0.2) 44 | erubis (2.7.0) 45 | i18n (0.7.0) 46 | json (1.8.3) 47 | mail (2.6.4) 48 | mime-types (>= 1.16, < 4) 49 | mime-types (3.1) 50 | mime-types-data (~> 3.2015) 51 | mime-types-data (3.2016.0521) 52 | minitest (5.9.0) 53 | power_assert (0.3.0) 54 | rack (1.5.5) 55 | rack-test (0.6.3) 56 | rack (>= 1.0) 57 | rails (4.1.13) 58 | actionmailer (= 4.1.13) 59 | actionpack (= 4.1.13) 60 | actionview (= 4.1.13) 61 | activemodel (= 4.1.13) 62 | activerecord (= 4.1.13) 63 | activesupport (= 4.1.13) 64 | bundler (>= 1.3.0, < 2.0) 65 | railties (= 4.1.13) 66 | sprockets-rails (~> 2.0) 67 | railties (4.1.13) 68 | actionpack (= 4.1.13) 69 | activesupport (= 4.1.13) 70 | rake (>= 0.8.7) 71 | thor (>= 0.18.1, < 2.0) 72 | rake (11.2.2) 73 | sprockets (3.6.3) 74 | concurrent-ruby (~> 1.0) 75 | rack (> 1, < 3) 76 | sprockets-rails (2.3.3) 77 | actionpack (>= 3.0) 78 | activesupport (>= 3.0) 79 | sprockets (>= 2.8, < 4.0) 80 | sqlite3 (1.3.11) 81 | test-unit (3.2.0) 82 | power_assert 83 | thor (0.19.1) 84 | thread_safe (0.3.5) 85 | tzinfo (1.2.2) 86 | thread_safe (~> 0.1) 87 | 88 | PLATFORMS 89 | ruby 90 | 91 | DEPENDENCIES 92 | appraisal 93 | has_translations! 94 | rails (= 4.1.13) 95 | rake 96 | sqlite3 97 | test-unit 98 | 99 | BUNDLED WITH 100 | 1.12.4 101 | -------------------------------------------------------------------------------- /gemfiles/3.2.gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: ../ 3 | specs: 4 | has_translations (1.1.0) 5 | activerecord (>= 3.2.0) 6 | activesupport (>= 3.2.0) 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | actionmailer (3.2.22.2) 12 | actionpack (= 3.2.22.2) 13 | mail (~> 2.5.4) 14 | actionpack (3.2.22.2) 15 | activemodel (= 3.2.22.2) 16 | activesupport (= 3.2.22.2) 17 | builder (~> 3.0.0) 18 | erubis (~> 2.7.0) 19 | journey (~> 1.0.4) 20 | rack (~> 1.4.5) 21 | rack-cache (~> 1.2) 22 | rack-test (~> 0.6.1) 23 | sprockets (~> 2.2.1) 24 | activemodel (3.2.22.2) 25 | activesupport (= 3.2.22.2) 26 | builder (~> 3.0.0) 27 | activerecord (3.2.22.2) 28 | activemodel (= 3.2.22.2) 29 | activesupport (= 3.2.22.2) 30 | arel (~> 3.0.2) 31 | tzinfo (~> 0.3.29) 32 | activeresource (3.2.22.2) 33 | activemodel (= 3.2.22.2) 34 | activesupport (= 3.2.22.2) 35 | activesupport (3.2.22.2) 36 | i18n (~> 0.6, >= 0.6.4) 37 | multi_json (~> 1.0) 38 | appraisal (2.1.0) 39 | bundler 40 | rake 41 | thor (>= 0.14.0) 42 | arel (3.0.3) 43 | builder (3.0.4) 44 | erubis (2.7.0) 45 | hike (1.2.3) 46 | i18n (0.7.0) 47 | journey (1.0.4) 48 | json (1.8.3) 49 | mail (2.5.4) 50 | mime-types (~> 1.16) 51 | treetop (~> 1.4.8) 52 | mime-types (1.25.1) 53 | multi_json (1.12.1) 54 | polyglot (0.3.5) 55 | power_assert (0.3.0) 56 | rack (1.4.7) 57 | rack-cache (1.6.1) 58 | rack (>= 0.4) 59 | rack-ssl (1.3.4) 60 | rack 61 | rack-test (0.6.3) 62 | rack (>= 1.0) 63 | rails (3.2.22.2) 64 | actionmailer (= 3.2.22.2) 65 | actionpack (= 3.2.22.2) 66 | activerecord (= 3.2.22.2) 67 | activeresource (= 3.2.22.2) 68 | activesupport (= 3.2.22.2) 69 | bundler (~> 1.0) 70 | railties (= 3.2.22.2) 71 | railties (3.2.22.2) 72 | actionpack (= 3.2.22.2) 73 | activesupport (= 3.2.22.2) 74 | rack-ssl (~> 1.3.2) 75 | rake (>= 0.8.7) 76 | rdoc (~> 3.4) 77 | thor (>= 0.14.6, < 2.0) 78 | rake (11.2.2) 79 | rdoc (3.12.2) 80 | json (~> 1.4) 81 | sprockets (2.2.3) 82 | hike (~> 1.2) 83 | multi_json (~> 1.0) 84 | rack (~> 1.0) 85 | tilt (~> 1.1, != 1.3.0) 86 | sqlite3 (1.3.11) 87 | test-unit (3.2.0) 88 | power_assert 89 | thor (0.19.1) 90 | tilt (1.4.1) 91 | treetop (1.4.15) 92 | polyglot 93 | polyglot (>= 0.3.1) 94 | tzinfo (0.3.51) 95 | 96 | PLATFORMS 97 | ruby 98 | 99 | DEPENDENCIES 100 | appraisal 101 | has_translations! 102 | rails (~> 3.2.22) 103 | rake 104 | sqlite3 105 | test-unit 106 | 107 | BUNDLED WITH 108 | 1.12.4 109 | -------------------------------------------------------------------------------- /lib/has_translations.rb: -------------------------------------------------------------------------------- 1 | # Provides ability to add the translations for the model using delegate pattern. 2 | # Uses has_many association to the ModelNameTranslation. 3 | # 4 | # For example you have model Article with attributes title and text. 5 | # You want that attributes title and text to be translated. 6 | # For this reason you need to generate new model ArticleTranslation. 7 | # In migration you need to add: 8 | # 9 | # create_table :article_translations do |t| 10 | # t.references :article, :null => false 11 | # t.string :locale, :length => 2, :null => false 12 | # t.string :name, :null => false 13 | # end 14 | # 15 | # add_index :articles, [:article_id, :locale], :unique => true, :name => 'unique_locale_for_article_id' 16 | # 17 | # And in the Article model: 18 | # 19 | # translations :title, :text 20 | # 21 | # This will adds: 22 | # 23 | # * named_scope (translated) and has_many association to the Article model 24 | # * locale presence validation to the ArticleTranslation model. 25 | # 26 | # Notice: if you want to have validates_presence_of :article, you should use :inverse_of. 27 | # Support this by yourself. Better is always to use artile.translations.build() method. 28 | # 29 | # === 30 | # 31 | # You also can pass attributes and options to the translations class method: 32 | # 33 | # translations :title, :text, :fallback => true, :writer => true, :nil => nil 34 | # 35 | # === 36 | # 37 | # Configuration options: 38 | # 39 | # * :fallback - if translation for the current locale not found. 40 | # By default false. Set to true if you want to use reader fallback. 41 | # Uses algorithm of fallback: 42 | # 0) current translation (using I18n.locale); 43 | # 1) default locale (using I18n.default_locale); 44 | # 2) first from translations association; 45 | # 3) :nil value (see :nil configuration option) 46 | # * :reader - add reader attributes to the model and delegate them 47 | # to the translation model columns. Add's fallback if it is set to true. 48 | # * :writer - add writer attributes to the model and assign them 49 | # to the translation model attributes. 50 | # * :nil - when reader cant find string, it returns by default an 51 | # empty string. If you want to change this setting for example to nil, 52 | # add :nil => nil 53 | # 54 | # === 55 | # 56 | # When you are using :writer option, you can create translations using 57 | # update_attributes method. For example: 58 | # 59 | # Article.create! 60 | # Article.update_attributes(:title => 'title', :text => 'text') 61 | # 62 | # === 63 | # 64 | # translated named_scope is useful when you want to find only those 65 | # records that are translated to a specific locale. 66 | # For example if you want to find all Articles that is translated to an english 67 | # language, you can write: Article.translated(:en) 68 | # 69 | # has_translation?(locale) method, that returns true if object's model 70 | # have a translation for a specified locale 71 | # 72 | # translation(locale) method finds translation with specified locale. 73 | # 74 | # all_translations method that returns all possible translations in 75 | # ordered hash (useful when creating forms with nested attributes). 76 | 77 | require "has_translations/model_additions" 78 | require "has_translations/railtie" if defined? Rails 79 | 80 | module HasTranslations 81 | 82 | end 83 | -------------------------------------------------------------------------------- /gemfiles/4.2.gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: ../ 3 | specs: 4 | has_translations (1.1.0) 5 | activerecord (>= 3.2.0) 6 | activesupport (>= 3.2.0) 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | actionmailer (4.2.4) 12 | actionpack (= 4.2.4) 13 | actionview (= 4.2.4) 14 | activejob (= 4.2.4) 15 | mail (~> 2.5, >= 2.5.4) 16 | rails-dom-testing (~> 1.0, >= 1.0.5) 17 | actionpack (4.2.4) 18 | actionview (= 4.2.4) 19 | activesupport (= 4.2.4) 20 | rack (~> 1.6) 21 | rack-test (~> 0.6.2) 22 | rails-dom-testing (~> 1.0, >= 1.0.5) 23 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 24 | actionview (4.2.4) 25 | activesupport (= 4.2.4) 26 | builder (~> 3.1) 27 | erubis (~> 2.7.0) 28 | rails-dom-testing (~> 1.0, >= 1.0.5) 29 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 30 | activejob (4.2.4) 31 | activesupport (= 4.2.4) 32 | globalid (>= 0.3.0) 33 | activemodel (4.2.4) 34 | activesupport (= 4.2.4) 35 | builder (~> 3.1) 36 | activerecord (4.2.4) 37 | activemodel (= 4.2.4) 38 | activesupport (= 4.2.4) 39 | arel (~> 6.0) 40 | activesupport (4.2.4) 41 | i18n (~> 0.7) 42 | json (~> 1.7, >= 1.7.7) 43 | minitest (~> 5.1) 44 | thread_safe (~> 0.3, >= 0.3.4) 45 | tzinfo (~> 1.1) 46 | appraisal (2.1.0) 47 | bundler 48 | rake 49 | thor (>= 0.14.0) 50 | arel (6.0.3) 51 | builder (3.2.2) 52 | concurrent-ruby (1.0.2) 53 | erubis (2.7.0) 54 | globalid (0.3.6) 55 | activesupport (>= 4.1.0) 56 | i18n (0.7.0) 57 | json (1.8.3) 58 | loofah (2.0.3) 59 | nokogiri (>= 1.5.9) 60 | mail (2.6.4) 61 | mime-types (>= 1.16, < 4) 62 | mime-types (3.1) 63 | mime-types-data (~> 3.2015) 64 | mime-types-data (3.2016.0521) 65 | mini_portile2 (2.1.0) 66 | minitest (5.9.0) 67 | nokogiri (1.6.8) 68 | mini_portile2 (~> 2.1.0) 69 | pkg-config (~> 1.1.7) 70 | pkg-config (1.1.7) 71 | power_assert (0.3.0) 72 | rack (1.6.4) 73 | rack-test (0.6.3) 74 | rack (>= 1.0) 75 | rails (4.2.4) 76 | actionmailer (= 4.2.4) 77 | actionpack (= 4.2.4) 78 | actionview (= 4.2.4) 79 | activejob (= 4.2.4) 80 | activemodel (= 4.2.4) 81 | activerecord (= 4.2.4) 82 | activesupport (= 4.2.4) 83 | bundler (>= 1.3.0, < 2.0) 84 | railties (= 4.2.4) 85 | sprockets-rails 86 | rails-deprecated_sanitizer (1.0.3) 87 | activesupport (>= 4.2.0.alpha) 88 | rails-dom-testing (1.0.7) 89 | activesupport (>= 4.2.0.beta, < 5.0) 90 | nokogiri (~> 1.6.0) 91 | rails-deprecated_sanitizer (>= 1.0.1) 92 | rails-html-sanitizer (1.0.3) 93 | loofah (~> 2.0) 94 | railties (4.2.4) 95 | actionpack (= 4.2.4) 96 | activesupport (= 4.2.4) 97 | rake (>= 0.8.7) 98 | thor (>= 0.18.1, < 2.0) 99 | rake (11.2.2) 100 | sprockets (3.6.3) 101 | concurrent-ruby (~> 1.0) 102 | rack (> 1, < 3) 103 | sprockets-rails (3.1.1) 104 | actionpack (>= 4.0) 105 | activesupport (>= 4.0) 106 | sprockets (>= 3.0.0) 107 | sqlite3 (1.3.11) 108 | test-unit (3.2.0) 109 | power_assert 110 | thor (0.19.1) 111 | thread_safe (0.3.5) 112 | tzinfo (1.2.2) 113 | thread_safe (~> 0.1) 114 | 115 | PLATFORMS 116 | ruby 117 | 118 | DEPENDENCIES 119 | appraisal 120 | has_translations! 121 | rails (= 4.2.4) 122 | rake 123 | sqlite3 124 | test-unit 125 | 126 | BUNDLED WITH 127 | 1.12.4 128 | -------------------------------------------------------------------------------- /gemfiles/5.0.gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: ../ 3 | specs: 4 | has_translations (1.1.0) 5 | activerecord (>= 3.2.0) 6 | activesupport (>= 3.2.0) 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | actioncable (5.0.0) 12 | actionpack (= 5.0.0) 13 | nio4r (~> 1.2) 14 | websocket-driver (~> 0.6.1) 15 | actionmailer (5.0.0) 16 | actionpack (= 5.0.0) 17 | actionview (= 5.0.0) 18 | activejob (= 5.0.0) 19 | mail (~> 2.5, >= 2.5.4) 20 | rails-dom-testing (~> 2.0) 21 | actionpack (5.0.0) 22 | actionview (= 5.0.0) 23 | activesupport (= 5.0.0) 24 | rack (~> 2.0) 25 | rack-test (~> 0.6.3) 26 | rails-dom-testing (~> 2.0) 27 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 28 | actionview (5.0.0) 29 | activesupport (= 5.0.0) 30 | builder (~> 3.1) 31 | erubis (~> 2.7.0) 32 | rails-dom-testing (~> 2.0) 33 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 34 | activejob (5.0.0) 35 | activesupport (= 5.0.0) 36 | globalid (>= 0.3.6) 37 | activemodel (5.0.0) 38 | activesupport (= 5.0.0) 39 | activerecord (5.0.0) 40 | activemodel (= 5.0.0) 41 | activesupport (= 5.0.0) 42 | arel (~> 7.0) 43 | activesupport (5.0.0) 44 | concurrent-ruby (~> 1.0, >= 1.0.2) 45 | i18n (~> 0.7) 46 | minitest (~> 5.1) 47 | tzinfo (~> 1.1) 48 | appraisal (2.1.0) 49 | bundler 50 | rake 51 | thor (>= 0.14.0) 52 | arel (7.0.0) 53 | builder (3.2.2) 54 | concurrent-ruby (1.0.2) 55 | erubis (2.7.0) 56 | globalid (0.3.6) 57 | activesupport (>= 4.1.0) 58 | i18n (0.7.0) 59 | loofah (2.0.3) 60 | nokogiri (>= 1.5.9) 61 | mail (2.6.4) 62 | mime-types (>= 1.16, < 4) 63 | method_source (0.8.2) 64 | mime-types (3.1) 65 | mime-types-data (~> 3.2015) 66 | mime-types-data (3.2016.0521) 67 | mini_portile2 (2.1.0) 68 | minitest (5.9.0) 69 | nio4r (1.2.1) 70 | nokogiri (1.6.8) 71 | mini_portile2 (~> 2.1.0) 72 | pkg-config (~> 1.1.7) 73 | pkg-config (1.1.7) 74 | power_assert (0.3.0) 75 | rack (2.0.1) 76 | rack-test (0.6.3) 77 | rack (>= 1.0) 78 | rails (5.0.0) 79 | actioncable (= 5.0.0) 80 | actionmailer (= 5.0.0) 81 | actionpack (= 5.0.0) 82 | actionview (= 5.0.0) 83 | activejob (= 5.0.0) 84 | activemodel (= 5.0.0) 85 | activerecord (= 5.0.0) 86 | activesupport (= 5.0.0) 87 | bundler (>= 1.3.0, < 2.0) 88 | railties (= 5.0.0) 89 | sprockets-rails (>= 2.0.0) 90 | rails-dom-testing (2.0.1) 91 | activesupport (>= 4.2.0, < 6.0) 92 | nokogiri (~> 1.6.0) 93 | rails-html-sanitizer (1.0.3) 94 | loofah (~> 2.0) 95 | railties (5.0.0) 96 | actionpack (= 5.0.0) 97 | activesupport (= 5.0.0) 98 | method_source 99 | rake (>= 0.8.7) 100 | thor (>= 0.18.1, < 2.0) 101 | rake (11.2.2) 102 | sprockets (3.6.3) 103 | concurrent-ruby (~> 1.0) 104 | rack (> 1, < 3) 105 | sprockets-rails (3.1.1) 106 | actionpack (>= 4.0) 107 | activesupport (>= 4.0) 108 | sprockets (>= 3.0.0) 109 | sqlite3 (1.3.11) 110 | test-unit (3.2.0) 111 | power_assert 112 | thor (0.19.1) 113 | thread_safe (0.3.5) 114 | tzinfo (1.2.2) 115 | thread_safe (~> 0.1) 116 | websocket-driver (0.6.4) 117 | websocket-extensions (>= 0.1.0) 118 | websocket-extensions (0.1.2) 119 | 120 | PLATFORMS 121 | ruby 122 | 123 | DEPENDENCIES 124 | appraisal 125 | has_translations! 126 | rails (= 5.0.0) 127 | rake 128 | sqlite3 129 | test-unit 130 | 131 | BUNDLED WITH 132 | 1.12.4 133 | -------------------------------------------------------------------------------- /lib/has_translations/model_additions.rb: -------------------------------------------------------------------------------- 1 | module HasTranslations 2 | module ModelAdditions 3 | extend ActiveSupport::Concern 4 | 5 | module ClassMethods 6 | def translated(locale) 7 | where("#{self.has_translations_options[:translation_class].table_name}.locale = ?", locale).joins(:translations) 8 | end 9 | 10 | def has_translations(*attrs) 11 | new_options = attrs.extract_options! 12 | options = { 13 | fallback: false, 14 | reader: true, 15 | writer: false, 16 | nil: '', 17 | autosave: new_options[:writer], 18 | translation_class: nil 19 | }.merge(new_options) 20 | 21 | translation_class_name = options[:translation_class].try(:name) || "#{self.model_name}Translation" 22 | options[:translation_class] ||= translation_class_name.constantize 23 | 24 | options.assert_valid_keys( 25 | [ 26 | :fallback, 27 | :reader, 28 | :writer, 29 | :nil, 30 | :inverse_of, 31 | :autosave, 32 | :translation_class, 33 | :foreign_key 34 | ] 35 | ) 36 | 37 | belongs_to = self.model_name.to_s.demodulize.underscore.to_sym 38 | 39 | class_attribute :has_translations_options 40 | self.has_translations_options = options 41 | 42 | # associations, validations and scope definitions 43 | options[:translation_class].belongs_to(belongs_to, foreign_key: options[:foreign_key]) 44 | has_many( 45 | :translations, 46 | class_name: translation_class_name, 47 | dependent: :destroy, 48 | autosave: options[:autosave], 49 | inverse_of: options[:inverse_of], 50 | foreign_key: options[:foreign_key] 51 | ) 52 | options[:translation_class].validates( 53 | :locale, 54 | presence: true, 55 | uniqueness: {scope: (options[:foreign_key] || "#{belongs_to}_id").to_sym} 56 | ) 57 | 58 | # Optionals delegated readers 59 | if options[:reader] 60 | attrs.each do |name| 61 | send :define_method, name do |*args| 62 | locale = args.first || I18n.locale 63 | translation = self.translation(locale) 64 | translation.try(name) || has_translations_options[:nil] 65 | end 66 | end 67 | end 68 | 69 | # Optionals delegated writers 70 | if options[:writer] 71 | attrs.each do |name| 72 | send :define_method, "#{name}_before_type_cast" do 73 | translation = self.translation(I18n.locale, false) 74 | translation.try(name) 75 | end 76 | 77 | send :define_method, "#{name}=" do |value| 78 | translation = find_or_build_translation(I18n.locale) 79 | translation.send(:"#{name}=", value) 80 | end 81 | end 82 | end 83 | end 84 | end 85 | 86 | def find_or_create_translation(locale) 87 | locale = locale.to_s 88 | (find_translation(locale) || self.has_translations_options[:translation_class].new).tap do |t| 89 | t.locale = locale 90 | t.send(:"#{self.class.model_name.to_s.demodulize.underscore.to_sym}_id=", self.id) 91 | end 92 | end 93 | 94 | def find_or_build_translation(locale) 95 | locale = locale.to_s 96 | (find_translation(locale) || self.translations.build).tap do |t| 97 | t.locale = locale 98 | end 99 | end 100 | 101 | def translation(locale, fallback=has_translations_options[:fallback]) 102 | find_translation(locale) || (fallback && !translations.empty? ? find_translation(I18n.default_locale) || translations.first : nil) 103 | end 104 | 105 | def all_translations 106 | t = I18n.available_locales.map do |locale| 107 | [locale, find_or_create_translation(locale)] 108 | end 109 | ActiveSupport::OrderedHash[t] 110 | end 111 | 112 | def has_translation?(locale) 113 | find_translation(locale).present? 114 | end 115 | 116 | def find_translation(locale) 117 | locale = locale.to_s 118 | translations.detect { |t| t.locale == locale } 119 | end 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | HasTranslations 2 | ============================== 3 | 4 | **UNMAINTAINED**: Please use [mobility gem](https://github.com/shioyama/mobility), it has a lot of options ([including table](https://github.com/shioyama/mobility/wiki/Table-Backend)) and maintained. 5 | 6 | [![Build Status](https://secure.travis-ci.org/dmitry/has_translations.png?branch=master)](http://travis-ci.org/dmitry/has_translations) 7 | [![Gem Version](https://badge.fury.io/rb/has_translations.png)](http://badge.fury.io/rb/has_translations) 8 | 9 | This simple plugin creates translations for your model. 10 | Uses delegation pattern: http://en.wikipedia.org/wiki/Delegation_pattern 11 | 12 | Tested with ActiveRecord versions: 3.2.x, 4.0.x, 4.1.x, 4.2.x, 5.0.x 13 | And tested with ruby 1.9.3, 2.0.0, 2.1.x, 2.2.x, 2.3.x 14 | 15 | Compatibility 16 | ============= 17 | 18 | This version only support Rails 4.x.x and 3.2.x. For Rails 2.3.x support please get the 0.3.5 version of this gem and for Rails 3.1.x get the 1.0.0 of this gem. 19 | Plugin support is deprecated in Rails and will be removed soon so this version drop plugin support. 20 | To prevent method shadowing between "translations" class method and "translations" relation in models the class 21 | method has been renamed has_translations. 22 | 23 | ```ruby 24 | class Article < ActiveRecord::Base 25 | translations :title, :text 26 | end 27 | ``` 28 | 29 | become 30 | 31 | ```ruby 32 | class Article < ActiveRecord::Base 33 | has_translations :title, :text 34 | end 35 | ``` 36 | 37 | Installation 38 | ============ 39 | 40 | ```bash 41 | gem install has_translations 42 | ``` 43 | 44 | Example 45 | ======= 46 | 47 | For example you have Article model and you want to have title and text to be translated. 48 | 49 | Run in command line: 50 | 51 | ```bash 52 | rails g translation_for article title:string text:text 53 | ``` 54 | 55 | It will produce ArticleTranslation model and migration. 56 | 57 | Add to article model `translations :value1, :value2`: 58 | 59 | ```ruby 60 | class Article < ActiveRecord::Base 61 | has_translations :title, :text 62 | end 63 | ``` 64 | 65 | And that's it. Now you can add your translations using: 66 | 67 | ```ruby 68 | article = Article.create 69 | 70 | article.translations.create(:locale => 'en', :title => 'title', :text => 'text') # or ArticleTranslation.create(:article => article, :locale => 'en', :title => 'title', :text => 'text') 71 | article.translations.create(:locale => 'ru', :title => 'заголовок', :text => 'текст') 72 | article.reload # reload cached translations association array 73 | I18n.locale = :en 74 | article.text # text 75 | I18n.locale = :ru 76 | article.title # заголовок 77 | ``` 78 | 79 | You can use text filtering plugins, like acts_as_sanitiled and validations, and anything else that is available to the ActiveRecord: 80 | 81 | ```ruby 82 | class ArticleTranslation < ActiveRecord::Base 83 | acts_as_sanitiled :title, :text 84 | 85 | validates_presence_of :title, :text 86 | validates_length_of :title, :maximum => 100 87 | end 88 | ``` 89 | 90 | Options: 91 | 92 | * `:fallback => true` [default: false] - fallback 1) default locale; 2) first from translations; 93 | * `:reader => false` [default: true] - add reader to the model object 94 | * `:writer => true` [default: false] - add writer to the model object 95 | * `:autosave => true` [default: false] - use [autosave option](http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-has_many) for the ActiveRecord translations relation 96 | * `:nil => nil` [default: ''] - if no model found by default returns empty string, you can set it for example to `nil` (no `lambda` supported) 97 | * `:foreign_key => nil` [default: nil] - add the foreign_key for has_many and belogns_to associations 98 | 99 | It's better to use translations with `accepts_nested_attributes_for`: 100 | 101 | ```ruby 102 | accepts_nested_attributes_for :translations 103 | ``` 104 | 105 | To create a form for this you can use `all_translations` method. It's have all 106 | the locales that you have added using the `I18n.available_locales=` method. 107 | If translation for one of the locale isn't exists, it will build it with :locale. 108 | So an example which I used in the production (using `formtastic` gem): 109 | 110 | ```ruby 111 | <% semantic_form_for [:admin, @article] do |f| %> 112 | <%= f.error_messages %> 113 | 114 | <% f.inputs :name => "Basic" do %> 115 | <% object.all_translations.values.each do |translation| %> 116 | <% f.semantic_fields_for :translations, translation do |ft| %> 117 | <%= ft.input :title, :label => "Title #{ft.object.locale.to_s.upcase}" %> 118 | <%= ft.input :text, :label => "Text #{ft.object.locale.to_s.upcase}" %> 119 | <%= ft.input :locale, :as => :hidden %> 120 | <% end %> 121 | <% end %> 122 | <% end %> 123 | <% end %> 124 | ``` 125 | 126 | Sometimes you have validations in the translation model, and if you want to skip 127 | the translations that you don't want to add to the database, you can use 128 | `:reject_if` option, which is available for the `accepts_nested_attributes_for`: 129 | 130 | ```ruby 131 | accepts_nested_attributes_for :translations, :reject_if => lambda { |attrs| attrs['title'].blank? && attrs['text'].blank? } 132 | ``` 133 | 134 | named_scope `translated(locale)` - with that named_scope you can find only 135 | those models that is translated only to specific locale. For example if you will 136 | have 2 models, one is translated to english and the second one isn't, then it 137 | `Article.translated(:en)` will find only first one. 138 | 139 | TODO 140 | ==== 141 | 142 | * caching 143 | * write more examples: fallback feature 144 | * write blog post about comparison and benefits of this plugin between another translation model plugins 145 | 146 | 147 | Alternatives 148 | ============ 149 | 150 | I know three of them: 151 | 152 | * [globalize3](https://github.com/svenfuchs/globalize3) - Globalize3 is the successor of Globalize for Rails. 153 | * [puret](http://github.com/jo/puret) - special for Rails 3 and almost the same as this project. 154 | * [model_translations](http://github.com/janne/model_translations) - almost the same as this project, but more with more code in lib. 155 | * [translatable_columns](http://github.com/iain/translatable_columns) - different approach: every column have own postfix "_#{locale}" in the same table (sometimes it could be fine). 156 | 157 | 158 | Copyright (c) 2009-2016 [Dmitry Polushkin], released under the MIT license 159 | -------------------------------------------------------------------------------- /test/has_translations_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'test_helper' 4 | 5 | class HasTranslationsTest < Test::Unit::TestCase 6 | def setup 7 | setup_db 8 | 9 | [Article, ArticleTranslation, Team, TeamTranslation, Organization, Organization, OrganizationTranslation].each do |k| 10 | k.delete_all 11 | end 12 | I18n.locale = :ru 13 | end 14 | 15 | def teardown 16 | teardown_db 17 | end 18 | 19 | def test_schema_has_loaded_correctly 20 | [Article, ArticleTranslation, Team, TeamTranslation, Organization, OrganizationTranslation].each do |k| 21 | assert_equal [], k.all 22 | end 23 | assert_equal :ru, I18n.locale 24 | end 25 | 26 | def test_reader_text_for_a_given_locale 27 | article = Article.create! 28 | article_translation = ArticleTranslation.new(:description => 'desc', :text => 'text') 29 | article_translation.article = article 30 | article_translation.locale = 'en' 31 | article_translation.save! 32 | assert_not_equal article.text, article_translation.text 33 | I18n.locale = :en 34 | assert_equal article.text, article_translation.text 35 | end 36 | 37 | def test_reader_for_single_column 38 | article = Article.create! 39 | %w{en ru wu}.each do |locale| 40 | translation = article.translations.build(:description=>"description in #{locale}", :text=>"text in #{locale}") 41 | translation.locale=locale 42 | translation.save! 43 | end 44 | I18n.locale = :ru 45 | assert_equal article.description(:en), "description in en" 46 | assert_equal article.text(:wu), "text in wu" 47 | assert_equal article.text, "text in ru" 48 | end 49 | 50 | def test_writer_text_for_a_given_locale 51 | article = Article.create! 52 | assert_equal '', article.text 53 | assert_equal nil, article.text_before_type_cast 54 | article.text = 'text' 55 | assert_equal 'text', article.text_before_type_cast 56 | assert_equal 0, article.translations.count 57 | article.save! 58 | assert_equal 'text', article.text_before_type_cast 59 | assert_equal 1, article.translations.length 60 | assert_equal 1, article.translations.count 61 | I18n.locale = :en 62 | assert_equal '', article.text 63 | article.update_attributes!(:text => 'text') 64 | assert_equal 2, Article.first.translations.count 65 | article.text = 'text new' 66 | article.save! 67 | assert_equal 'text new', article.text 68 | end 69 | 70 | def test_translations_association_and_translations 71 | article = Article.create! 72 | assert_equal [], article.translations 73 | article_translation = ArticleTranslation.new(:description => 'описание', :text => 'текст') 74 | article_translation.article = article 75 | article_translation.locale = 'ru' 76 | article_translation.save! 77 | assert_equal [], article.translations 78 | assert_equal [article_translation], article.reload.translations 79 | assert_equal 'текст', article.text 80 | I18n.locale = :en 81 | assert_equal '', article.text 82 | assert_equal article_translation, article.translation('ru') 83 | assert_equal article_translation, article.translation(:ru) 84 | assert article.destroy 85 | assert_equal [], ArticleTranslation.all 86 | end 87 | 88 | def test_all_translation_works_fine_with_attr_accessible 89 | article = Article.create! 90 | t = article.all_translations 91 | assert_equal 'ru', t[:ru].locale 92 | end 93 | 94 | def test_translation_validations 95 | article_translation = ArticleTranslation.create(:description => 'description', :text => 'text') 96 | assert article_translation.errors[:locale].present? 97 | # TODO may be add :inverse_of to has_many and add presence validation for the belongs_to 98 | end 99 | 100 | def test_fallback_and_nil_options 101 | article = Article.create! 102 | assert_equal '', article.text 103 | team = Team.create! 104 | assert_equal nil, team.text 105 | first_translation = TeamTranslation.create!(:team => team, :locale => 'es', :text => 'text') 106 | assert_equal first_translation.text, team.reload.text 107 | default_translation = TeamTranslation.create!(:team => team, :locale => 'en', :text => 'text') 108 | assert_equal default_translation.text, team.reload.text 109 | real_translation = TeamTranslation.create!(:team => team, :locale => 'ru', :text => 'текст') 110 | assert_equal real_translation.text, team.reload.text 111 | end 112 | 113 | def test_all_translations_sorted_build_or_translation_getted 114 | team = Team.create! 115 | team_translation = TeamTranslation.create!(:team => team, :locale => 'en', :text => 'text') 116 | assert_equal team_translation, team.all_translations[:en] 117 | assert_equal 'ru', team.all_translations[:ru].locale 118 | team_translation_new = team.translations.build(:locale => :ru) 119 | assert_equal team_translation_new.locale.to_s, team.all_translations[:ru].locale 120 | assert_equal team.all_translations[:ru].team_id, team.id 121 | end 122 | 123 | def test_all_translations_should_not_have_build_translations 124 | team = Team.create! 125 | assert_equal 0, team.translations.length 126 | team.all_translations 127 | assert_equal 0, team.translations.length 128 | end 129 | 130 | def test_has_translation? 131 | team = Team.create! 132 | assert !team.has_translation?(:en) 133 | team.translations.create!(:locale => 'en', :text => 'text') 134 | assert team.has_translation?(:en) 135 | end 136 | 137 | def test_named_scope_translated 138 | assert_equal 0, Team.translated(:en).count 139 | assert_equal 0, Team.translated(:ru).count 140 | team = Team.create! 141 | team.translations.create!(:locale => 'en', :text => 'text') 142 | assert_equal 0, Team.translated(:ru).count 143 | assert_equal 1, Team.translated(:en).count 144 | team.translations.create!(:locale => 'ru', :text => 'текст') 145 | assert_equal 1, Team.translated(:ru).count 146 | end 147 | 148 | def test_foreign_key_for_has_many_associations 149 | organization = Organization.create! 150 | organization.translations.create!(:locale => 'en', :text => 'text') 151 | assert organization.has_translation?(:en) 152 | organization.translations.create!(:locale => 'ru', :text => 'текст') 153 | assert_equal 2, organization.reload.translations.count 154 | assert_equal 1, Organization.translated(:en).count 155 | assert_equal 1, Organization.translated(:ru).count 156 | end 157 | end 158 | --------------------------------------------------------------------------------