├── .gitignore ├── .travis.yml ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── Rakefile ├── lib ├── multilang-hstore.rb └── multilang-hstore │ ├── active_record_extensions.rb │ ├── exceptions.rb │ ├── translation_keeper.rb │ ├── translation_proxy.rb │ ├── validators │ └── translation_count_validator.rb │ └── version.rb ├── multilang-hstore.gemspec └── spec ├── multilang_spec.rb ├── schema.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *# 3 | .#* 4 | .DS_Store 5 | nbproject/* 6 | .idea/* 7 | *.erb~ 8 | *.rb~ 9 | *.yml~ 10 | *.html~ 11 | *.gem 12 | rdoc/* 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 1.9.3 4 | - ruby-head 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gem 'pg', '>= 0.0.1' 4 | gem 'activerecord', '>= 4.0.0' 5 | 6 | gem 'rake' 7 | 8 | group :test, :development do 9 | gem 'rspec' 10 | end 11 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | activemodel (4.2.0) 5 | activesupport (= 4.2.0) 6 | builder (~> 3.1) 7 | activerecord (4.2.0) 8 | activemodel (= 4.2.0) 9 | activesupport (= 4.2.0) 10 | arel (~> 6.0) 11 | activesupport (4.2.0) 12 | i18n (~> 0.7) 13 | json (~> 1.7, >= 1.7.7) 14 | minitest (~> 5.1) 15 | thread_safe (~> 0.3, >= 0.3.4) 16 | tzinfo (~> 1.1) 17 | arel (6.0.0) 18 | builder (3.2.2) 19 | diff-lcs (1.2.5) 20 | i18n (0.7.0) 21 | json (1.8.2) 22 | minitest (5.5.1) 23 | pg (0.18.1) 24 | rake (10.4.2) 25 | rspec (3.1.0) 26 | rspec-core (~> 3.1.0) 27 | rspec-expectations (~> 3.1.0) 28 | rspec-mocks (~> 3.1.0) 29 | rspec-core (3.1.7) 30 | rspec-support (~> 3.1.0) 31 | rspec-expectations (3.1.2) 32 | diff-lcs (>= 1.2.0, < 2.0) 33 | rspec-support (~> 3.1.0) 34 | rspec-mocks (3.1.3) 35 | rspec-support (~> 3.1.0) 36 | rspec-support (3.1.2) 37 | thread_safe (0.3.4) 38 | tzinfo (1.2.2) 39 | thread_safe (~> 0.1) 40 | 41 | PLATFORMS 42 | ruby 43 | 44 | DEPENDENCIES 45 | activerecord (>= 4.0.0) 46 | pg (>= 0.0.1) 47 | rake 48 | rspec 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Arthur Meinart 2 | Copyright (c) 2012-2014 Bithavoc - http://bithavoc.io 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Multilang-hstore 2 | 3 | [![Build Status](https://travis-ci.org/bithavoc/multilang-hstore.svg)](https://travis-ci.org/bithavoc/multilang-hstore) 4 | 5 | > Multilang is a small translation library for translating database values for Active Support/Rails 4 using the [Hstore datatype](http://www.postgresql.org/docs/9.0/static/hstore.html). 6 | 7 | This project is a fork of [artworklv/multilang](https://github.com/artworklv/multilang) with some remarkable differences: 8 | 9 | * Replaced YAML text fields in favor of Hstore fields. 10 | * The translation hash is no longer limited to locales in `I18n.available_locales`. 11 | * Support for Rails 3 and Rails 4. 12 | 13 | ## Installation 14 | 15 | ### Rails 3 16 | 17 | The last version of the gem for the Rails 3 series is [0.4](https://github.com/bithavoc/multilang-hstore/tree/v0.4). You need configure the multilang gem inside your gemfile: 18 | 19 | gem 'multilang-hstore' 20 | 21 | Do not forget to run: 22 | 23 | bundle install 24 | 25 | ### Rails 4 26 | 27 | Starting with version `1.0.0`, this gem is intented to be used in Rails 4. If you are migrating an existing project from Rails 3, make sure you read [Migrating to Rails 4](#Migrating-to-Rails-4). 28 | 29 | You need configure the multilang gem inside your gemfile: 30 | 31 | gem 'multilang-hstore', '~> 1.0.0' 32 | 33 | Do not forget to run: 34 | 35 | bundle install 36 | 37 | ## Basic Usage 38 | 39 | This is a walkthrough with all steps you need to setup multilang translated attributes, including model and migration. 40 | 41 | We're assuming here you want a Post model with some multilang attributes, as outlined below: 42 | 43 | class Post < ActiveRecord::Base 44 | multilang :title 45 | end 46 | 47 | or 48 | 49 | class Post < ActiveRecord::Base 50 | multilang :title, :description, :required => true, :length => 100 51 | end 52 | 53 | The multilang translations are stored in the same model attributes (eg. title): 54 | 55 | You may need to create migration for Post as usual, but multilang attributes should be in hstore type: 56 | 57 | create_table(:posts) do |t| 58 | t.hstore :title 59 | t.timestamps 60 | end 61 | 62 | Thats it! 63 | 64 | Now you are able to translate values for the attributes :title and :description per locale: 65 | 66 | I18n.locale = :en 67 | post.title = 'Multilang rocks!' 68 | I18n.locale = :lv 69 | post.title = 'Multilang rulle!' 70 | 71 | I18n.locale = :en 72 | post.title #=> Multilang rocks! 73 | I18n.locale = :lv 74 | post.title #=> Multilang rulle! 75 | 76 | 77 | You may assign attributes through auto generated methods (this methods depend from I18n.available_locales): 78 | 79 | I18n.available_locales #=> [:en. :lv] 80 | 81 | post.title_en = 'Multilang rocks!' 82 | post.title_lv = 'Multilang rulle!' 83 | 84 | post.title_en #=> 'Multilang rocks!' 85 | post.title_lv #=> 'Multilang rulle!' 86 | 87 | You may use initialization if needed: 88 | 89 | Post.new(:title => {:en => 'Multilang rocks!', :lv => 'Multilang rulle!'}) 90 | 91 | or 92 | 93 | Post.new(:title_en => 'Multilang rocks!', :title_lv => 'Multilang rulle!') 94 | 95 | Also, you may ise same hashes with setters: 96 | 97 | post.title = {:en => 'Multilang rocks!', :lv => 'Multilang rulle!'} 98 | 99 | ## Attribute methods 100 | 101 | You may get other translations via attribute translation method: 102 | 103 | post.title.translation[:lv] #=> 'Multilang rocks!' 104 | post.title.translation[:en] #=> 'Multilang rulle!' 105 | post.title.translation.locales #=> [:en, :lv] 106 | 107 | If you have incomplete translations, you can get translation from other locale: 108 | 109 | post.title = {:en => 'Multilang rocks!', :lv => ''} 110 | I18n.locale = :lv 111 | post.title.any #=> 'Multilang rocks!' 112 | 113 | The value from "any" method returns value for I18n.current_locale or, if value is empty, return value for I18n.default_locale, if it's empty too it searches through all locales. It takes searching order from I18n.available_locales array. If you don't need ANY value, you can also use current_or_default method like `post.title.current_or_default` (it searches value for current and default locales only). 114 | 115 | ## Validations 116 | 117 | Multilang has some validation features: 118 | 119 | multilang :title, :length => 100 #define maximal length validator 120 | multilang :title, :required => true #define requirement validator for all available_locales 121 | multilang :title, :required => 1 #define requirement validator for 1 locale 122 | multilang :title, :required => [:en, :es] #define requirement validator for specific locales 123 | multilang :title, :format => /regexp/ #define validates_format_of validator 124 | 125 | ## Tests 126 | 127 | Test runs using a temporary database in your local PostgreSQL server: 128 | 129 | Create a test database: 130 | 131 | $ createdb multilang-hstore-test 132 | 133 | Create the [hstore extension](http://www.postgresql.org/docs/9.1/static/sql-createextension.html): 134 | 135 | psql -d multilang-hstore-test -c "CREATE EXTENSION IF NOT EXISTS hstore" 136 | 137 | Create the role *postgres* if necessary: 138 | 139 | $ createuser -s -r postgres 140 | 141 | Finally, you can run your tests: 142 | 143 | rspec 144 | 145 | ## Migrating to Rails 4 146 | 147 | Migrating to Rails 4 and multilang-hstore 1.x is a very straightforward process. 148 | 149 | ### Deprecated Dependencies 150 | 151 | Rails 4 has built-in support for `hstore` datatype, so using any dependency to `activerecord-postgres-hstore` must be removed from your Gemfile: 152 | 153 | ### Mass-Assignment 154 | 155 | Mass-assignment was deprecated in Rails 4, so it was in our gem. You will receive an error similar to this: 156 | 157 | Multilang::Exceptions::DeprecationError: :accessible was deprecated starting multilang-hstore >= 1.0.0 which is intended for Rails >= 4.0.0. Check more info about the deprecation of mass-asignment in Rails 4 158 | 159 | This basically means you are trying to use the option `:accessible` which is deprecated. Removing the option will solve the issue: 160 | 161 | Before: 162 | 163 | class Post < ActiveRecord::Base 164 | multilang :title, :accessible=>true 165 | end 166 | 167 | After: 168 | 169 | class Post < ActiveRecord::Base 170 | multilang :title 171 | end 172 | 173 | ## Bugs and Feedback 174 | 175 | Use [http://github.com/bithavoc/multilang-hstore/issues](http://github.com/bithavoc/multilang-hstore/issues) 176 | 177 | ## License(MIT) 178 | 179 | * Copyright (c) 2012 - 2014 Bithavoc and Contributors - http://bithavoc.io 180 | * Copyright (c) 2010 Arthur Meinart 181 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'rspec/core/rake_task' 3 | require 'rdoc/task' 4 | 5 | desc 'Default: run specs.' 6 | task :default => :spec 7 | 8 | desc "Run specs" 9 | RSpec::Core::RakeTask.new do |t| 10 | t.pattern = "spec/**/*_spec.rb" 11 | t.rspec_opts = ["-c"] 12 | end 13 | 14 | desc 'Generate documentation for the multilang plugin.' 15 | Rake::RDocTask.new(:rdoc) do |rdoc| 16 | rdoc.rdoc_dir = 'rdoc' 17 | rdoc.title = 'Multilang' 18 | rdoc.options << '--line-numbers' << '--inline-source' 19 | rdoc.rdoc_files.include('README.rdoc') 20 | rdoc.rdoc_files.include('lib/**/*.rb') 21 | end 22 | -------------------------------------------------------------------------------- /lib/multilang-hstore.rb: -------------------------------------------------------------------------------- 1 | require 'multilang-hstore/version' 2 | require 'multilang-hstore/exceptions' 3 | require 'multilang-hstore/translation_proxy' 4 | require 'multilang-hstore/translation_keeper' 5 | require 'multilang-hstore/validators/translation_count_validator' 6 | require 'multilang-hstore/active_record_extensions' 7 | 8 | module Multilang 9 | end 10 | -------------------------------------------------------------------------------- /lib/multilang-hstore/active_record_extensions.rb: -------------------------------------------------------------------------------- 1 | module Multilang 2 | module ActiveRecordExtensions 3 | 4 | def self.raise_mass_asignment_deprecation_error! 5 | raise Multilang::Exceptions::DeprecationError.new(":accessible was deprecated starting multilang-hstore >= 1.0.0 which is intended for Rails >= 4.0.0. Check more info about the deprecation of mass-asignment in Rails 4") 6 | end 7 | 8 | module ClassMethods 9 | 10 | def multilang(*args) 11 | 12 | options = { 13 | :required => false, 14 | :length => nil, 15 | :accessible => false, 16 | :format => nil 17 | }.merge(args.extract_options!) 18 | 19 | options.assert_valid_keys([:required, :length, :accessible, :format, :nil]) 20 | 21 | define_translation_base! 22 | 23 | args.each do |attribute| 24 | 25 | define_method attribute do 26 | multilang_translation_keeper(attribute).value 27 | end 28 | 29 | define_method "#{attribute}=" do |value| 30 | multilang_translation_keeper(attribute).update(value) 31 | end 32 | 33 | define_method "#{attribute}_before_type_cast" do 34 | multilang_translation_keeper(attribute).translations 35 | end 36 | 37 | #attribute accessibility for mass assignment 38 | if options[:accessible] 39 | Multilang::ActiveRecordExtensions.raise_mass_asignment_deprecation_error! 40 | end 41 | 42 | module_eval do 43 | serialize "#{attribute}", ActiveRecord::Coders::Hstore 44 | end if defined?(ActiveRecord::Coders::Hstore) 45 | 46 | if options[:required].is_a? Numeric 47 | module_eval do 48 | validates "#{attribute}_before_type_cast", :'multilang/validators/translation_count' => {min: options[:required]} 49 | end 50 | end 51 | 52 | I18n.available_locales.each do |locale| 53 | 54 | define_method "#{attribute}_#{locale}" do 55 | multilang_translation_keeper(attribute)[locale] 56 | end 57 | 58 | define_method "#{attribute}_#{locale}=" do |value| 59 | multilang_translation_keeper(attribute)[locale] = value 60 | end 61 | 62 | # locale based attribute accessibility for mass assignment 63 | if options[:accessible] 64 | Multilang::ActiveRecordExtensions.raise_mass_asignment_deprecation_error! 65 | end 66 | 67 | # attribute presence validator 68 | if options[:required] == true or locale_required?(options[:required], locale) 69 | module_eval do 70 | validates_presence_of "#{attribute}_#{locale}" 71 | end 72 | end 73 | 74 | #attribute maximal length validator 75 | if options[:length] 76 | module_eval do 77 | validates_length_of "#{attribute}_#{locale}", :maximum => options[:length] 78 | end 79 | end 80 | 81 | #attribute format validator 82 | if options[:format] 83 | module_eval do 84 | validates_format_of "#{attribute}_#{locale}", :with => options[:format] 85 | end 86 | end 87 | 88 | end 89 | end 90 | end 91 | 92 | private 93 | 94 | def define_translation_base! 95 | 96 | define_method "multilang_translation_keeper" do |attribute| 97 | unless instance_variable_defined?("@multilang_attribute_#{attribute}") 98 | instance_variable_set("@multilang_attribute_#{attribute}", MultilangTranslationKeeper.new(self, attribute)) 99 | end 100 | instance_variable_get "@multilang_attribute_#{attribute}" 101 | end unless method_defined? :multilang_translation_keeper 102 | 103 | end 104 | 105 | def locale_required? options_required, locale 106 | options_required.is_a? Array and options_required.map(&:to_s).include?(locale.to_s) 107 | end 108 | 109 | end 110 | 111 | end #module ActiveRecordExtensions 112 | end ##module Multilang 113 | 114 | ActiveRecord::Base.extend Multilang::ActiveRecordExtensions::ClassMethods 115 | 116 | -------------------------------------------------------------------------------- /lib/multilang-hstore/exceptions.rb: -------------------------------------------------------------------------------- 1 | module Multilang 2 | module Exceptions 3 | class DeprecationError < StandardError; end 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/multilang-hstore/translation_keeper.rb: -------------------------------------------------------------------------------- 1 | module Multilang 2 | 3 | class MultilangTranslationKeeper 4 | attr_reader :model 5 | attr_reader :attribute 6 | attr_reader :translations 7 | 8 | def initialize(model, attribute) 9 | @model = model 10 | @attribute = attribute 11 | @translations = {} 12 | load! 13 | end 14 | 15 | def value(locale = nil) 16 | @translations.value(locale) 17 | end 18 | 19 | def current_or_any_value 20 | @translations.current_or_any_value 21 | end 22 | 23 | def current_or_default_value 24 | @translations.current_or_default_value 25 | end 26 | 27 | def to_s 28 | raw_read(actual_locale) 29 | end 30 | 31 | def to_str(locale = nil) 32 | locale ||= actual_locale 33 | raw_read(locale) 34 | end 35 | 36 | def update(value) 37 | if value.is_a?(Hash) 38 | clear 39 | value.each { |k, v| write(k, v) } 40 | elsif value.is_a?(MultilangTranslationProxy) 41 | update(value.translation.translations) 42 | elsif value.is_a?(String) 43 | write(actual_locale, value) 44 | end 45 | flush! 46 | end 47 | 48 | def [](locale) 49 | read(locale) 50 | end 51 | 52 | def []=(locale, value) 53 | write(locale, value) 54 | flush! 55 | end 56 | 57 | def locales 58 | @translations.locales 59 | end 60 | 61 | def empty? 62 | @translations.empty? 63 | end 64 | 65 | private 66 | 67 | def actual_locale 68 | @translations.actual_locale 69 | end 70 | 71 | def write(locale, value) 72 | @translations[locale.to_s] = value 73 | end 74 | 75 | def read(locale) 76 | @translations.read(locale) 77 | end 78 | 79 | def raw_read(locale) 80 | @translations[locale.to_s] || '' 81 | end 82 | 83 | def clear 84 | @translations.clear 85 | end 86 | 87 | def load! 88 | data = @model[@attribute] 89 | data = data.blank? ? nil : data 90 | @translations = data.is_a?(Hash) ? data : {} 91 | 92 | class << translations 93 | attr_accessor :multilang_keeper 94 | def to_s 95 | "#{current_or_any_value}" 96 | end 97 | 98 | def locales 99 | self.keys.map(&:to_sym) 100 | end 101 | 102 | def read(locale) 103 | MultilangTranslationProxy.new(self.multilang_keeper, locale) 104 | end 105 | 106 | def current_or_default_value 107 | value.empty? ? value(default_locale) : value 108 | end 109 | 110 | def current_or_any_value 111 | v = value 112 | if v.empty? 113 | reduced_locales = locales - [actual_locale] 114 | reduced_locales.each do |locale| 115 | v = value(locale) 116 | return v unless v.empty? 117 | end 118 | else 119 | return v 120 | end 121 | return '' 122 | end 123 | 124 | def value(locale = actual_locale) 125 | read(locale) 126 | end 127 | 128 | def actual_locale 129 | I18n.locale 130 | end 131 | 132 | def default_locale 133 | I18n.default_locale 134 | end 135 | end 136 | @translations.public_send("multilang_keeper=", self) 137 | end 138 | 139 | def flush! 140 | @model.send("#{@attribute}_will_change!") 141 | @model[@attribute] = @translations 142 | end 143 | 144 | end 145 | end 146 | -------------------------------------------------------------------------------- /lib/multilang-hstore/translation_proxy.rb: -------------------------------------------------------------------------------- 1 | module Multilang 2 | class MultilangTranslationProxy < String 3 | attr_reader :multilang_keeper 4 | 5 | def initialize(multi_keeper, locale) 6 | @multilang_keeper = multi_keeper 7 | super(@multilang_keeper.to_str(locale)) 8 | end 9 | 10 | def translation 11 | @multilang_keeper 12 | end 13 | 14 | def to_s 15 | String.new(self) 16 | end 17 | 18 | def any 19 | @multilang_keeper.current_or_any_value 20 | end 21 | 22 | def current_or_default 23 | @multilang_keeper.current_or_default_value 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/multilang-hstore/validators/translation_count_validator.rb: -------------------------------------------------------------------------------- 1 | module Multilang 2 | module Validators 3 | class TranslationCountValidator < ActiveModel::EachValidator 4 | 5 | def validate_each(record, attribute, value) 6 | count = record.send("#{attribute}").reject{|l,v| v.blank?}.size 7 | if count < options[:min] 8 | record.errors[:attribute] << (options[:message] || "has insufficient translations defined") 9 | end 10 | end 11 | 12 | end 13 | end 14 | end -------------------------------------------------------------------------------- /lib/multilang-hstore/version.rb: -------------------------------------------------------------------------------- 1 | module Multilang 2 | VERSION = "1.0.2" 3 | end 4 | -------------------------------------------------------------------------------- /multilang-hstore.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "multilang-hstore/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = 'multilang-hstore' 7 | s.version = Multilang::VERSION 8 | 9 | s.authors = ["Arthur Meinart", "bithavoc"] 10 | s.description = 'Model translations for Rails 3 backed by PostgreSQL and Hstore' 11 | s.licenses = ['MIT'] 12 | s.email = 'im@bithavoc.io' 13 | s.files = `git ls-files`.split($/) 14 | s.homepage = "http://bithavoc.io/multilang-hstore/" 15 | s.require_paths = ["lib"] 16 | s.summary = %q{Model translations for Rails 3 and Rails 4 backed by PostgreSQL and Hstore} 17 | s.test_files = [ 18 | "spec/multilang_spec.rb", 19 | "spec/schema.rb", 20 | "spec/spec_helper.rb" 21 | ] 22 | s.add_dependency 'pg', '~> 0.0' 23 | s.add_dependency 'activerecord', '~> 4.0' 24 | end 25 | 26 | -------------------------------------------------------------------------------- /spec/multilang_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | require File.expand_path(File.dirname(__FILE__) + '/spec_helper') 4 | 5 | describe Multilang do 6 | before :each do 7 | setup_db 8 | I18n.locale = :lv 9 | end 10 | 11 | after :each do 12 | teardown_db 13 | end 14 | 15 | it "should test database connection" do 16 | AbstractPost.create 17 | AbstractPost.count.should == 1 18 | end 19 | 20 | it "should add getters/setters in RegularPost" do 21 | regular_post = RegularPost.new 22 | %w(title body).each do |attr| 23 | regular_post.should respond_to(attr) 24 | regular_post.should respond_to("#{attr}=") 25 | I18n.available_locales.each do |loc| 26 | regular_post.should respond_to("#{attr}_#{loc}") 27 | regular_post.should respond_to("#{attr}_#{loc}=") 28 | end 29 | end 30 | end 31 | 32 | it "should set attributes in RegularPost" do 33 | rp = RegularPost.new 34 | 35 | # set 36 | rp.title = "Latviešu nosaukums" 37 | rp.body = "Latviešu apraksts" 38 | I18n.locale = :ru 39 | rp.title = "Русский заголовок" 40 | rp.body = "Русский текст" 41 | 42 | # test 43 | I18n.locale = :lv 44 | rp.title.should == "Latviešu nosaukums" 45 | rp.body.should == "Latviešu apraksts" 46 | 47 | I18n.locale = :ru 48 | rp.title.should == "Русский заголовок" 49 | rp.body.should == "Русский текст" 50 | end 51 | 52 | it "should set attributes through alternative setters in RegularPost" do 53 | rp = RegularPost.new 54 | 55 | # set 56 | rp.title_lv = "Latviešu nosaukums" 57 | rp.body_lv = "Latviešu apraksts" 58 | rp.title_ru = "Русский заголовок" 59 | rp.body_ru = "Русский текст" 60 | 61 | # test 62 | rp.title_lv.should == "Latviešu nosaukums" 63 | rp.body_lv.should == "Latviešu apraksts" 64 | rp.title_ru.should == "Русский заголовок" 65 | rp.body_ru.should == "Русский текст" 66 | 67 | I18n.locale = :lv 68 | rp.title.should == "Latviešu nosaukums" 69 | rp.body.should == "Latviešu apraksts" 70 | 71 | I18n.locale = :ru 72 | rp.title.should == "Русский заголовок" 73 | rp.body.should == "Русский текст" 74 | end 75 | 76 | it "should set attributes through fullset hash in RegularPost" do 77 | rp = RegularPost.new 78 | 79 | # set 80 | rp.title = {:lv => "Latviešu nosaukums", :ru => "Русский заголовок"} 81 | rp.body = {:lv => "Latviešu apraksts", :ru => "Русский текст"} 82 | 83 | # test 84 | rp.title_lv.should == "Latviešu nosaukums" 85 | rp.body_lv.should == "Latviešu apraksts" 86 | rp.title_ru.should == "Русский заголовок" 87 | rp.body_ru.should == "Русский текст" 88 | end 89 | 90 | it "should set attributes through halfset hash in RegularPost" do 91 | rp = RegularPost.new 92 | 93 | # set 94 | rp.title = {:lv => "Latviešu nosaukums"} 95 | rp.body = {:lv => "Latviešu apraksts"} 96 | 97 | # test 98 | rp.title_lv.should == "Latviešu nosaukums" 99 | rp.body_lv.should == "Latviešu apraksts" 100 | rp.title_ru.should == "" 101 | rp.body_ru.should == "" 102 | end 103 | 104 | it "should be validated in RegularPost" do 105 | rp = RegularPost.new 106 | 107 | rp.title = "Latviešu nosaukums" 108 | rp.body = "Latviešu apraksts" 109 | I18n.locale = :ru 110 | rp.title = "Русский заголовок" 111 | rp.body = "Русский текст" 112 | 113 | rp.should be_valid 114 | end 115 | 116 | it "should not be validated in RegularPost" do 117 | rp = RegularPost.new 118 | 119 | rp.title_lv = "Latviešu nosaukums"*100 #too long 120 | rp.body_lv = "" #empty 121 | rp.title_ru = "" #empty 122 | rp.body_ru = "Русский текст"*1000 #too long 123 | 124 | rp.valid? 125 | 126 | expect(rp.errors.size).to be >= 4 127 | end 128 | 129 | it "should mass assign attributes in RegularPost" do 130 | rp = RegularPost.new( :title_lv => "Latviešu nosaukums", 131 | :title_ru => "Русский заголовок", 132 | :body_lv => "Latviešu apraksts", 133 | :body_ru => "Русский текст" ) 134 | 135 | # test 136 | rp.title_lv.should == "Latviešu nosaukums" 137 | rp.body_lv.should == "Latviešu apraksts" 138 | rp.title_ru.should == "Русский заголовок" 139 | rp.body_ru.should == "Русский текст" 140 | end 141 | 142 | it "should raise an exception if the deprecated option :accessible is passed as true" do 143 | expect { 144 | class ProtectedPost < ActiveRecord::Base 145 | self.table_name = 'protected_posts' 146 | multilang :title, :accessible => true 147 | multilang :body, :accessible => true 148 | end 149 | }.to raise_error #Multilang::Exceptions::DeprecationError 150 | end 151 | 152 | it "should not raise an exception if the deprecated option :accessible is passed as false" do 153 | expect { 154 | class ProtectedPost < ActiveRecord::Base 155 | self.table_name = 'protected_posts' 156 | multilang :title, :accessible => false 157 | multilang :body, :accessible => false 158 | end 159 | }.to_not raise_error #Multilang::Exceptions::DeprecationError 160 | end 161 | 162 | it "should save/load attributes in RegularPost" do 163 | rp = RegularPost.new( :title_lv => "Latviešu nosaukums", 164 | :title_ru => "Русский заголовок", 165 | :body_lv => "Latviešu apraksts", 166 | :body_ru => "Русский текст" ) 167 | 168 | rp.save 169 | 170 | rp = RegularPost.last 171 | 172 | rp.title_lv.should == "Latviešu nosaukums" 173 | rp.body_lv.should == "Latviešu apraksts" 174 | rp.title_ru.should == "Русский заголовок" 175 | rp.body_ru.should == "Русский текст" 176 | end 177 | 178 | it "should save attributes in db as indexable values" do 179 | rp = RegularPost.new( :title_lv => "Latviešu nosaukums", 180 | :title_ru => "Русский заголовок", 181 | :body_lv => "Latviešu apraksts", 182 | :body_ru => "Русский текст" ) 183 | 184 | rp.save 185 | 186 | rp = RegularPost.last 187 | 188 | rp.title_before_type_cast.should == {"lv"=>"Latviešu nosaukums", "ru"=>"Русский заголовок"} 189 | rp.body_before_type_cast.should == {"lv"=>"Latviešu apraksts", "ru"=>"Русский текст"} 190 | end 191 | 192 | it "should return proxied attribute" do 193 | rp = RegularPost.new( :title_lv => "Latviešu nosaukums", 194 | :title_ru => "Русский заголовок", 195 | :body_lv => "Latviešu apraksts", 196 | :body_ru => "Русский текст" ) 197 | 198 | 199 | rp.title.should be_an_instance_of(Multilang::MultilangTranslationProxy) 200 | rp.title.should be_a_kind_of(String) 201 | 202 | rp.body.should respond_to(:translation) 203 | rp.title.translation[:lv].should == "Latviešu nosaukums" 204 | rp.body.translation[:ru].should == "Русский текст" 205 | 206 | rp.title.to_s.should be_an_instance_of(String) 207 | end 208 | 209 | it "should set/get some attributes in/from PartialrPost" do 210 | rp = RegularPost.new 211 | 212 | # set 213 | rp.title = "Latviešu nosaukums" 214 | rp.body = "Latviešu apraksts" 215 | 216 | # test 217 | I18n.locale = :lv 218 | rp.title.should == "Latviešu nosaukums" 219 | rp.body.should == "Latviešu apraksts" 220 | 221 | I18n.locale = :ru 222 | rp.title.should == "" 223 | rp.body.should == "" 224 | 225 | rp.title.any.should == "Latviešu nosaukums" 226 | rp.body.any.should == "Latviešu apraksts" 227 | 228 | end 229 | 230 | it "should set/get attributes directly from HStore underlying type returned by before_type_cast" do 231 | rp = RegularPost.new 232 | 233 | # set 234 | I18n.locale = :es 235 | rp.title = "Hola" 236 | rp.body = "Hola Mundo" 237 | 238 | I18n.locale = :en 239 | rp.title = "Hello" 240 | rp.body = "Hello World" 241 | 242 | I18n.locale = :ru 243 | # test 244 | rp.title_before_type_cast.actual_locale.should be :ru 245 | expect(rp.title_before_type_cast.locales.size).to be >= 2 246 | rp.title_before_type_cast.locales.should match_array [:es, :en] 247 | rp.title_before_type_cast.should be_kind_of Hash 248 | I18n.locale = :es 249 | rp.title_before_type_cast.value("en").should eq("Hello") 250 | rp.title_before_type_cast.value("es").should eq("Hola") 251 | rp.title_before_type_cast.value.should eq("Hola") 252 | 253 | end 254 | 255 | it "should load the updated translations" do 256 | # create 257 | post = NamedPost.new 258 | post.title = {:en=>"English", :es=>"Spanish"} 259 | post.name = "First" 260 | post.save! 261 | 262 | # query 263 | query_post = NamedPost.where(:name=>"First").first 264 | query_post.should_not be_nil 265 | I18n.locale = :es 266 | query_post.title.should == "Spanish" 267 | I18n.locale = :en 268 | query_post.title.should == "English" 269 | 270 | # update 271 | query_post.title = {:en=>"English USA", :es=>"Spanish LAT"} 272 | query_post.save! 273 | 274 | #reload 275 | query_post = NamedPost.where(:name=>"First").first 276 | query_post.should_not be_nil 277 | I18n.locale = :es 278 | query_post.title.should == "Spanish LAT" 279 | I18n.locale = :en 280 | query_post.title.should == "English USA" 281 | end 282 | 283 | it "should be valid when required specified translations are present" do 284 | post = TacoPost.new 285 | post.title = {lv: "Lv", ru: "Ru"} 286 | post.valid? 287 | post.should be_valid 288 | 289 | I18n.locale = :nl 290 | post.title = "Nl" 291 | post.valid? 292 | post.should be_valid 293 | end 294 | 295 | it "should be invalid when required specified translations are not present" do 296 | post = TacoPost.new 297 | I18n.locale = :es 298 | post.title = {lv: "Latvian"} 299 | post.valid? 300 | expect(post.errors.size).to be >= 1 301 | end 302 | 303 | it "should be valid when require number validation is met" do 304 | post = SloppyPost.new 305 | I18n.locale = :es 306 | post.title = {en: "English"} 307 | post.valid? 308 | post.should be_valid 309 | 310 | post.title = "In Spanish" 311 | post.valid? 312 | post.should be_valid 313 | end 314 | 315 | it "should be invalid when require number validation is not met" do 316 | post = SloppyPost.new 317 | I18n.locale = :es 318 | post.valid? 319 | expect(post.errors.size).to be >= 1 320 | end 321 | 322 | end 323 | -------------------------------------------------------------------------------- /spec/schema.rb: -------------------------------------------------------------------------------- 1 | ActiveRecord::Schema.define(:version => 1) do 2 | 3 | create_table :abstract_posts do |t| 4 | t.hstore :title 5 | t.hstore :body 6 | t.string :name 7 | t.integer :void, :default => 1 8 | end 9 | 10 | create_table :named_posts do |t| 11 | t.hstore :title 12 | t.string :name 13 | end 14 | 15 | end 16 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.expand_path(File.dirname(__FILE__) + '/../lib') 2 | $:.unshift File.expand_path(File.dirname(__FILE__)) 3 | 4 | require 'rubygems' 5 | require 'active_support' 6 | require 'active_record' 7 | require 'multilang-hstore' 8 | require 'logger' 9 | 10 | ActiveRecord::Base.logger = Logger.new(nil) 11 | ActiveRecord::Base.establish_connection(:adapter => "postgresql", :host=>'127.0.0.1', :user=>'postgres') 12 | begin 13 | ActiveRecord::Base.connection.execute('CREATE DATABASE "multilang-hstore-test" WITH OWNER postgres;') 14 | rescue ActiveRecord::StatementInvalid 15 | puts "Database already exists" 16 | end 17 | ActiveRecord::Base.establish_connection(:adapter => "postgresql", :database => "multilang-hstore-test", :host=>'127.0.0.1', :user=>'postgres') 18 | ActiveRecord::Base.connection.execute('CREATE EXTENSION IF NOT EXISTS hstore;') 19 | 20 | I18n.enforce_available_locales = false 21 | I18n.available_locales = [:lv, :ru] 22 | I18n.locale = I18n.default_locale = :lv 23 | 24 | def setup_db 25 | ActiveRecord::Migration.verbose = false 26 | load "schema.rb" 27 | end 28 | 29 | def teardown_db 30 | ActiveRecord::Base.connection.tables.each do |table| 31 | ActiveRecord::Base.connection.drop_table(table) 32 | end 33 | end 34 | 35 | #testable models 36 | class AbstractPost < ActiveRecord::Base 37 | end 38 | 39 | class MinimalPost < ActiveRecord::Base 40 | self.table_name = 'abstract_posts' 41 | multilang :title, :required => false 42 | multilang :body, :required => false 43 | end 44 | 45 | class RegularPost < ActiveRecord::Base 46 | self.table_name = 'abstract_posts' 47 | multilang :title, :required => true, :length => 25 48 | multilang :body, :required => true, :length => 1000 49 | end 50 | 51 | class PartialPost < ActiveRecord::Base 52 | self.table_name = 'abstract_posts' 53 | multilang :title 54 | multilang :body 55 | end 56 | 57 | class NamedPost < ActiveRecord::Base 58 | self.table_name = 'named_posts' 59 | multilang :title 60 | end 61 | 62 | class TacoPost < ActiveRecord::Base 63 | self.table_name = 'named_posts' 64 | multilang :title, required: [:lv, :ru] 65 | end 66 | 67 | class SloppyPost < ActiveRecord::Base 68 | self.table_name = 'named_posts' 69 | multilang :title, required: 1 70 | end 71 | --------------------------------------------------------------------------------