globalize2 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. data/.gitignore +2 -1
  2. data/README.textile +27 -143
  3. data/VERSION +1 -1
  4. data/generators/templates/db_backend_migration.rb +3 -3
  5. data/globalize2.gemspec +30 -49
  6. data/init.rb +1 -8
  7. data/lib/globalize.rb +15 -0
  8. data/lib/globalize/active_record.rb +195 -0
  9. data/lib/globalize/active_record/adapter.rb +80 -0
  10. data/lib/globalize/active_record/attributes.rb +25 -0
  11. data/lib/globalize/active_record/migration.rb +40 -0
  12. data/lib/{globalize/i18n → i18n}/missing_translations_log_handler.rb +8 -8
  13. data/lib/{globalize/i18n → i18n}/missing_translations_raise_handler.rb +3 -5
  14. data/test/active_record/fallbacks_test.rb +102 -0
  15. data/test/{model/active_record → active_record}/migration_test.rb +21 -26
  16. data/test/{model/active_record → active_record}/sti_translated_test.rb +4 -30
  17. data/test/active_record/translates_test.rb +87 -0
  18. data/test/active_record/translation_class_test.rb +30 -0
  19. data/test/active_record/validation_tests.rb +75 -0
  20. data/test/active_record_test.rb +451 -0
  21. data/test/data/models.rb +16 -0
  22. data/test/data/schema.rb +23 -7
  23. data/test/i18n/missing_translations_test.rb +6 -6
  24. data/test/test_helper.rb +55 -15
  25. metadata +33 -46
  26. data/lib/globalize/backend/chain.rb +0 -102
  27. data/lib/globalize/backend/pluralizing.rb +0 -37
  28. data/lib/globalize/backend/static.rb +0 -61
  29. data/lib/globalize/load_path.rb +0 -63
  30. data/lib/globalize/locale/fallbacks.rb +0 -63
  31. data/lib/globalize/locale/language_tag.rb +0 -81
  32. data/lib/globalize/model/active_record.rb +0 -56
  33. data/lib/globalize/model/active_record/adapter.rb +0 -100
  34. data/lib/globalize/model/active_record/translated.rb +0 -174
  35. data/lib/globalize/translation.rb +0 -32
  36. data/lib/locale/root.yml +0 -3
  37. data/lib/rails_edge_load_path_patch.rb +0 -40
  38. data/notes.textile +0 -51
  39. data/test/backends/chained_test.rb +0 -175
  40. data/test/backends/pluralizing_test.rb +0 -63
  41. data/test/backends/static_test.rb +0 -147
  42. data/test/data/locale/all.yml +0 -2
  43. data/test/data/locale/de-DE.yml +0 -2
  44. data/test/data/locale/en-US.yml +0 -2
  45. data/test/data/locale/en-US/module.yml +0 -2
  46. data/test/data/locale/fi-FI/module.yml +0 -2
  47. data/test/data/locale/root.yml +0 -0
  48. data/test/load_path_test.rb +0 -49
  49. data/test/locale/fallbacks_test.rb +0 -154
  50. data/test/locale/language_tag_test.rb +0 -130
  51. data/test/model/active_record/translated_test.rb +0 -487
  52. data/test/translation_test.rb +0 -54
