simonmenke-globalize2 0.0.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 (43) hide show
  1. data/LICENSE +21 -0
  2. data/README.textile +202 -0
  3. data/generators/db_backend.rb +0 -0
  4. data/generators/templates/db_backend_migration.rb +25 -0
  5. data/init.rb +1 -0
  6. data/lib/globalize/backend/chain.rb +102 -0
  7. data/lib/globalize/backend/pluralizing.rb +37 -0
  8. data/lib/globalize/backend/static.rb +60 -0
  9. data/lib/globalize/i18n/missing_translations_log_handler.rb +41 -0
  10. data/lib/globalize/i18n/missing_translations_raise_handler.rb +27 -0
  11. data/lib/globalize/load_path.rb +63 -0
  12. data/lib/globalize/locale/fallbacks.rb +63 -0
  13. data/lib/globalize/locale/language_tag.rb +81 -0
  14. data/lib/globalize/model/active_record.rb +38 -0
  15. data/lib/globalize/model/active_record/adapter.rb +96 -0
  16. data/lib/globalize/model/active_record/translated.rb +154 -0
  17. data/lib/globalize/translation.rb +32 -0
  18. data/lib/locale/root.yml +3 -0
  19. data/lib/rails_edge_load_path_patch.rb +40 -0
  20. data/notes.textile +51 -0
  21. data/rails/init.rb +9 -0
  22. data/test/backends/chained_test.rb +175 -0
  23. data/test/backends/pluralizing_test.rb +63 -0
  24. data/test/backends/static_test.rb +143 -0
  25. data/test/data/locale/all.yml +2 -0
  26. data/test/data/locale/de-DE.yml +2 -0
  27. data/test/data/locale/en-US.yml +2 -0
  28. data/test/data/locale/en-US/module.yml +2 -0
  29. data/test/data/locale/fi-FI/module.yml +2 -0
  30. data/test/data/locale/root.yml +0 -0
  31. data/test/data/no_globalize_schema.rb +11 -0
  32. data/test/data/post.rb +24 -0
  33. data/test/data/schema.rb +39 -0
  34. data/test/i18n/missing_translations_test.rb +36 -0
  35. data/test/load_path_test.rb +49 -0
  36. data/test/locale/fallbacks_test.rb +154 -0
  37. data/test/locale/language_tag_test.rb +130 -0
  38. data/test/model/active_record/migration_test.rb +73 -0
  39. data/test/model/active_record/sti_translated_test.rb +75 -0
  40. data/test/model/active_record/translated_test.rb +458 -0
  41. data/test/test_helper.rb +26 -0
  42. data/test/translation_test.rb +54 -0
  43. metadata +114 -0
@@ -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,27 @@
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
+
17
+ # self.exception_handler = :missing_translations_raise_handler
18
+ end
19
+
20
+ I18n.exception_handler = :missing_translations_raise_handler
21
+
22
+ ActionView::Helpers::TranslationHelper.module_eval do
23
+ def translate(key, options = {})
24
+ I18n.translate(key, options)
25
+ end
26
+ alias :t :translate
27
+ end
@@ -0,0 +1,63 @@
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
@@ -0,0 +1,63 @@
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
@@ -0,0 +1,81 @@
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
@@ -0,0 +1,38 @@
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
+ Object.const_set "#{klass.name}Translation", Class.new(::ActiveRecord::Base){
12
+ belongs_to "#{klass.name.underscore}".intern
13
+
14
+ def locale
15
+ read_attribute(:locale).to_sym
16
+ end
17
+
18
+ def locale=(locale)
19
+ write_attribute(:locale, locale.to_s)
20
+ end
21
+ }
22
+ end
23
+
24
+ def define_accessors(klass, attr_names)
25
+ attr_names.each do |attr_name|
26
+ klass.send :define_method, attr_name, lambda {
27
+ globalize.fetch self.class.locale, attr_name
28
+ }
29
+ klass.send :define_method, "#{attr_name}=", lambda {|val|
30
+ globalize.stash self.class.locale, attr_name, val
31
+ self[attr_name] = val
32
+ }
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,96 @@
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
+ private
63
+
64
+ def fetch_attribute(locale, attr_name)
65
+ fallbacks = I18n.fallbacks[locale].map{|tag| tag.to_s}.map(&:to_sym)
66
+
67
+ # If the translations were included with
68
+ # :include => globalize_translations
69
+ # there is no need to query them again.
70
+ unless @record.globalize_translations.loaded?
71
+ translations = @record.globalize_translations.by_locales(fallbacks)
72
+ else
73
+ translations = @record.globalize_translations
74
+ end
75
+ result, requested_locale = nil, locale
76
+
77
+ # Walk through the fallbacks, starting with the current locale itself, and moving
78
+ # to the next best choice, until we find a match.
79
+ # Check the @globalize_set_translations cache first to see if we've just changed the
80
+ # attribute and not saved yet.
81
+ fallbacks.each do |fallback|
82
+ # TODO should we be checking stash or just cache?
83
+ result = @stash.read(fallback, attr_name) || begin
84
+ translation = translations.detect {|tr| tr.locale == fallback }
85
+ translation && translation.send(attr_name)
86
+ end
87
+ if result
88
+ locale = fallback
89
+ break
90
+ end
91
+ end
92
+ result && Translation::Attribute.new(result, :locale => locale, :requested_locale => requested_locale)
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,154 @@
1
+ module Globalize
2
+ module Model
3
+
4
+ class MigrationError < StandardError; end
5
+ class UntranslatedMigrationField < MigrationError; 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 {|cb| send cb }
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
+ fields.each do |name, type|
86
+ unless translated_fields.member? name
87
+ raise UntranslatedMigrationField, "Can't migrate untranslated field: #{name}"
88
+ end
89
+ unless [ :string, :text ].member? type
90
+ raise BadMigrationFieldType, "Bad field type for #{name}, should be :string or :text"
91
+ end
92
+ end
93
+ translation_table_name = self.name.underscore + '_translations'
94
+ self.connection.create_table(translation_table_name) do |t|
95
+ t.references self.table_name.singularize
96
+ t.string :locale
97
+ fields.each do |name, type|
98
+ t.column name, type
99
+ end
100
+ t.timestamps
101
+ end
102
+ end
103
+
104
+ def drop_translation_table!
105
+ translation_table_name = self.name.underscore + '_translations'
106
+ self.connection.drop_table translation_table_name
107
+ end
108
+
109
+ private
110
+
111
+ def i18n_attr(attribute_name)
112
+ self.base_class.name.underscore + "_translations.#{attribute_name}"
113
+ end
114
+ end
115
+
116
+ module InstanceMethods
117
+ def reload(options = nil)
118
+ globalize.clear
119
+
120
+ # clear all globalized attributes
121
+ # TODO what's the best way to handle this?
122
+ self.class.globalize_options[:translated_attributes].each do |attr|
123
+ @attributes.delete attr.to_s
124
+ end
125
+
126
+ super options
127
+ end
128
+
129
+ def globalize
130
+ @globalize ||= Adapter.new self
131
+ end
132
+
133
+ def update_globalize_record
134
+ globalize.update_translations!
135
+ end
136
+
137
+ def translated_locales
138
+ globalize_translations.scoped(:select => 'DISTINCT locale').map {|gt| gt.locale.to_sym }
139
+ end
140
+
141
+ def set_translations options
142
+ options.keys.each do |key|
143
+
144
+ translation = globalize_translations.find_by_locale(key.to_s) ||
145
+ globalize_translations.build(:locale => key.to_s)
146
+ translation.update_attributes!(options[key])
147
+ end
148
+ end
149
+
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end