Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

First commit

  • Loading branch information...
commit b47ff5514429e57d64dd0f38f423970e3fcae05a 0 parents
Iain Hecker authored
22 README
... ... @@ -0,0 +1,22 @@
  1 +TranslatableColumns
  2 +===================
  3 +
  4 +Makes proxies for localized columns, depending on the locale set by I18n.
  5 +
  6 +Read the Rdoc for more options.
  7 +
  8 +Example
  9 +=======
  10 +
  11 + class Topic < ActiveRecord::Base
  12 + translatable_columns :title, :body
  13 + validates_translation_of :title
  14 + end
  15 +
  16 + t = Topic.new
  17 + I18n.locale = 'nl-NL'
  18 + t.title # fetches t.title_nl
  19 + I18n.locale = 'de-DE'
  20 + t.title # fetches t.title_de
  21 +
  22 +Copyright (c) 2008 Iain Hecker, released under the MIT license
2  init.rb
... ... @@ -0,0 +1,2 @@
  1 +ActiveRecord::Base.extend TranslatableColumns::ClassMethods
  2 +ActiveRecord::Base.send :include, TranslatableColumns::InstanceMethods
164 lib/translatable_columns.rb
... ... @@ -0,0 +1,164 @@
  1 +module TranslatableColumns
  2 +
  3 + # A config class for TranslatableColumns
  4 + # Stores configuration options, so global settings can be changed.
  5 + # These options are available:
  6 + #
  7 + # <ul>
  8 + # <li>+full_locale+, when *true* it uses the full locale in
  9 + # the column names (e.g. _title_en_us_), when *false* (default!) it
  10 + # uses only the language part (e.g. _title_en_)</li>
  11 + # <li>+use_default+, when *false* it will not try to find a value
  12 + # from other languages, when the request locale is nil. (default = true)
  13 + # </li>
  14 + # </ul>
  15 + #
  16 + # To change the config globally, create an initializer or add to your
  17 + # environtment-file:
  18 + #
  19 + # ActiveRecord::Base.translatable_columns_config.full_locale = false
  20 + # ActiveRecord::Base.translatable_columns_config.set_default = true
  21 + #
  22 + class Config#nodoc:
  23 + attr_accessor :full_locale, :use_default
  24 +
  25 + def initialize
  26 + set_defaults
  27 + end
  28 +
  29 + def set_defaults
  30 + @full_locale = false
  31 + @use_default = true
  32 + end
  33 +
  34 + end
  35 +
  36 + module ClassMethods
  37 +
  38 + # Accessor for the config, which makes sure you read from the same config
  39 + # every time. See +Config+
  40 + def translatable_columns_config
  41 + @@translatable_columns_config ||= TranslatableColumns::Config.new
  42 + end
  43 +
  44 +
  45 + # Used to define which columns can be translatable
  46 + #
  47 + # class Topic < ActiveRecord::Base
  48 + # translatable_columns :title, :body, :use_default => false
  49 + # end
  50 + #
  51 + # The use of the option +:use_default+ is optional and used to overwrite
  52 + # the global config. See +Config+ for that.
  53 + def translatable_columns(*columns)
  54 +
  55 + options = columns.extract_options!
  56 + options[:use_default] = translatable_columns_config.use_default unless options.has_key?(:use_default)
  57 +
  58 + columns.each do |column|
  59 + define_translated_setter(column)
  60 + if options[:use_default]
  61 + define_translated_getter_with_defaults(column)
  62 + else
  63 + define_translated_getter_without_defaults(column)
  64 + end
  65 + end
  66 +
  67 + end
  68 +
  69 + # Defines the method needed to get the translated value, defaults
  70 + # to values from other columns when needed.
  71 + # If a column doesn't exist, it'll default to the I18n.default_locale.
  72 + def define_translated_getter_with_defaults(column)
  73 + define_method column do
  74 + self.send(self.class.column_translated(column)) or
  75 + self.send(self.class.column_name_localized(column, I18n.default_locale)) or
  76 + self.find_translated_value_for(column)
  77 + end
  78 + end
  79 +
  80 + # Defines the method needed to get the translated value, but
  81 + # doesn't look beyond its own value, even if it's nil. It will still
  82 + # look for the column belonging to I18n.default_locale if the locale
  83 + # doesn't have it's own column.
  84 + def define_translated_getter_without_defaults(column)
  85 + define_method column do
  86 + self.send(self.class.column_translated(column))
  87 + end
  88 + end
  89 +
  90 + # Defines the method needed to fill the proper column. Will set the
  91 + # default column if no column is found for this locale.
  92 + def define_translated_setter(column)
  93 + define_method :"#{column}=" do |value|
  94 + self.send(:"#{self.class.column_translated(column)}=", value)
  95 + end
  96 + end
  97 +
  98 + # Returns the column associated with the locale specified.
  99 + #
  100 + # column_translated("name") # => "name_en"
  101 + def column_translated(name, locale = I18n.locale)
  102 + translated_column_exists?(name, locale) ? column_name_localized(name, locale) : column_name_localized(name, I18n.default_locale)
  103 + end
  104 +
  105 + # Finds all localized columns belonging to the given column.
  106 + #
  107 + # available_translatable_columns_of("name") # => [ "name_en", "name_nl" ]
  108 + #
  109 + # TODO It will also find non localized columns, if they start with the same name as the translatable name.
  110 + def available_translatable_columns_of(name)
  111 + self.column_names.select { |column| column =~ /^#{name}_\w{2,}$/ }
  112 + end
  113 +
  114 + # Returns true if a column exist for the supplied attribute name.
  115 + def translated_column_exists?(name, locale = I18n.locale)
  116 + available_translatable_columns_of(name).include?(column_name_localized(name, locale))
  117 + end
  118 +
  119 + # Makes the column
  120 + def column_name_localized(name, locale = I18n.locale)
  121 + "#{name}_#{column_locale(locale)}"
  122 + end
  123 +
  124 + # Returns the proper column name
  125 + def column_locale(locale = I18n.locale)
  126 + translatable_columns_config.full_locale ? locale.sub('-','_').downcase : locale.split('-').first
  127 + end
  128 +
  129 + # Validates presence of at least one of the localized columns.
  130 + # Usage is the same as +validates_presence_of+
  131 + #
  132 + # Translation scope of the error message is:
  133 + #
  134 + # # activerecord.errors.models.topic.attributes.title.must_have_translation
  135 + # # activerecord.errors.models.topic.must_have_translation
  136 + # # activerecord.errors.messages.must_have_translation
  137 + # # activerecord.errors.messages.blank
  138 + def validates_translation_of(*attr_names)
  139 + configuration = { :on => :save }
  140 + configuration.update(attr_names.extract_options!)
  141 + send(validation_method(configuration[:on]), configuration) do |record|
  142 + attr_names.each do |attr_name|
  143 + if record.find_translated_value_for(attr_name).blank?
  144 + custom_message = configuration[:message] || :must_have_translation
  145 + record.errors.add(attr_name, :blank, :default => custom_message)
  146 + end
  147 + end
  148 + end
  149 + end
  150 +
  151 + end
  152 +
  153 + module InstanceMethods
  154 +
  155 + # Finds a value for a translatable column. Just iterates, returns first value found.
  156 + def find_translated_value_for(name)
  157 + self.class.available_translatable_columns_of(name).each do |column|
  158 + return send(column) unless send(column).blank?
  159 + end
  160 + nil
  161 + end
  162 + end
  163 +
  164 +end
4 spec/cases/topic.rb
... ... @@ -0,0 +1,4 @@
  1 +class Topic < ActiveRecord::Base
  2 + translatable_columns :title, :body
  3 + validates_translation_of :title, :body
  4 +end
3  spec/db/database.yml
... ... @@ -0,0 +1,3 @@
  1 +sqlite3:
  2 + :adapter: sqlite3
  3 + :dbfile: vendor/plugins/translatable_columns/spec/db/translatable_columns.sqlite3.db
7 spec/db/schema.rb
... ... @@ -0,0 +1,7 @@
  1 +ActiveRecord::Schema.define(:version => 0) do
  2 + create_table :topics, :force => true do |t|
  3 + t.string :title_en, :title_nl, :title_de, :title_fr
  4 + t.string :body_en, :body_nl, :body_de, :body_fr
  5 + t.integer :author_id
  6 + end
  7 +end
BIN  spec/db/translatable_columns.sqlite3.db
Binary file not shown
13 spec/spec_helper.rb
... ... @@ -0,0 +1,13 @@
  1 +begin
  2 + require File.dirname(__FILE__) + '/../../../../spec/spec_helper'
  3 +rescue LoadError
  4 + puts "You need to install rspec in your base app"
  5 + exit
  6 +end
  7 +
  8 +plugin_spec_dir = File.dirname(__FILE__)
  9 +ActiveRecord::Base.logger = Logger.new(plugin_spec_dir + "/debug.log")
  10 +
  11 +databases = YAML::load(IO.read(plugin_spec_dir + "/db/database.yml"))
  12 +ActiveRecord::Base.establish_connection(databases[ENV["DB"] || "sqlite3"])
  13 +load(File.join(plugin_spec_dir, "db", "schema.rb"))
144 spec/translatable_columns_spec.rb
... ... @@ -0,0 +1,144 @@
  1 +require File.dirname(__FILE__) + '/spec_helper'
  2 +require File.dirname(__FILE__) + '/cases/topic'
  3 +
  4 +describe TranslatableColumns do
  5 +
  6 + before :each do
  7 + @languages = %w{en nl de fr}
  8 + @columns = @languages.map{|l|"title_#{l}"}
  9 + @topic = Topic.new
  10 + Topic.stub!(:find).and_return(@topic)
  11 +
  12 + # return these persistent values to their defaults
  13 + ActiveRecord::Base.translatable_columns_config.set_defaults
  14 + I18n.locale = 'nl-NL'
  15 + I18n.default_locale = 'en-US'
  16 + end
  17 +
  18 + it "should have a mocked model" do
  19 + Topic.column_names.should include("title_en")
  20 + end
  21 +
  22 + it "should find all localized columns" do
  23 + Topic.available_translatable_columns_of(:title).should include(*@columns)
  24 + end
  25 +
  26 + it "should get the language of a locale" do
  27 + Topic.column_locale('nl-BE').should == 'nl'
  28 + end
  29 +
  30 + it "should change nl-BE to nl_be because of database usage" do
  31 + ActiveRecord::Base.translatable_columns_config.full_locale = true
  32 + Topic.column_locale('nl-BE').should == 'nl_be'
  33 + end
  34 +
  35 + it "should return a localized column" do
  36 + Topic.column_name_localized('title', 'nl-NL').should == 'title_nl'
  37 + end
  38 +
  39 + it "should check for existence of localized columns" do
  40 + Topic.translated_column_exists?('title', 'nl-NL').should be_true
  41 + Topic.translated_column_exists?('title', 'jp-JP').should be_false
  42 + end
  43 +
  44 + it "should find the default column when asking for a non existing locale" do
  45 + Topic.column_translated('title', 'jp-JP').should == 'title_en'
  46 + end
  47 +
  48 + it "should find the column when asking for an existing locale" do
  49 + Topic.column_translated('title', 'nl-NL').should == 'title_nl'
  50 + end
  51 +
  52 + it "should define an accessor" do
  53 + @topic.should respond_to(:title=)
  54 + @topic.should respond_to(:title)
  55 + @topic.should respond_to(:body=)
  56 + @topic.should respond_to(:body)
  57 + end
  58 +
  59 + it "should define a reader using defaults" do
  60 + Topic.should_receive(:define_translated_getter_with_defaults)
  61 + Topic.translatable_columns(:title)
  62 + end
  63 +
  64 + it "should define a reader without using defaults" do
  65 + Topic.should_receive(:define_translated_getter_without_defaults)
  66 + Topic.translatable_columns(:title, :use_default => false)
  67 + end
  68 +
  69 + it "should define a reader without using defaults, because of config" do
  70 + ActiveRecord::Base.translatable_columns_config.use_default = false
  71 + Topic.should_receive(:define_translated_getter_without_defaults)
  72 + Topic.translatable_columns(:title)
  73 + end
  74 +
  75 + it "should define a reader with defaults, inspite of config" do
  76 + ActiveRecord::Base.translatable_columns_config.use_default = false
  77 + Topic.should_receive(:define_translated_getter_with_defaults)
  78 + Topic.translatable_columns(:title, :use_default => true)
  79 + end
  80 +
  81 + it "should find a value in any column" do
  82 + @columns.each do |column|
  83 + @topic.send(:"#{column}=", column)
  84 + @topic.find_translated_value_for(:title).should == column
  85 + @topic = Topic.new # reset
  86 + end
  87 + end
  88 +
  89 + it "should retrieve the translated value, not using defaults" do
  90 + Topic.translatable_columns :title, :use_default => false
  91 + @topic.should_receive(:title_nl).and_return("foo")
  92 + @topic.title.should == "foo"
  93 + end
  94 +
  95 + it "should retrieve the translated value, using defaults" do
  96 + Topic.translatable_columns :title, :use_default => true
  97 + @topic.should_receive(:title_nl).and_return("foo")
  98 + @topic.title.should == "foo"
  99 + end
  100 +
  101 + it "should retrieve the default translation value when the locale doesn't exist, using no defaults" do
  102 + I18n.locale = 'jp-JP'
  103 + Topic.translatable_columns :title, :use_default => false
  104 + @topic.should_receive(:title_en).and_return("foo")
  105 + @topic.title.should == 'foo'
  106 + end
  107 +
  108 + it "should retrieve the default translation value when the locale doesn't exist, when using defaults" do
  109 + I18n.locale = 'jp-JP'
  110 + Topic.translatable_columns :title, :use_default => true
  111 + @topic.should_receive(:title_en).and_return("foo")
  112 + @topic.title.should == "foo"
  113 + end
  114 +
  115 + it "should find any value when using defaults" do
  116 + @topic.should_receive(:title_fr).at_least(:once).and_return("foo")
  117 + @topic.title.should == "foo"
  118 + end
  119 +
  120 + it "should set a value for the current locale" do
  121 + @topic.should_receive(:title_nl=)
  122 + @topic.title = "foo"
  123 + end
  124 +
  125 + it "should return a value for the default locale" do
  126 + I18n.locale = 'jp-JP'
  127 + @topic.should_receive(:title_en=)
  128 + @topic.title = "foo"
  129 + end
  130 +
  131 + it "should validate at least one translation" do
  132 + @topic.valid?.should be_false
  133 + @topic.should have(1).error_on(:title)
  134 + @topic.should have(1).error_on(:body)
  135 + end
  136 +
  137 + it "should have a :must_have_translation error message" do
  138 + options = [:blank, { :default => :must_have_translation }]
  139 + @topic.errors.should_receive(:add).with(:title, *options)
  140 + @topic.errors.should_receive(:add).with(:body, *options)
  141 + @topic.valid?
  142 + end
  143 +
  144 +end

0 comments on commit b47ff55

Please sign in to comment.
Something went wrong with that request. Please try again.