globalize2 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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