Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

First commit

  • Loading branch information...
commit b47ff5514429e57d64dd0f38f423970e3fcae05a 0 parents
Iain Hecker authored
22 README
@@ -0,0 +1,22 @@
+TranslatableColumns
+===================
+
+Makes proxies for localized columns, depending on the locale set by I18n.
+
+Read the Rdoc for more options.
+
+Example
+=======
+
+ class Topic < ActiveRecord::Base
+ translatable_columns :title, :body
+ validates_translation_of :title
+ end
+
+ t = Topic.new
+ I18n.locale = 'nl-NL'
+ t.title # fetches t.title_nl
+ I18n.locale = 'de-DE'
+ t.title # fetches t.title_de
+
+Copyright (c) 2008 Iain Hecker, released under the MIT license
2  init.rb
@@ -0,0 +1,2 @@
+ActiveRecord::Base.extend TranslatableColumns::ClassMethods
+ActiveRecord::Base.send :include, TranslatableColumns::InstanceMethods
164 lib/translatable_columns.rb
@@ -0,0 +1,164 @@
+module TranslatableColumns
+
+ # A config class for TranslatableColumns
+ # Stores configuration options, so global settings can be changed.
+ # These options are available:
+ #
+ # <ul>
+ # <li>+full_locale+, when *true* it uses the full locale in
+ # the column names (e.g. _title_en_us_), when *false* (default!) it
+ # uses only the language part (e.g. _title_en_)</li>
+ # <li>+use_default+, when *false* it will not try to find a value
+ # from other languages, when the request locale is nil. (default = true)
+ # </li>
+ # </ul>
+ #
+ # To change the config globally, create an initializer or add to your
+ # environtment-file:
+ #
+ # ActiveRecord::Base.translatable_columns_config.full_locale = false
+ # ActiveRecord::Base.translatable_columns_config.set_default = true
+ #
+ class Config#nodoc:
+ attr_accessor :full_locale, :use_default
+
+ def initialize
+ set_defaults
+ end
+
+ def set_defaults
+ @full_locale = false
+ @use_default = true
+ end
+
+ end
+
+ module ClassMethods
+
+ # Accessor for the config, which makes sure you read from the same config
+ # every time. See +Config+
+ def translatable_columns_config
+ @@translatable_columns_config ||= TranslatableColumns::Config.new
+ end
+
+
+ # Used to define which columns can be translatable
+ #
+ # class Topic < ActiveRecord::Base
+ # translatable_columns :title, :body, :use_default => false
+ # end
+ #
+ # The use of the option +:use_default+ is optional and used to overwrite
+ # the global config. See +Config+ for that.
+ def translatable_columns(*columns)
+
+ options = columns.extract_options!
+ options[:use_default] = translatable_columns_config.use_default unless options.has_key?(:use_default)
+
+ columns.each do |column|
+ define_translated_setter(column)
+ if options[:use_default]
+ define_translated_getter_with_defaults(column)
+ else
+ define_translated_getter_without_defaults(column)
+ end
+ end
+
+ end
+
+ # Defines the method needed to get the translated value, defaults
+ # to values from other columns when needed.
+ # If a column doesn't exist, it'll default to the I18n.default_locale.
+ def define_translated_getter_with_defaults(column)
+ define_method column do
+ self.send(self.class.column_translated(column)) or
+ self.send(self.class.column_name_localized(column, I18n.default_locale)) or
+ self.find_translated_value_for(column)
+ end
+ end
+
+ # Defines the method needed to get the translated value, but
+ # doesn't look beyond its own value, even if it's nil. It will still
+ # look for the column belonging to I18n.default_locale if the locale
+ # doesn't have it's own column.
+ def define_translated_getter_without_defaults(column)
+ define_method column do
+ self.send(self.class.column_translated(column))
+ end
+ end
+
+ # Defines the method needed to fill the proper column. Will set the
+ # default column if no column is found for this locale.
+ def define_translated_setter(column)
+ define_method :"#{column}=" do |value|
+ self.send(:"#{self.class.column_translated(column)}=", value)
+ end
+ end
+
+ # Returns the column associated with the locale specified.
+ #
+ # column_translated("name") # => "name_en"
+ def column_translated(name, locale = I18n.locale)
+ translated_column_exists?(name, locale) ? column_name_localized(name, locale) : column_name_localized(name, I18n.default_locale)
+ end
+
+ # Finds all localized columns belonging to the given column.
+ #
+ # available_translatable_columns_of("name") # => [ "name_en", "name_nl" ]
+ #
+ # TODO It will also find non localized columns, if they start with the same name as the translatable name.
+ def available_translatable_columns_of(name)
+ self.column_names.select { |column| column =~ /^#{name}_\w{2,}$/ }
+ end
+
+ # Returns true if a column exist for the supplied attribute name.
+ def translated_column_exists?(name, locale = I18n.locale)
+ available_translatable_columns_of(name).include?(column_name_localized(name, locale))
+ end
+
+ # Makes the column
+ def column_name_localized(name, locale = I18n.locale)
+ "#{name}_#{column_locale(locale)}"
+ end
+
+ # Returns the proper column name
+ def column_locale(locale = I18n.locale)
+ translatable_columns_config.full_locale ? locale.sub('-','_').downcase : locale.split('-').first
+ end
+
+ # Validates presence of at least one of the localized columns.
+ # Usage is the same as +validates_presence_of+
+ #
+ # Translation scope of the error message is:
+ #
+ # # activerecord.errors.models.topic.attributes.title.must_have_translation
+ # # activerecord.errors.models.topic.must_have_translation
+ # # activerecord.errors.messages.must_have_translation
+ # # activerecord.errors.messages.blank
+ def validates_translation_of(*attr_names)
+ configuration = { :on => :save }
+ configuration.update(attr_names.extract_options!)
+ send(validation_method(configuration[:on]), configuration) do |record|
+ attr_names.each do |attr_name|
+ if record.find_translated_value_for(attr_name).blank?
+ custom_message = configuration[:message] || :must_have_translation
+ record.errors.add(attr_name, :blank, :default => custom_message)
+ end
+ end
+ end
+ end
+
+ end
+
+ module InstanceMethods
+
+ # Finds a value for a translatable column. Just iterates, returns first value found.
+ def find_translated_value_for(name)
+ self.class.available_translatable_columns_of(name).each do |column|
+ return send(column) unless send(column).blank?
+ end
+ nil
+ end
+ end
+
+end
4 spec/cases/topic.rb
@@ -0,0 +1,4 @@
+class Topic < ActiveRecord::Base
+ translatable_columns :title, :body
+ validates_translation_of :title, :body
+end
3  spec/db/database.yml
@@ -0,0 +1,3 @@
+sqlite3:
+ :adapter: sqlite3
+ :dbfile: vendor/plugins/translatable_columns/spec/db/translatable_columns.sqlite3.db
7 spec/db/schema.rb
@@ -0,0 +1,7 @@
+ActiveRecord::Schema.define(:version => 0) do
+ create_table :topics, :force => true do |t|
+ t.string :title_en, :title_nl, :title_de, :title_fr
+ t.string :body_en, :body_nl, :body_de, :body_fr
+ t.integer :author_id
+ end
+end
BIN  spec/db/translatable_columns.sqlite3.db
Binary file not shown
13 spec/spec_helper.rb
@@ -0,0 +1,13 @@
+begin
+ require File.dirname(__FILE__) + '/../../../../spec/spec_helper'
+rescue LoadError
+ puts "You need to install rspec in your base app"
+ exit
+end
+
+plugin_spec_dir = File.dirname(__FILE__)
+ActiveRecord::Base.logger = Logger.new(plugin_spec_dir + "/debug.log")
+
+databases = YAML::load(IO.read(plugin_spec_dir + "/db/database.yml"))
+ActiveRecord::Base.establish_connection(databases[ENV["DB"] || "sqlite3"])
+load(File.join(plugin_spec_dir, "db", "schema.rb"))
144 spec/translatable_columns_spec.rb
@@ -0,0 +1,144 @@
+require File.dirname(__FILE__) + '/spec_helper'
+require File.dirname(__FILE__) + '/cases/topic'
+
+describe TranslatableColumns do
+
+ before :each do
+ @languages = %w{en nl de fr}
+ @columns = @languages.map{|l|"title_#{l}"}
+ @topic = Topic.new
+ Topic.stub!(:find).and_return(@topic)
+
+ # return these persistent values to their defaults
+ ActiveRecord::Base.translatable_columns_config.set_defaults
+ I18n.locale = 'nl-NL'
+ I18n.default_locale = 'en-US'
+ end
+
+ it "should have a mocked model" do
+ Topic.column_names.should include("title_en")
+ end
+
+ it "should find all localized columns" do
+ Topic.available_translatable_columns_of(:title).should include(*@columns)
+ end
+
+ it "should get the language of a locale" do
+ Topic.column_locale('nl-BE').should == 'nl'
+ end
+
+ it "should change nl-BE to nl_be because of database usage" do
+ ActiveRecord::Base.translatable_columns_config.full_locale = true
+ Topic.column_locale('nl-BE').should == 'nl_be'
+ end
+
+ it "should return a localized column" do
+ Topic.column_name_localized('title', 'nl-NL').should == 'title_nl'
+ end
+
+ it "should check for existence of localized columns" do
+ Topic.translated_column_exists?('title', 'nl-NL').should be_true
+ Topic.translated_column_exists?('title', 'jp-JP').should be_false
+ end
+
+ it "should find the default column when asking for a non existing locale" do
+ Topic.column_translated('title', 'jp-JP').should == 'title_en'
+ end
+
+ it "should find the column when asking for an existing locale" do
+ Topic.column_translated('title', 'nl-NL').should == 'title_nl'
+ end
+
+ it "should define an accessor" do
+ @topic.should respond_to(:title=)
+ @topic.should respond_to(:title)
+ @topic.should respond_to(:body=)
+ @topic.should respond_to(:body)
+ end
+
+ it "should define a reader using defaults" do
+ Topic.should_receive(:define_translated_getter_with_defaults)
+ Topic.translatable_columns(:title)
+ end
+
+ it "should define a reader without using defaults" do
+ Topic.should_receive(:define_translated_getter_without_defaults)
+ Topic.translatable_columns(:title, :use_default => false)
+ end
+
+ it "should define a reader without using defaults, because of config" do
+ ActiveRecord::Base.translatable_columns_config.use_default = false
+ Topic.should_receive(:define_translated_getter_without_defaults)
+ Topic.translatable_columns(:title)
+ end
+
+ it "should define a reader with defaults, inspite of config" do
+ ActiveRecord::Base.translatable_columns_config.use_default = false
+ Topic.should_receive(:define_translated_getter_with_defaults)
+ Topic.translatable_columns(:title, :use_default => true)
+ end
+
+ it "should find a value in any column" do
+ @columns.each do |column|
+ @topic.send(:"#{column}=", column)
+ @topic.find_translated_value_for(:title).should == column
+ @topic = Topic.new # reset
+ end
+ end
+
+ it "should retrieve the translated value, not using defaults" do
+ Topic.translatable_columns :title, :use_default => false
+ @topic.should_receive(:title_nl).and_return("foo")
+ @topic.title.should == "foo"
+ end
+
+ it "should retrieve the translated value, using defaults" do
+ Topic.translatable_columns :title, :use_default => true
+ @topic.should_receive(:title_nl).and_return("foo")
+ @topic.title.should == "foo"
+ end
+
+ it "should retrieve the default translation value when the locale doesn't exist, using no defaults" do
+ I18n.locale = 'jp-JP'
+ Topic.translatable_columns :title, :use_default => false
+ @topic.should_receive(:title_en).and_return("foo")
+ @topic.title.should == 'foo'
+ end
+
+ it "should retrieve the default translation value when the locale doesn't exist, when using defaults" do
+ I18n.locale = 'jp-JP'
+ Topic.translatable_columns :title, :use_default => true
+ @topic.should_receive(:title_en).and_return("foo")
+ @topic.title.should == "foo"
+ end
+
+ it "should find any value when using defaults" do
+ @topic.should_receive(:title_fr).at_least(:once).and_return("foo")
+ @topic.title.should == "foo"
+ end
+
+ it "should set a value for the current locale" do
+ @topic.should_receive(:title_nl=)
+ @topic.title = "foo"
+ end
+
+ it "should return a value for the default locale" do
+ I18n.locale = 'jp-JP'
+ @topic.should_receive(:title_en=)
+ @topic.title = "foo"
+ end
+
+ it "should validate at least one translation" do
+ @topic.valid?.should be_false
+ @topic.should have(1).error_on(:title)
+ @topic.should have(1).error_on(:body)
+ end
+
+ it "should have a :must_have_translation error message" do
+ options = [:blank, { :default => :must_have_translation }]
+ @topic.errors.should_receive(:add).with(:title, *options)
+ @topic.errors.should_receive(:add).with(:body, *options)
+ @topic.valid?
+ end
+
+end
Please sign in to comment.
Something went wrong with that request. Please try again.