Add Globalize2 so that we get some nice fall backs to other languages when a translation is missing in the requested language.

This commit is contained in:
Shaun McDonald 2009-05-27 15:39:14 +00:00
parent 6ba51da46e
commit 283a3e9ba9
42 changed files with 2670 additions and 0 deletions

View file

@ -0,0 +1,38 @@
require 'globalize/translation'
require 'globalize/locale/fallbacks'
require 'globalize/model/active_record/adapter'
require 'globalize/model/active_record/translated'
module Globalize
module Model
module ActiveRecord
class << self
def create_proxy_class(klass)
Object.const_set "#{klass.name}Translation", Class.new(::ActiveRecord::Base){
belongs_to "#{klass.name.underscore}".intern
def locale
read_attribute(:locale).to_sym
end
def locale=(locale)
write_attribute(:locale, locale.to_s)
end
}
end
def define_accessors(klass, attr_names)
attr_names.each do |attr_name|
klass.send :define_method, attr_name, lambda {
globalize.fetch self.class.locale, attr_name
}
klass.send :define_method, "#{attr_name}=", lambda {|val|
globalize.stash self.class.locale, attr_name, val
self[attr_name] = val
}
end
end
end
end
end
end

View file

@ -0,0 +1,96 @@
module Globalize
module Model
class AttributeStash < Hash
def contains?(locale, attr_name)
locale = locale.to_sym
self[locale] ||= {}
self[locale].has_key? attr_name
end
def read(locale, attr_name)
locale = locale.to_sym
self[locale] ||= {}
self[locale][attr_name]
end
def write(locale, attr_name, value)
locale = locale.to_sym
self[locale] ||= {}
self[locale][attr_name] = value
end
end
class Adapter
def initialize(record)
@record = record
# TODO what exactly are the roles of cache and stash
@cache = AttributeStash.new
@stash = AttributeStash.new
end
def fetch(locale, attr_name)
# locale = I18n.locale
is_cached = @cache.contains?(locale, attr_name)
is_cached ? @cache.read(locale, attr_name) : begin
value = fetch_attribute locale, attr_name
@cache.write locale, attr_name, value if value && value.locale == locale
value
end
end
def stash(locale, attr_name, value)
@stash.write locale, attr_name, value
@cache.write locale, attr_name, value
end
def update_translations!
@stash.each do |locale, attrs|
translation = @record.globalize_translations.find_or_initialize_by_locale(locale.to_s)
attrs.each{|attr_name, value| translation[attr_name] = value }
translation.save!
end
@stash.clear
end
# Clears the cache
def clear
@cache.clear
@stash.clear
end
private
def fetch_attribute(locale, attr_name)
fallbacks = I18n.fallbacks[locale].map{|tag| tag.to_s}.map(&:to_sym)
# If the translations were included with
# :include => globalize_translations
# there is no need to query them again.
unless @record.globalize_translations.loaded?
translations = @record.globalize_translations.by_locales(fallbacks)
else
translations = @record.globalize_translations
end
result, requested_locale = nil, locale
# Walk through the fallbacks, starting with the current locale itself, and moving
# to the next best choice, until we find a match.
# Check the @globalize_set_translations cache first to see if we've just changed the
# attribute and not saved yet.
fallbacks.each do |fallback|
# TODO should we be checking stash or just cache?
result = @stash.read(fallback, attr_name) || begin
translation = translations.detect {|tr| tr.locale == fallback }
translation && translation.send(attr_name)
end
if result
locale = fallback
break
end
end
result && Translation::Attribute.new(result, :locale => locale, :requested_locale => requested_locale)
end
end
end
end

View file

@ -0,0 +1,154 @@
module Globalize
module Model
class MigrationError < StandardError; end
class UntranslatedMigrationField < MigrationError; end
class MigrationMissingTranslatedField < MigrationError; end
class BadMigrationFieldType < MigrationError; end
module ActiveRecord
module Translated
def self.included(base)
base.extend ActMethods
end
module ActMethods
def translates(*attr_names)
options = attr_names.extract_options!
options[:translated_attributes] = attr_names
# Only set up once per class
unless included_modules.include? InstanceMethods
class_inheritable_accessor :globalize_options, :globalize_proxy
include InstanceMethods
extend ClassMethods
self.globalize_proxy = Globalize::Model::ActiveRecord.create_proxy_class(self)
has_many(
:globalize_translations,
:class_name => globalize_proxy.name,
:extend => Extensions,
:dependent => :delete_all,
:foreign_key => class_name.foreign_key
)
after_save :update_globalize_record
end
self.globalize_options = options
Globalize::Model::ActiveRecord.define_accessors(self, attr_names)
# Import any callbacks that have been defined by extensions to Globalize2
# and run them.
extend Callbacks
Callbacks.instance_methods.each {|cb| send cb }
end
def locale=(locale)
@@locale = locale
end
def locale
(defined?(@@locale) && @@locale) || I18n.locale
end
end
# Dummy Callbacks module. Extensions to Globalize2 can insert methods into here
# and they'll be called at the end of the translates class method.
module Callbacks
end
# Extension to the has_many :globalize_translations association
module Extensions
def by_locales(locales)
find :all, :conditions => { :locale => locales.map(&:to_s) }
end
end
module ClassMethods
def method_missing(method, *args)
if method.to_s =~ /^find_by_(\w+)$/ && globalize_options[:translated_attributes].include?($1.to_sym)
find(:first, :joins => :globalize_translations,
:conditions => [ "#{i18n_attr($1)} = ? AND #{i18n_attr('locale')} IN (?)",
args.first,I18n.fallbacks[I18n.locale].map{|tag| tag.to_s}])
else
super
end
end
def create_translation_table!(fields)
translated_fields = self.globalize_options[:translated_attributes]
translated_fields.each do |f|
raise MigrationMissingTranslatedField, "Missing translated field #{f}" unless fields[f]
end
fields.each do |name, type|
unless translated_fields.member? name
raise UntranslatedMigrationField, "Can't migrate untranslated field: #{name}"
end
unless [ :string, :text ].member? type
raise BadMigrationFieldType, "Bad field type for #{name}, should be :string or :text"
end
end
translation_table_name = self.name.underscore + '_translations'
self.connection.create_table(translation_table_name) do |t|
t.references self.table_name.singularize
t.string :locale
fields.each do |name, type|
t.column name, type
end
t.timestamps
end
end
def drop_translation_table!
translation_table_name = self.name.underscore + '_translations'
self.connection.drop_table translation_table_name
end
private
def i18n_attr(attribute_name)
self.base_class.name.underscore + "_translations.#{attribute_name}"
end
end
module InstanceMethods
def reload(options = nil)
globalize.clear
# clear all globalized attributes
# TODO what's the best way to handle this?
self.class.globalize_options[:translated_attributes].each do |attr|
@attributes.delete attr.to_s
end
super options
end
def globalize
@globalize ||= Adapter.new self
end
def update_globalize_record
globalize.update_translations!
end
def translated_locales
globalize_translations.scoped(:select => 'DISTINCT locale').map {|gt| gt.locale.to_sym }
end
def set_translations options
options.keys.each do |key|
translation = globalize_translations.find_by_locale(key.to_s) ||
globalize_translations.build(:locale => key.to_s)
translation.update_attributes!(options[key])
end
end
end
end
end
end
end