globalize2 0.1.0

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 (47) hide show
  1. data/.gitignore +4 -0
  2. data/LICENSE +21 -0
  3. data/README.textile +202 -0
  4. data/Rakefile +39 -0
  5. data/VERSION +1 -0
  6. data/generators/db_backend.rb +0 -0
  7. data/generators/templates/db_backend_migration.rb +25 -0
  8. data/globalize2.gemspec +100 -0
  9. data/init.rb +8 -0
  10. data/lib/globalize/backend/chain.rb +102 -0
  11. data/lib/globalize/backend/pluralizing.rb +37 -0
  12. data/lib/globalize/backend/static.rb +61 -0
  13. data/lib/globalize/i18n/missing_translations_log_handler.rb +41 -0
  14. data/lib/globalize/i18n/missing_translations_raise_handler.rb +27 -0
  15. data/lib/globalize/load_path.rb +63 -0
  16. data/lib/globalize/locale/fallbacks.rb +63 -0
  17. data/lib/globalize/locale/language_tag.rb +81 -0
  18. data/lib/globalize/model/active_record.rb +56 -0
  19. data/lib/globalize/model/active_record/adapter.rb +100 -0
  20. data/lib/globalize/model/active_record/translated.rb +174 -0
  21. data/lib/globalize/translation.rb +32 -0
  22. data/lib/locale/root.yml +3 -0
  23. data/lib/rails_edge_load_path_patch.rb +40 -0
  24. data/notes.textile +51 -0
  25. data/test/all.rb +2 -0
  26. data/test/backends/chained_test.rb +175 -0
  27. data/test/backends/pluralizing_test.rb +63 -0
  28. data/test/backends/static_test.rb +147 -0
  29. data/test/data/locale/all.yml +2 -0
  30. data/test/data/locale/de-DE.yml +2 -0
  31. data/test/data/locale/en-US.yml +2 -0
  32. data/test/data/locale/en-US/module.yml +2 -0
  33. data/test/data/locale/fi-FI/module.yml +2 -0
  34. data/test/data/locale/root.yml +0 -0
  35. data/test/data/models.rb +40 -0
  36. data/test/data/no_globalize_schema.rb +11 -0
  37. data/test/data/schema.rb +39 -0
  38. data/test/i18n/missing_translations_test.rb +36 -0
  39. data/test/load_path_test.rb +49 -0
  40. data/test/locale/fallbacks_test.rb +154 -0
  41. data/test/locale/language_tag_test.rb +130 -0
  42. data/test/model/active_record/migration_test.rb +123 -0
  43. data/test/model/active_record/sti_translated_test.rb +75 -0
  44. data/test/model/active_record/translated_test.rb +487 -0
  45. data/test/test_helper.rb +36 -0
  46. data/test/translation_test.rb +54 -0
  47. metadata +116 -0
@@ -0,0 +1,37 @@
1
+ require 'i18n/backend/simple'
2
+
3
+ module Globalize
4
+ module Backend
5
+ class Pluralizing < I18n::Backend::Simple
6
+ def pluralize(locale, entry, count)
7
+ return entry unless entry.is_a?(Hash) and count
8
+ key = :zero if count == 0 && entry.has_key?(:zero)
9
+ key ||= pluralizer(locale).call(count)
10
+ raise InvalidPluralizationData.new(entry, count) unless entry.has_key?(key)
11
+ translation entry[key], :plural_key => key
12
+ end
13
+
14
+ def add_pluralizer(locale, pluralizer)
15
+ pluralizers[locale.to_sym] = pluralizer
16
+ end
17
+
18
+ def pluralizer(locale)
19
+ pluralizers[locale.to_sym] || default_pluralizer
20
+ end
21
+
22
+ protected
23
+ def default_pluralizer
24
+ pluralizers[:en]
25
+ end
26
+
27
+ def pluralizers
28
+ @pluralizers ||= { :en => lambda{|n| n == 1 ? :one : :other } }
29
+ end
30
+
31
+ # Overwrite this method to return something other than a String
32
+ def translation(string, attributes)
33
+ string
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,61 @@
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
@@ -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,56 @@
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
@@ -0,0 +1,100 @@
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