@@ -1,61 +0,0 @@
1
- require 'globalize/backend/pluralizing'
2
- require 'globalize/locale/fallbacks'
3
- require 'globalize/translation'
4
-
5
- module Globalize
6
- module Backend
7
- class Static < Pluralizing
8
- def initialize(*args)
9
- add(*args) unless args.empty?
10
- end
11
-
12
- def translate(locale, key, options = {})
13
- result, default, fallback = nil, options.delete(:default), nil
14
- I18n.fallbacks[locale].each do |fallback|
15
- begin
16
- result = super(fallback, key, options) and break
17
- rescue I18n::MissingTranslationData
18
- end
19
- end
20
- result ||= default locale, default, options
21
-
22
- attrs = {:requested_locale => locale, :locale => fallback, :key => key, :options => options}
23
- translation(result, attrs)
24
- # translation(result, attrs) || raise(I18n::MissingTranslationData.new(locale, key, options))
25
- end
26
-
27
- protected
28
-
29
- alias :orig_interpolate :interpolate unless method_defined? :orig_interpolate
30
- def interpolate(locale, string, values = {})
31
- result = orig_interpolate(locale, string, values)
32
- translation = translation(string)
33
- translation.nil? ? result : translation.replace(result)
34
- end
35
-
36
- def translation(result, meta = nil)
37
- return unless result
38
-
39
- case result
40
- when Numeric
41
- result
42
- when String
43
- result = Translation::Static.new(result) unless result.is_a? Translation::Static
44
- result.set_meta meta
45
- result
46
- when Hash
47
- Hash[*result.map do |key, value|
48
- [key, translation(value, meta)]
49
- end.flatten]
50
- when Array
51
- result.map do |value|
52
- translation(value, meta)
53
- end
54
- else
55
- result
56
- # raise "unexpected translation type: #{result.inspect}"
57
- end
58
- end
59
- end
60
- end
61
- end
@@ -1,63 +0,0 @@
1
- # Locale load_path and Locale loading support.
2
- #
3
- # To use this include the Globalize::LoadPath::I18n module to I18n like this:
4
- #
5
- # I18n.send :include, Globalize::LoadPath::I18n
6
- #
7
- # Clients can add load_paths using:
8
- #
9
- # I18n.load_path.add load_path, 'rb', 'yml' # pass any number of extensions like this
10
- # I18n.load_path << 'path/to/dir' # usage without an extension, defaults to 'yml'
11
- #
12
- # And load locale data using either of:
13
- #
14
- # I18n.load_locales 'en-US', 'de-DE'
15
- # I18n.load_locale 'en-US'
16
- #
17
- # This will lookup all files named like:
18
- #
19
- # 'path/to/dir/all.yml'
20
- # 'path/to/dir/en-US.yml'
21
- # 'path/to/dir/en-US/*.yml'
22
- #
23
- # The filenames will be passed to I18n.load_translations which delegates to
24
- # the backend. So the actual behaviour depends on the implementation of the
25
- # backend. I18n::Backend::Simple will be able to read YAML and plain Ruby
26
- # files. See the documentation for I18n.load_translations for details.
27
-
28
- module Globalize
29
- class LoadPath < Array
30
- def extensions
31
- @extensions ||= ['rb', 'yml']
32
- end
33
- attr_writer :extensions
34
-
35
- def locales
36
- @locales ||= ['*']
37
- end
38
- attr_writer :locales
39
-
40
- def <<(path)
41
- push path
42
- end
43
-
44
- def push(*paths)
45
- super(*paths.map{|path| filenames(path) }.flatten.uniq.sort)
46
- end
47
-
48
- protected
49
-
50
- def filenames(path)
51
- return [path] if File.file? path
52
- patterns(path).map{|pattern| Dir[pattern] }
53
- end
54
-
55
- def patterns(path)
56
- locales.map do |locale|
57
- extensions.map do |extension|
58
- %W(#{path}/all.#{extension} #{path}/#{locale}.#{extension} #{path}/#{locale}/**/*.#{extension})
59
- end
60
- end.flatten.uniq
61
- end
62
- end
63
- end
@@ -1,63 +0,0 @@
1
- require 'globalize/locale/language_tag'
2
-
3
- module I18n
4
- @@fallbacks = nil
5
-
6
- class << self
7
- # Returns the current fallbacks. Defaults to +Globalize::Locale::Fallbacks+.
8
- def fallbacks
9
- @@fallbacks ||= Globalize::Locale::Fallbacks.new
10
- end
11
-
12
- # Sets the current fallbacks. Used to set a custom fallbacks instance.
13
- def fallbacks=(fallbacks)
14
- @@fallbacks = fallbacks
15
- end
16
- end
17
- end
18
-
19
- module Globalize
20
- module Locale
21
- class Fallbacks < Hash
22
- def initialize(*defaults)
23
- @map = {}
24
- map defaults.pop if defaults.last.is_a?(Hash)
25
-
26
- defaults = [I18n.default_locale.to_sym] if defaults.empty?
27
- self.defaults = defaults
28
- end
29
-
30
- def defaults=(defaults)
31
- @defaults = defaults.map{|default| compute(default, false) }.flatten << :root
32
- end
33
- attr_reader :defaults
34
-
35
- def [](tag)
36
- tag = tag.to_sym
37
- has_key?(tag) ? fetch(tag) : store(tag, compute(tag))
38
- end
39
-
40
- def map(mappings)
41
- mappings.each do |from, to|
42
- from, to = from.to_sym, Array(to)
43
- to.each do |to|
44
- @map[from] ||= []
45
- @map[from] << to.to_sym
46
- end
47
- end
48
- end
49
-
50
- protected
51
-
52
- def compute(tags, include_defaults = true)
53
- result = Array(tags).collect do |tag|
54
- tags = LanguageTag::tag(tag.to_sym).parents(true).map! {|t| t.to_sym }
55
- tags.each{|tag| tags += compute(@map[tag]) if @map[tag] }
56
- tags
57
- end.flatten
58
- result.push *defaults if include_defaults
59
- result.uniq
60
- end
61
- end
62
- end
63
- end
@@ -1,81 +0,0 @@
1
- # for specifications see http://en.wikipedia.org/wiki/IETF_language_tag
2
- #
3
- # SimpleParser does not implement advanced usages such as grandfathered tags
4
-
5
- module Globalize
6
- module Locale
7
- module Rfc4646
8
- SUBTAGS = [:language, :script, :region, :variant, :extension, :privateuse, :grandfathered]
9
- FORMATS = {:language => :downcase, :script => :capitalize, :region => :upcase, :variant => :downcase}
10
- end
11
-
12
- class LanguageTag < Struct.new(*Rfc4646::SUBTAGS)
13
- class << self
14
- def parser
15
- @@parser ||= SimpleParser
16
- end
17
-
18
- def parser=(parser)
19
- @@parser = parser
20
- end
21
-
22
- def tag(tag)
23
- matches = parser.match(tag)
24
- new *matches if matches
25
- end
26
- end
27
-
28
- Rfc4646::FORMATS.each do |name, format|
29
- define_method(name) { self[name].send(format) unless self[name].nil? }
30
- end
31
-
32
- def to_sym
33
- to_s.to_sym
34
- end
35
-
36
- def to_s
37
- @tag ||= to_a.compact.join("-")
38
- end
39
-
40
- def to_a
41
- members.collect {|attr| self.send(attr) }
42
- end
43
-
44
- def parent
45
- segs = to_a.compact
46
- segs.length < 2 ? nil : LanguageTag.tag(segs[0..(segs.length-2)].join('-'))
47
- end
48
-
49
- def parents(include_self = true)
50
- result, parent = [], self.dup
51
- result << parent if include_self
52
- while parent = parent.parent
53
- result << parent
54
- end
55
- result
56
- end
57
-
58
- module SimpleParser
59
- PATTERN = %r{\A(?:
60
- ([a-z]{2,3}(?:(?:-[a-z]{3}){0,3})?|[a-z]{4}|[a-z]{5,8}) # language
61
- (?:-([a-z]{4}))? # script
62
- (?:-([a-z]{2}|\d{3}))? # region
63
- (?:-([0-9a-z]{5,8}|\d[0-9a-z]{3}))* # variant
64
- (?:-([0-9a-wyz](?:-[0-9a-z]{2,8})+))* # extension
65
- (?:-(x(?:-[0-9a-z]{1,8})+))?| # privateuse subtag
66
- (x(?:-[0-9a-z]{1,8})+)| # privateuse tag
67
- /* ([a-z]{1,3}(?:-[0-9a-z]{2,8}){1,2}) */ # grandfathered
68
- )\z}xi
69
-
70
- class << self
71
- def match(tag)
72
- c = PATTERN.match(tag.to_s).captures
73
- c[0..4] << (c[5].nil? ? c[6] : c[5]) << c[7] # TODO c[7] is grandfathered, throw a NotImplemented exception here?
74
- rescue
75
- false
76
- end
77
- end
78
- end
79
- end
80
- end
81
- end
@@ -1,56 +0,0 @@
1
- require 'globalize/translation'
2
- require 'globalize/locale/fallbacks'
3
- require 'globalize/model/active_record/adapter'
4
- require 'globalize/model/active_record/translated'
5
-
6
- module Globalize
7
- module Model
8
- module ActiveRecord
9
- class << self
10
- def create_proxy_class(klass)
11
- module_names = klass.name.split('::')
12
- klass_name = module_names.pop
13
- target = module_names.empty? ? Object : module_names.join('::').constantize
14
-
15
- proxy_class_name = "#{klass_name}Translation"
16
- proxy_class = nil
17
- begin
18
- proxy_class = proxy_class_name.constantize
19
- rescue NameError
20
- proxy_class = target.const_set proxy_class_name, Class.new(::ActiveRecord::Base)
21
- end
22
-
23
- proxy_class.instance_eval do
24
- belongs_to "#{klass.name.underscore.gsub('/', '_')}".intern
25
- end
26
- proxy_class.class_eval do
27
- def locale
28
- read_attribute(:locale).to_sym
29
- end
30
-
31
- def locale=(locale)
32
- write_attribute(:locale, locale.to_s)
33
- end
34
- end
35
-
36
- return proxy_class
37
- end
38
-
39
- def define_accessors(klass, attr_names)
40
- attr_names.each do |attr_name|
41
- klass.send :define_method, attr_name, lambda {
42
- globalize.fetch self.class.locale, attr_name
43
- }
44
- klass.send :define_method, "#{attr_name}_before_type_cast", lambda {
45
- globalize.fetch self.class.locale, attr_name
46
- }
47
- klass.send :define_method, "#{attr_name}=", lambda {|val|
48
- globalize.stash self.class.locale, attr_name, val
49
- self[attr_name] = val
50
- }
51
- end
52
- end
53
- end
54
- end
55
- end
56
- end
@@ -1,100 +0,0 @@
1
- module Globalize
2
- module Model
3
- class AttributeStash < Hash
4
- def contains?(locale, attr_name)
5
- locale = locale.to_sym
6
- self[locale] ||= {}
7
- self[locale].has_key? attr_name
8
- end
9
-
10
- def read(locale, attr_name)
11
- locale = locale.to_sym
12
- self[locale] ||= {}
13
- self[locale][attr_name]
14
- end
15
-
16
- def write(locale, attr_name, value)
17
- locale = locale.to_sym
18
- self[locale] ||= {}
19
- self[locale][attr_name] = value
20
- end
21
- end
22
-
23
- class Adapter
24
- def initialize(record)
25
- @record = record
26
-
27
- # TODO what exactly are the roles of cache and stash
28
- @cache = AttributeStash.new
29
- @stash = AttributeStash.new
30
- end
31
-
32
- def fetch(locale, attr_name)
33
- # locale = I18n.locale
34
- is_cached = @cache.contains?(locale, attr_name)
35
- is_cached ? @cache.read(locale, attr_name) : begin
36
- value = fetch_attribute locale, attr_name
37
- @cache.write locale, attr_name, value if value && value.locale == locale
38
- value
39
- end
40
- end
41
-
42
- def stash(locale, attr_name, value)
43
- @stash.write locale, attr_name, value
44
- @cache.write locale, attr_name, value
45
- end
46
-
47
- def update_translations!
48
- @stash.each do |locale, attrs|
49
- translation = @record.globalize_translations.find_or_initialize_by_locale(locale.to_s)
50
- attrs.each{|attr_name, value| translation[attr_name] = value }
51
- translation.save!
52
- end
53
- @stash.clear
54
- end
55
-
56
- # Clears the cache
57
- def clear
58
- @cache.clear
59
- @stash.clear
60
- end
61
-
62
- def clear_cache
63
- @cache.clear
64
- end
65
-
66
- private
67
-
68
- def fetch_attribute(locale, attr_name)
69
- fallbacks = I18n.fallbacks[locale].map{|tag| tag.to_s}.map(&:to_sym)
70
-
71
- # If the translations were included with
72
- # :include => globalize_translations
73
- # there is no need to query them again.
74
- unless @record.globalize_translations.loaded?
75
- translations = @record.globalize_translations.by_locales(fallbacks)
76
- else
77
- translations = @record.globalize_translations
78
- end
79
- result, requested_locale = nil, locale
80
-
81
- # Walk through the fallbacks, starting with the current locale itself, and moving
82
- # to the next best choice, until we find a match.
83
- # Check the @globalize_set_translations cache first to see if we've just changed the
84
- # attribute and not saved yet.
85
- fallbacks.each do |fallback|
86
- # TODO should we be checking stash or just cache?
87
- result = @cache.read(fallback, attr_name) || begin
88
- translation = translations.detect {|tr| tr.locale == fallback }
89
- translation && translation.send(attr_name)
90
- end
91
- if result
92
- locale = fallback
93
- break
94
- end
95
- end
96
- result && Translation::Attribute.new(result, :locale => locale, :requested_locale => requested_locale)
97
- end
98
- end
99
- end
100
- end
@@ -1,174 +0,0 @@
1
- require 'digest/sha1'
2
-
3
- module Globalize
4
- module Model
5
- class MigrationError < StandardError; end
6
- class MigrationMissingTranslatedField < MigrationError; end
7
- class BadMigrationFieldType < MigrationError; end
8
-
9
- module ActiveRecord
10
- module Translated
11
- def self.included(base)
12
- base.extend ActMethods
13
- end
14
-
15
- module ActMethods
16
- def translates(*attr_names)
17
- options = attr_names.extract_options!
18
- options[:translated_attributes] = attr_names
19
-
20
- # Only set up once per class
21
- unless included_modules.include? InstanceMethods
22
- class_inheritable_accessor :globalize_options, :globalize_proxy
23
-
24
- include InstanceMethods
25
- extend ClassMethods
26
-
27
- self.globalize_proxy = Globalize::Model::ActiveRecord.create_proxy_class(self)
28
- has_many(
29
- :globalize_translations,
30
- :class_name => globalize_proxy.name,
31
- :extend => Extensions,
32
- :dependent => :delete_all,
33
- :foreign_key => class_name.foreign_key
34
- )
35
-
36
- after_save :update_globalize_record
37
- end
38
-
39
- self.globalize_options = options
40
- Globalize::Model::ActiveRecord.define_accessors(self, attr_names)
41
-
42
- # Import any callbacks that have been defined by extensions to Globalize2
43
- # and run them.
44
- extend Callbacks
45
- Callbacks.instance_methods.each { |callback| send(callback) }
46
- end
47
-
48
- def locale=(locale)
49
- @@locale = locale
50
- end
51
-
52
- def locale
53
- (defined?(@@locale) && @@locale) || I18n.locale
54
- end
55
- end
56
-
57
- # Dummy Callbacks module. Extensions to Globalize2 can insert methods into here
58
- # and they'll be called at the end of the translates class method.
59
- module Callbacks
60
- end
61
-
62
- # Extension to the has_many :globalize_translations association
63
- module Extensions
64
- def by_locales(locales)
65
- find :all, :conditions => { :locale => locales.map(&:to_s) }
66
- end
67
- end
68
-
69
- module ClassMethods
70
- def method_missing(method, *args)
71
- if method.to_s =~ /^find_by_(\w+)$/ && globalize_options[:translated_attributes].include?($1.to_sym)
72
- find(:first, :joins => :globalize_translations,
73
- :conditions => [ "#{i18n_attr($1)} = ? AND #{i18n_attr('locale')} IN (?)",
74
- args.first,I18n.fallbacks[I18n.locale].map{|tag| tag.to_s}])
75
- else
76
- super
77
- end
78
- end
79
-
80
- def create_translation_table!(fields)
81
- translated_fields = self.globalize_options[:translated_attributes]
82
- translated_fields.each do |f|
83
- raise MigrationMissingTranslatedField, "Missing translated field #{f}" unless fields[f]
84
- end
85
-
86
- fields.each do |name, type|
87
- if translated_fields.include?(name) && ![:string, :text].include?(type)
88
- raise BadMigrationFieldType, "Bad field type for #{name}, should be :string or :text"
89
- end
90
- end
91
-
92
- self.connection.create_table(translation_table_name) do |t|
93
- t.references self.table_name.singularize
94
- t.string :locale
95
- fields.each do |name, type|
96
- t.column name, type
97
- end
98
- t.timestamps
99
- end
100
-
101
- self.connection.add_index(translation_table_name, "#{self.table_name.singularize}_id", :name => translation_index_name)
102
- end
103
-
104
- def set_translation_table_name(table_name)
105
- globalize_proxy.set_table_name(table_name)
106
- end
107
-
108
- def translation_table_name
109
- globalize_proxy.table_name
110
- end
111
-
112
- def translation_index_name
113
- # FIXME what's the max size of an index name?
114
- index_name = "index_#{translation_table_name}_on_#{self.table_name.singularize}_id"
115
- index_name.size < 50 ? index_name : "index_#{Digest::SHA1.hexdigest(index_name)}"
116
- end
117
-
118
- def drop_translation_table!
119
- self.connection.remove_index(translation_table_name, :name => translation_index_name)
120
- self.connection.drop_table translation_table_name
121
- end
122
-
123
- private
124
-
125
- def i18n_attr(attribute_name)
126
- self.base_class.name.underscore.gsub('/', '_') + "_translations.#{attribute_name}"
127
- end
128
- end
129
-
130
- module InstanceMethods
131
- def reload(options = nil)
132
- globalize.clear_cache
133
-
134
- # clear all globalized attributes
135
- # TODO what's the best way to handle this?
136
- self.class.globalize_options[:translated_attributes].each do |attr|
137
- @attributes.delete(attr.to_s)
138
- end
139
-
140
- super(options)
141
- end
142
-
143
- def translated_attributes
144
- self.class.globalize_options[:translated_attributes].inject({}) {|h, tf| h[tf] = send(tf); h }
145
- end
146
-
147
- def globalize
148
- @globalize ||= Adapter.new self
149
- end
150
-
151
- def update_globalize_record
152
- globalize.update_translations!
153
- end
154
-
155
- def translated_locales
156
- globalize_translations.scoped(:select => 'DISTINCT locale').map do |translation|
157
- translation.locale.to_sym
158
- end
159
- end
160
-
161
- def set_translations(options)
162
- options.keys.each do |key|
163
- translation = globalize_translations.find_by_locale(key.to_s) ||
164
- globalize_translations.build(:locale => key.to_s)
165
- translation.update_attributes!(options[key])
166
- end
167
- end
168
- end
169
- end
170
- end
171
- end
172
- end
173
-
174
- ActiveRecord::Base.send(:include, Globalize::Model::ActiveRecord::Translated)