galetahub-globalize3 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.textile +206 -0
- data/Rakefile +22 -0
- data/lib/globalize.rb +59 -0
- data/lib/globalize/active_record.rb +13 -0
- data/lib/globalize/active_record/accessors.rb +22 -0
- data/lib/globalize/active_record/act_macro.rb +67 -0
- data/lib/globalize/active_record/adapter.rb +101 -0
- data/lib/globalize/active_record/attributes.rb +27 -0
- data/lib/globalize/active_record/class_methods.rb +125 -0
- data/lib/globalize/active_record/exceptions.rb +19 -0
- data/lib/globalize/active_record/instance_methods.rb +166 -0
- data/lib/globalize/active_record/migration.rb +125 -0
- data/lib/globalize/active_record/translation.rb +37 -0
- data/lib/globalize/engine.rb +17 -0
- data/lib/globalize/utils.rb +142 -0
- data/lib/globalize/versioning.rb +5 -0
- data/lib/globalize/versioning/paper_trail.rb +41 -0
- data/lib/globalize3.rb +1 -0
- data/lib/globalize3/version.rb +3 -0
- data/lib/i18n/missing_translations_log_handler.rb +41 -0
- data/lib/i18n/missing_translations_raise_handler.rb +25 -0
- data/lib/patches/active_record/query_method.rb +35 -0
- data/lib/patches/active_record/xml_attribute_serializer.rb +13 -0
- data/lib/tasks/globalize.rake +13 -0
- data/test/all.rb +1 -0
- data/test/data/models.rb +68 -0
- data/test/data/schema.rb +108 -0
- data/test/globalize3/attributes_test.rb +133 -0
- data/test/globalize3/clone_test.rb +58 -0
- data/test/globalize3/dirty_tracking_test.rb +61 -0
- data/test/globalize3/dynamic_finders_test.rb +171 -0
- data/test/globalize3/fallbacks_test.rb +146 -0
- data/test/globalize3/locale_test.rb +81 -0
- data/test/globalize3/migration_test.rb +156 -0
- data/test/globalize3/set_translations_test.rb +54 -0
- data/test/globalize3/translation_class_test.rb +59 -0
- data/test/globalize3/validations_test.rb +92 -0
- data/test/globalize3/versioning_test.rb +87 -0
- data/test/globalize3_test.rb +159 -0
- data/test/i18n/missing_translations_test.rb +35 -0
- data/test/test_helper.rb +105 -0
- metadata +243 -0
@@ -0,0 +1,37 @@
|
|
1
|
+
module Globalize
|
2
|
+
module ActiveRecord
|
3
|
+
class Translation < ::ActiveRecord::Base
|
4
|
+
attr_accessible :locale
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def with_locales(*locales)
|
8
|
+
# Avoid using "IN" with SQL queries when only using one locale.
|
9
|
+
locales = locales.flatten.map(&:to_s)
|
10
|
+
locales = locales.first if locales.one?
|
11
|
+
where(:locale => locales)
|
12
|
+
end
|
13
|
+
alias with_locale with_locales
|
14
|
+
|
15
|
+
def translated_locales
|
16
|
+
select('DISTINCT locale').map(&:locale)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def locale
|
21
|
+
read_attribute(:locale).to_sym
|
22
|
+
end
|
23
|
+
|
24
|
+
def locale=(locale)
|
25
|
+
write_attribute(:locale, locale.to_s)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Setting this will force polymorphic associations to subclassed objects
|
32
|
+
# to use their table_name rather than the parent object's table name,
|
33
|
+
# which will allow you to get their models back in a more appropriate
|
34
|
+
# format.
|
35
|
+
#
|
36
|
+
# See http://www.ruby-forum.com/topic/159894 for details.
|
37
|
+
Globalize::ActiveRecord::Translation.abstract_class = true
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'rails'
|
3
|
+
require 'globalize'
|
4
|
+
|
5
|
+
module Globalize
|
6
|
+
class Engine < ::Rails::Engine
|
7
|
+
config.before_initialize do
|
8
|
+
ActiveSupport.on_load :active_record do
|
9
|
+
::ActiveRecord::Base.extend(Globalize::ActiveRecord::ActMacro)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
rake_tasks do
|
14
|
+
load "tasks/globalize.rake"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module Globalize
|
3
|
+
class Utils
|
4
|
+
class << self
|
5
|
+
def model_dir
|
6
|
+
@model_dir || "app/models"
|
7
|
+
end
|
8
|
+
|
9
|
+
def model_dir=(dir)
|
10
|
+
@model_dir = dir
|
11
|
+
end
|
12
|
+
|
13
|
+
def show_log(action, table_name, name=false, type=false)
|
14
|
+
log = []
|
15
|
+
log << action
|
16
|
+
log << ' ' + table_name.to_s
|
17
|
+
log << '.' + name.to_s if name
|
18
|
+
log << ' as ' + type.to_s if type
|
19
|
+
|
20
|
+
puts log.join
|
21
|
+
end
|
22
|
+
|
23
|
+
# Return a list of the model files to translate. If we have
|
24
|
+
# command line arguments, they're assumed to be either
|
25
|
+
# the underscore or CamelCase versions of model names.
|
26
|
+
# Otherwise we take all the model files in the
|
27
|
+
# model_dir directory.
|
28
|
+
def get_model_files
|
29
|
+
models = ARGV.dup
|
30
|
+
models.shift
|
31
|
+
models.reject!{|m| m.match(/^(.*)=/)}
|
32
|
+
if models.empty?
|
33
|
+
begin
|
34
|
+
Dir.chdir(model_dir) do
|
35
|
+
models = Dir["**/*.rb"]
|
36
|
+
end
|
37
|
+
rescue SystemCallError
|
38
|
+
puts "No models found in directory '#{model_dir}'."
|
39
|
+
exit 1;
|
40
|
+
end
|
41
|
+
end
|
42
|
+
models
|
43
|
+
end
|
44
|
+
|
45
|
+
# Retrieve the classes belonging to the model names we're asked to process
|
46
|
+
# Check for namespaced models in subdirectories as well as models
|
47
|
+
# in subdirectories without namespacing.
|
48
|
+
def get_model_class(file)
|
49
|
+
require File.expand_path("#{model_dir}/#{file}") # this is for non-rails projects, which don't get Rails auto-require magic
|
50
|
+
model = file.gsub(/\.rb$/, '').camelize
|
51
|
+
parts = model.split('::')
|
52
|
+
begin
|
53
|
+
parts.inject(Object) {|klass, part| klass.const_get(part) }
|
54
|
+
rescue LoadError, NameError
|
55
|
+
Object.const_get(parts.last)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# create or delete parent model fields, such as is_locale_{locale}
|
60
|
+
def make_parent_checkers(klass, connect)
|
61
|
+
columns = connect.columns(klass.table_name)
|
62
|
+
|
63
|
+
Globalize.available_locales.each do |locale|
|
64
|
+
name = "is_locale_#{locale}"
|
65
|
+
|
66
|
+
unless columns.map(&:name).include?(name)
|
67
|
+
connect.add_column klass.table_name, name, :boolean, :default => false
|
68
|
+
show_log("add column", klass.table_name, name, :boolean)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def make_up(klass)
|
74
|
+
conn = klass.connection
|
75
|
+
table_name = klass.translations_table_name
|
76
|
+
|
77
|
+
make_parent_checkers(klass, conn)
|
78
|
+
|
79
|
+
if conn.table_exists?(table_name) # translated table exits
|
80
|
+
columns = conn.columns(table_name)
|
81
|
+
|
82
|
+
klass.translated_columns_hash.each do |key, value|
|
83
|
+
columns.each do |column|
|
84
|
+
if column.name.to_sym == key && column.type != value
|
85
|
+
conn.change_column table_name, key, value
|
86
|
+
show_log("change column", table_name, key, value)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
unless columns.map(&:name).include?(key.to_s)
|
91
|
+
conn.add_column table_name, key, value
|
92
|
+
show_log("add column", table_name, key, value)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
columns.each do |column|
|
97
|
+
if !klass.translated_attribute_names.include?(column.name.to_sym) && [:string, :text].include?(column.type) && column.name != "locale"
|
98
|
+
conn.remove_column table_name, column.name
|
99
|
+
show_log("remove column", table_name, column.name)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
else
|
103
|
+
klass.create_translation_table!(klass.translated_columns_hash)
|
104
|
+
show_log("create table", table_name)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def make_down(klass)
|
109
|
+
if klass.connection.table_exists?(klass.translations_table_name)
|
110
|
+
klass.drop_translation_table!
|
111
|
+
show_log("drop table", klass.translations_table_name)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def init(kind)
|
116
|
+
get_model_files.each do |file|
|
117
|
+
begin
|
118
|
+
klass = get_model_class(file)
|
119
|
+
if klass < ::ActiveRecord::Base && !klass.abstract_class? && klass.respond_to?(:translated_attribute_names)
|
120
|
+
case kind
|
121
|
+
when :up then make_up(klass)
|
122
|
+
when :down then make_down(klass)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
rescue Exception => e
|
126
|
+
puts "Unable to #{kind} #{file}: #{e.inspect}"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Convert string "title:string" to hash { :title => :string }
|
132
|
+
# Convert array ["title:string", "content:text"] to hash { :title => :string, :content => :text }
|
133
|
+
def convert_columns(value)
|
134
|
+
[value].flatten.inject({}) do |hash, schema|
|
135
|
+
arr = schema.to_s.split(':')
|
136
|
+
hash[arr.first.to_sym] = (arr.size == 1 ? :text : arr.last.to_sym)
|
137
|
+
hash
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'paper_trail'
|
2
|
+
|
3
|
+
module Globalize
|
4
|
+
module Versioning
|
5
|
+
module PaperTrail
|
6
|
+
# At present this isn't used but we may use something similar in paper trail
|
7
|
+
# shortly, so leaving it around to reference easily.
|
8
|
+
#def versioned_columns
|
9
|
+
#super + self.class.translated_attribute_names
|
10
|
+
#end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
ActiveRecord::Base.class_eval do
|
16
|
+
class << self
|
17
|
+
def has_paper_trail_with_globalize(*args)
|
18
|
+
has_paper_trail_without_globalize(*args)
|
19
|
+
include Globalize::Versioning::PaperTrail
|
20
|
+
end
|
21
|
+
alias_method_chain :has_paper_trail, :globalize
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
Version.class_eval do
|
26
|
+
|
27
|
+
before_save do |version|
|
28
|
+
version.locale = Globalize.locale.to_s
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.locale_conditions_to_sql
|
32
|
+
"locale = '#{Globalize.locale.to_s}'"
|
33
|
+
end
|
34
|
+
|
35
|
+
scope :for_this_locale, lambda{ { :conditions => locale_conditions_to_sql } }
|
36
|
+
|
37
|
+
def sibling_versions_with_locales
|
38
|
+
sibling_versions_without_locales.for_this_locale
|
39
|
+
end
|
40
|
+
alias_method_chain :sibling_versions, :locales
|
41
|
+
end
|
data/lib/globalize3.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'globalize'
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# A simple exception handler that behaves like the default exception handler
|
2
|
+
# but additionally logs missing translations to a given log.
|
3
|
+
#
|
4
|
+
# Useful for identifying missing translations during testing.
|
5
|
+
#
|
6
|
+
# E.g.
|
7
|
+
#
|
8
|
+
# require 'globalize/i18n/missing_translations_log_handler'
|
9
|
+
# I18n.missing_translations_logger = RAILS_DEFAULT_LOGGER
|
10
|
+
# I18n.exception_handler = :missing_translations_log_handler
|
11
|
+
#
|
12
|
+
# To set up a different log file:
|
13
|
+
#
|
14
|
+
# logger = Logger.new("#{RAILS_ROOT}/log/missing_translations.log")
|
15
|
+
# I18n.missing_translations_logger = logger
|
16
|
+
|
17
|
+
module I18n
|
18
|
+
@@missing_translations_logger = nil
|
19
|
+
|
20
|
+
class << self
|
21
|
+
def missing_translations_logger
|
22
|
+
@@missing_translations_logger ||= begin
|
23
|
+
require 'logger' unless defined?(Logger)
|
24
|
+
Logger.new(STDOUT)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def missing_translations_logger=(logger)
|
29
|
+
@@missing_translations_logger = logger
|
30
|
+
end
|
31
|
+
|
32
|
+
def missing_translations_log_handler(exception, locale, key, options)
|
33
|
+
if MissingTranslationData === exception
|
34
|
+
missing_translations_logger.warn(exception.message)
|
35
|
+
return exception.message
|
36
|
+
else
|
37
|
+
raise exception
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# A simple exception handler that behaves like the default exception handler
|
2
|
+
# but also raises on missing translations.
|
3
|
+
#
|
4
|
+
# Useful for identifying missing translations during testing.
|
5
|
+
#
|
6
|
+
# E.g.
|
7
|
+
#
|
8
|
+
# require 'globalize/i18n/missing_translations_raise_handler'
|
9
|
+
# I18n.exception_handler = :missing_translations_raise_handler
|
10
|
+
module I18n
|
11
|
+
class << self
|
12
|
+
def missing_translations_raise_handler(exception, locale, key, options)
|
13
|
+
raise exception
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
I18n.exception_handler = :missing_translations_raise_handler
|
19
|
+
|
20
|
+
ActionView::Helpers::TranslationHelper.module_eval do
|
21
|
+
def translate(key, options = {})
|
22
|
+
I18n.translate(key, options)
|
23
|
+
end
|
24
|
+
alias :t :translate
|
25
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'active_record/attribute_methods/query'
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module AttributeMethods
|
5
|
+
module Query
|
6
|
+
def query_attribute(attr_name)
|
7
|
+
unless value = read_attribute(attr_name)
|
8
|
+
false
|
9
|
+
else
|
10
|
+
column = self.class.columns_hash[attr_name]
|
11
|
+
if column.nil?
|
12
|
+
|
13
|
+
# TODO submit a rails patch
|
14
|
+
|
15
|
+
# not sure what active_record tests say but i guess this should mean:
|
16
|
+
# call to_i and check zero? if the value is a Numeric or starts with
|
17
|
+
# a digit, so it can meaningfully be typecasted by to_i
|
18
|
+
|
19
|
+
# if Numeric === value || value !~ /[^0-9]/
|
20
|
+
if Numeric === value || value.to_s =~ /^[0-9]/
|
21
|
+
!value.to_i.zero?
|
22
|
+
else
|
23
|
+
return false if ActiveRecord::ConnectionAdapters::Column::FALSE_VALUES.include?(value)
|
24
|
+
!value.blank?
|
25
|
+
end
|
26
|
+
elsif column.number?
|
27
|
+
!value.zero?
|
28
|
+
else
|
29
|
+
!value.blank?
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'active_record/serializers/xml_serializer'
|
2
|
+
|
3
|
+
ActiveRecord::XmlSerializer::Attribute.class_eval do
|
4
|
+
def compute_type_with_translations
|
5
|
+
klass = @serializable.class
|
6
|
+
if klass.translates? && klass.translated_attribute_names.include?(name.to_sym)
|
7
|
+
:string
|
8
|
+
else
|
9
|
+
compute_type_without_translations
|
10
|
+
end
|
11
|
+
end
|
12
|
+
alias_method_chain :compute_type, :translations
|
13
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
namespace :db do
|
2
|
+
namespace :globalize do
|
3
|
+
desc "Synchronize translated columns with database (create/drop table or add/change/remove column)"
|
4
|
+
task :up => :environment do
|
5
|
+
Globalize::Utils.init(:up)
|
6
|
+
end
|
7
|
+
|
8
|
+
desc "Drop all globalize translations tables (but be careful: non-refundable)"
|
9
|
+
task :down => :environment do
|
10
|
+
Globalize::Utils.init(:down)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
data/test/all.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Dir[File.expand_path('../**/*_test.rb', __FILE__)].each { |file| require file }
|
data/test/data/models.rb
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
# this little innocent class here makes 0.0.9 test fail
|
2
|
+
class Translation < ActiveRecord::Base
|
3
|
+
end
|
4
|
+
|
5
|
+
class Post < ActiveRecord::Base
|
6
|
+
translates :title, :content, :published, :published_at, :versioning => true
|
7
|
+
validates_presence_of :title
|
8
|
+
scope :with_some_title, :conditions => { :title => 'some_title' }
|
9
|
+
end
|
10
|
+
|
11
|
+
class PostTranslation < ActiveRecord::Base
|
12
|
+
def existing_method ; end
|
13
|
+
end
|
14
|
+
|
15
|
+
class ReloadingPost < Post
|
16
|
+
after_create { reload }
|
17
|
+
end
|
18
|
+
|
19
|
+
class Blog < ActiveRecord::Base
|
20
|
+
has_many :posts, :order => 'id ASC'
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
class Validatee < ActiveRecord::Base
|
25
|
+
translates :string
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
class Parent < ActiveRecord::Base
|
30
|
+
translates :content
|
31
|
+
end
|
32
|
+
|
33
|
+
class Child < Parent
|
34
|
+
end
|
35
|
+
|
36
|
+
class Comment < ActiveRecord::Base
|
37
|
+
validates_presence_of :content
|
38
|
+
belongs_to :post
|
39
|
+
end
|
40
|
+
|
41
|
+
class TranslatedComment < Comment
|
42
|
+
translates :content
|
43
|
+
end
|
44
|
+
|
45
|
+
class User < ActiveRecord::Base
|
46
|
+
translates :name
|
47
|
+
validates_presence_of :name, :email
|
48
|
+
end
|
49
|
+
|
50
|
+
class Migrated < ActiveRecord::Base
|
51
|
+
translates :name
|
52
|
+
end
|
53
|
+
|
54
|
+
class MigratedWithUltraLongModelName < ActiveRecord::Base
|
55
|
+
translates :name
|
56
|
+
end
|
57
|
+
|
58
|
+
class UppercaseTableName < ActiveRecord::Base
|
59
|
+
set_table_name "UPPERCASE_TABLE_NAME"
|
60
|
+
translates :name
|
61
|
+
end
|
62
|
+
|
63
|
+
class Untranslated < ActiveRecord::Base
|
64
|
+
end
|
65
|
+
|
66
|
+
class Task < ActiveRecord::Base
|
67
|
+
translates :name, :fallbacks_for_empty_translations => true
|
68
|
+
end
|