diff options
Diffstat (limited to 'vendor/plugins/globalize2/lib')
7 files changed, 459 insertions, 0 deletions
diff --git a/vendor/plugins/globalize2/lib/globalize.rb b/vendor/plugins/globalize2/lib/globalize.rb new file mode 100644 index 000000000..67c1878ec --- /dev/null +++ b/vendor/plugins/globalize2/lib/globalize.rb @@ -0,0 +1,15 @@ +module Globalize + autoload :ActiveRecord, 'globalize/active_record' + + class << self + def fallbacks? + I18n.respond_to?(:fallbacks) + end + + def fallbacks(locale) + fallbacks? ? I18n.fallbacks[locale] : [locale.to_sym] + end + end +end + +ActiveRecord::Base.send(:include, Globalize::ActiveRecord) diff --git a/vendor/plugins/globalize2/lib/globalize/active_record.rb b/vendor/plugins/globalize2/lib/globalize/active_record.rb new file mode 100644 index 000000000..71cf8a8d1 --- /dev/null +++ b/vendor/plugins/globalize2/lib/globalize/active_record.rb @@ -0,0 +1,229 @@ +module Globalize + class MigrationError < StandardError; end + class MigrationMissingTranslatedField < MigrationError; end + class BadMigrationFieldType < MigrationError; end + + module ActiveRecord + autoload :Adapter, 'globalize/active_record/adapter' + autoload :Attributes, 'globalize/active_record/attributes' + autoload :Migration, 'globalize/active_record/migration' + + def self.included(base) + base.extend ActMacro + end + + class << self + def build_translation_class(target, options) + options[:table_name] ||= "#{target.table_name.singularize}_translations" + + klass = target.const_defined?(:Translation) ? + target.const_get(:Translation) : + target.const_set(:Translation, Class.new(::ActiveRecord::Base)) + + klass.class_eval do + set_table_name(options[:table_name]) + belongs_to target.name.underscore.gsub('/', '_') + def locale; read_attribute(:locale).to_sym; end + def locale=(locale); write_attribute(:locale, locale.to_s); end + end + + klass + end + end + + module ActMacro + def locale + (defined?(@@locale) && @@locale) + end + + def locale=(locale) + @@locale = locale + end + + def translates(*attr_names) + return if translates? + options = attr_names.extract_options! + + class_inheritable_accessor :translation_class, :translated_attribute_names + class_inheritable_writer :required_attributes + self.translation_class = ActiveRecord.build_translation_class(self, options) + self.translated_attribute_names = attr_names.map(&:to_sym) + + include InstanceMethods + extend ClassMethods, Migration + + after_save :save_translations! + has_many :translations, :class_name => translation_class.name, + :foreign_key => class_name.foreign_key, + :dependent => :delete_all, + :extend => HasManyExtensions + + named_scope :with_translations, lambda { |locale| + conditions = required_attributes.map do |attribute| + "#{quoted_translation_table_name}.#{attribute} IS NOT NULL" + end + conditions << "#{quoted_translation_table_name}.locale = ?" + { :include => :translations, :conditions => [conditions.join(' AND '), locale] } + } + + attr_names.each { |attr_name| translated_attr_accessor(attr_name) } + end + + def translates? + included_modules.include?(InstanceMethods) + end + end + + module HasManyExtensions + def by_locale(locale) + first(:conditions => { :locale => locale.to_s }) + end + + def by_locales(locales) + all(:conditions => { :locale => locales.map(&:to_s) }) + end + end + + module ClassMethods + delegate :set_translation_table_name, :to => :translation_class + + def with_locale(locale) + previous_locale, self.locale = self.locale, locale + result = yield + self.locale = previous_locale + result + end + + def translation_table_name + translation_class.table_name + end + + def quoted_translation_table_name + translation_class.quoted_table_name + end + + def required_attributes + @required_attributes ||= reflect_on_all_validations.select do |validation| + validation.macro == :validates_presence_of && translated_attribute_names.include?(validation.name) + end.map(&:name) + end + + def respond_to?(method, *args, &block) + method.to_s =~ /^find_by_(\w+)$/ && translated_attribute_names.include?($1.to_sym) || super + end + + def method_missing(method, *args) + if method.to_s =~ /^find_by_(\w+)$/ && translated_attribute_names.include?($1.to_sym) + find_first_by_translated_attr_and_locales($1, args.first) + elsif method.to_s =~ /^find_all_by_(\w+)$/ && translated_attribute_names.include?($1.to_sym) + find_all_by_translated_attr_and_locales($1, args.first) + else + super + end + end + + protected + + def find_first_by_translated_attr_and_locales(name, value) + query = "#{translated_attr_name(name)} = ? AND #{translated_attr_name('locale')} IN (?)" + locales = Globalize.fallbacks(locale || I18n.locale).map(&:to_s) + find( + :first, + :joins => :translations, + :conditions => [query, value, locales], + :readonly => false + ) + end + + def find_all_by_translated_attr_and_locales(name, value) + query = "#{translated_attr_name(name)} = ? AND #{translated_attr_name('locale')} IN (?)" + locales = Globalize.fallbacks(locale || I18n.locale).map(&:to_s) + find( + :all, +u :joins => :translations, + :conditions => [query, value, locales], + :readonly => false + ) + end + + def translated_attr_accessor(name) + define_method "#{name}=", lambda { |value| + globalize.write(self.class.locale || I18n.locale, name, value) + self[name] = value + } + define_method name, lambda { |*args| + globalize.fetch(args.first || self.class.locale || I18n.locale, name) + } + alias_method "#{name}_before_type_cast", name + end + + def translated_attr_name(name) + "#{translation_class.table_name}.#{name}" + end + end + + module InstanceMethods + def globalize + @globalize ||= Adapter.new self + end + + def attributes + self.attribute_names.inject({}) do |attrs, name| + attrs[name] = read_attribute(name) || + (globalize.fetch(I18n.locale, name) rescue nil) + attrs + end + end + + def attributes=(attributes, *args) + if attributes.respond_to?(:delete) && locale = attributes.delete(:locale) + self.class.with_locale(locale) { super } + else + super + end + end + + def attribute_names + translated_attribute_names.map(&:to_s) + super + end + + def available_locales + translations.scoped(:select => 'DISTINCT locale').map(&:locale) + end + + def translated_locales + translations.map(&:locale) + end + + def translated_attributes + translated_attribute_names.inject({}) do |attributes, name| + attributes.merge(name => send(name)) + end + end + + def set_translations(options) + options.keys.each do |locale| + translation = translations.find_by_locale(locale.to_s) || + translations.build(:locale => locale.to_s) + translation.update_attributes!(options[locale]) + end + end + + def reload(options = nil) + translated_attribute_names.each { |name| @attributes.delete(name.to_s) } + globalize.reset + super(options) + end + + protected + + def save_translations! + globalize.save_translations! + end + end + end +end + +def globalize_write(name, value) + globalize.write(self.class.locale || I18n.locale, name, value) +end diff --git a/vendor/plugins/globalize2/lib/globalize/active_record/adapter.rb b/vendor/plugins/globalize2/lib/globalize/active_record/adapter.rb new file mode 100644 index 000000000..12f89ec01 --- /dev/null +++ b/vendor/plugins/globalize2/lib/globalize/active_record/adapter.rb @@ -0,0 +1,80 @@ +module Globalize + module ActiveRecord + class Adapter + # The cache caches attributes that already were looked up for read access. + # The stash keeps track of new or changed values that need to be saved. + attr_reader :record, :cache, :stash + + def initialize(record) + @record = record + @cache = Attributes.new + @stash = Attributes.new + end + + def fetch(locale, attr_name) + cache.contains?(locale, attr_name) ? + cache.read(locale, attr_name) : + cache.write(locale, attr_name, fetch_attribute(locale, attr_name)) + end + + def write(locale, attr_name, value) + stash.write(locale, attr_name, value) + cache.write(locale, attr_name, value) + end + + def save_translations! + stash.each do |locale, attrs| + translation = record.translations.find_or_initialize_by_locale(locale.to_s) + attrs.each { |attr_name, value| translation[attr_name] = value } + translation.save! + end + stash.clear + end + + def reset + cache.clear + # stash.clear + end + + protected + + def fetch_translation(locale) + locale = locale.to_sym + record.translations.loaded? ? record.translations.detect { |t| t.locale == locale } : + record.translations.by_locale(locale) + end + + def fetch_translations(locale) + # only query if not already included with :include => translations + record.translations.loaded? ? record.translations : + record.translations.by_locales(Globalize.fallbacks(locale)) + end + + def fetch_attribute(locale, attr_name) + translations = fetch_translations(locale) + value, requested_locale = nil, locale + + Globalize.fallbacks(locale).each do |fallback| + translation = translations.detect { |t| t.locale == fallback } + value = translation && translation.send(attr_name) + locale = fallback && break if value + end + + set_metadata(value, :locale => locale, :requested_locale => requested_locale) + value + end + + def set_metadata(object, metadata) + if object.respond_to?(:translation_metadata) + object.translation_metadata.merge!(meta_data) + end + end + + def translation_metadata_accessor(object) + return if obj.respond_to?(:translation_metadata) + class << object; attr_accessor :translation_metadata end + object.translation_metadata ||= {} + end + end + end +end diff --git a/vendor/plugins/globalize2/lib/globalize/active_record/attributes.rb b/vendor/plugins/globalize2/lib/globalize/active_record/attributes.rb new file mode 100644 index 000000000..7bd923ce2 --- /dev/null +++ b/vendor/plugins/globalize2/lib/globalize/active_record/attributes.rb @@ -0,0 +1,25 @@ +# Helper class for storing values per locale. Used by Globalize::Adapter +# to stash and cache attribute values. +module Globalize + module ActiveRecord + class Attributes < Hash + def [](locale) + locale = locale.to_sym + self[locale] = {} unless has_key?(locale) + self.fetch(locale) + end + + def contains?(locale, attr_name) + self[locale].has_key?(attr_name) + end + + def read(locale, attr_name) + self[locale][attr_name] + end + + def write(locale, attr_name, value) + self[locale][attr_name] = value + end + end + end +end diff --git a/vendor/plugins/globalize2/lib/globalize/active_record/migration.rb b/vendor/plugins/globalize2/lib/globalize/active_record/migration.rb new file mode 100644 index 000000000..fa63aed8c --- /dev/null +++ b/vendor/plugins/globalize2/lib/globalize/active_record/migration.rb @@ -0,0 +1,44 @@ +module Globalize + module ActiveRecord + module Migration + def create_translation_table!(fields) + translated_attribute_names.each do |f| + raise MigrationMissingTranslatedField, "Missing translated field #{f}" unless fields[f] + end + + fields.each do |name, type| + if translated_attribute_names.include?(name) && ![:string, :text].include?(type) + raise BadMigrationFieldType, "Bad field type for #{name}, should be :string or :text" + end + end + + self.connection.create_table(translation_table_name) do |t| + t.references table_name.sub(/^#{table_name_prefix}/, "").singularize + t.string :locale + fields.each do |name, type| + t.column name, type + end + t.timestamps + end + + self.connection.add_index( + translation_table_name, + "#{table_name.sub(/^#{table_name_prefix}/, "").singularize}_id", + :name => translation_index_name + ) + end + + def translation_index_name + require 'digest/sha1' + # FIXME what's the max size of an index name? + index_name = "index_#{translation_table_name}_on_#{self.table_name.singularize}_id" + index_name.size < 50 ? index_name : "index_#{Digest::SHA1.hexdigest(index_name)}" + end + + def drop_translation_table! + self.connection.remove_index(translation_table_name, :name => translation_index_name) rescue nil + self.connection.drop_table(translation_table_name) + end + end + end +end diff --git a/vendor/plugins/globalize2/lib/i18n/missing_translations_log_handler.rb b/vendor/plugins/globalize2/lib/i18n/missing_translations_log_handler.rb new file mode 100644 index 000000000..24c10890a --- /dev/null +++ b/vendor/plugins/globalize2/lib/i18n/missing_translations_log_handler.rb @@ -0,0 +1,41 @@ +# A simple exception handler that behaves like the default exception handler +# but additionally logs missing translations to a given log. +# +# Useful for identifying missing translations during testing. +# +# E.g. +# +# require 'globalize/i18n/missing_translations_log_handler' +# I18n.missing_translations_logger = RAILS_DEFAULT_LOGGER +# I18n.exception_handler = :missing_translations_log_handler +# +# To set up a different log file: +# +# logger = Logger.new("#{RAILS_ROOT}/log/missing_translations.log") +# I18n.missing_translations_logger = logger + +module I18n + @@missing_translations_logger = nil + + class << self + def missing_translations_logger + @@missing_translations_logger ||= begin + require 'logger' unless defined?(Logger) + Logger.new(STDOUT) + end + end + + def missing_translations_logger=(logger) + @@missing_translations_logger = logger + end + + def missing_translations_log_handler(exception, locale, key, options) + if MissingTranslationData === exception + missing_translations_logger.warn(exception.message) + return exception.message + else + raise exception + end + end + end +end diff --git a/vendor/plugins/globalize2/lib/i18n/missing_translations_raise_handler.rb b/vendor/plugins/globalize2/lib/i18n/missing_translations_raise_handler.rb new file mode 100644 index 000000000..18237b151 --- /dev/null +++ b/vendor/plugins/globalize2/lib/i18n/missing_translations_raise_handler.rb @@ -0,0 +1,25 @@ +# A simple exception handler that behaves like the default exception handler +# but also raises on missing translations. +# +# Useful for identifying missing translations during testing. +# +# E.g. +# +# require 'globalize/i18n/missing_translations_raise_handler' +# I18n.exception_handler = :missing_translations_raise_handler +module I18n + class << self + def missing_translations_raise_handler(exception, locale, key, options) + raise exception + end + end +end + +I18n.exception_handler = :missing_translations_raise_handler + +ActionView::Helpers::TranslationHelper.module_eval do + def translate(key, options = {}) + I18n.translate(key, options) + end + alias :t :translate +end |