galetahub-globalize3 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. data/README.textile +206 -0
  2. data/Rakefile +22 -0
  3. data/lib/globalize.rb +59 -0
  4. data/lib/globalize/active_record.rb +13 -0
  5. data/lib/globalize/active_record/accessors.rb +22 -0
  6. data/lib/globalize/active_record/act_macro.rb +67 -0
  7. data/lib/globalize/active_record/adapter.rb +101 -0
  8. data/lib/globalize/active_record/attributes.rb +27 -0
  9. data/lib/globalize/active_record/class_methods.rb +125 -0
  10. data/lib/globalize/active_record/exceptions.rb +19 -0
  11. data/lib/globalize/active_record/instance_methods.rb +166 -0
  12. data/lib/globalize/active_record/migration.rb +125 -0
  13. data/lib/globalize/active_record/translation.rb +37 -0
  14. data/lib/globalize/engine.rb +17 -0
  15. data/lib/globalize/utils.rb +142 -0
  16. data/lib/globalize/versioning.rb +5 -0
  17. data/lib/globalize/versioning/paper_trail.rb +41 -0
  18. data/lib/globalize3.rb +1 -0
  19. data/lib/globalize3/version.rb +3 -0
  20. data/lib/i18n/missing_translations_log_handler.rb +41 -0
  21. data/lib/i18n/missing_translations_raise_handler.rb +25 -0
  22. data/lib/patches/active_record/query_method.rb +35 -0
  23. data/lib/patches/active_record/xml_attribute_serializer.rb +13 -0
  24. data/lib/tasks/globalize.rake +13 -0
  25. data/test/all.rb +1 -0
  26. data/test/data/models.rb +68 -0
  27. data/test/data/schema.rb +108 -0
  28. data/test/globalize3/attributes_test.rb +133 -0
  29. data/test/globalize3/clone_test.rb +58 -0
  30. data/test/globalize3/dirty_tracking_test.rb +61 -0
  31. data/test/globalize3/dynamic_finders_test.rb +171 -0
  32. data/test/globalize3/fallbacks_test.rb +146 -0
  33. data/test/globalize3/locale_test.rb +81 -0
  34. data/test/globalize3/migration_test.rb +156 -0
  35. data/test/globalize3/set_translations_test.rb +54 -0
  36. data/test/globalize3/translation_class_test.rb +59 -0
  37. data/test/globalize3/validations_test.rb +92 -0
  38. data/test/globalize3/versioning_test.rb +87 -0
  39. data/test/globalize3_test.rb +159 -0
  40. data/test/i18n/missing_translations_test.rb +35 -0
  41. data/test/test_helper.rb +105 -0
  42. 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,5 @@
1
+ module Globalize
2
+ module Versioning
3
+ autoload :PaperTrail, 'globalize/versioning/paper_trail'
4
+ end
5
+ 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
@@ -0,0 +1 @@
1
+ require 'globalize'
@@ -0,0 +1,3 @@
1
+ module Globalize3
2
+ VERSION = '0.2.1'
3
+ end
@@ -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
@@ -0,0 +1 @@
1
+ Dir[File.expand_path('../**/*_test.rb', __FILE__)].each { |file| require file }
@@ -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