kriss-gettext_i18n 0.2.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.
- data/CHANGELOG.rdoc +8 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +78 -0
- data/VERSION.yml +4 -0
- data/gettext_i18n.gemspec +86 -0
- data/init.rb +1 -0
- data/lib/gettext_i18n.rb +25 -0
- data/lib/globalize/backend/gettext.rb +26 -0
- data/lib/globalize/i18n.rb +46 -0
- data/lib/rails/extend.rb +7 -0
- data/lib/ruby/string.rb +15 -0
- data/lib/ruby/symbol.rb +15 -0
- data/test/gettext_i18n_test.rb +114 -0
- data/test/locales/en.yml +2 -0
- data/test/locales/pl.yml +6 -0
- data/test/locales/root.yml +2 -0
- data/test/test_helper.rb +13 -0
- data/vendor/globalize2/LICENSE +21 -0
- data/vendor/globalize2/README.textile +202 -0
- data/vendor/globalize2/Rakefile +22 -0
- data/vendor/globalize2/generators/db_backend.rb +0 -0
- data/vendor/globalize2/generators/templates/db_backend_migration.rb +25 -0
- data/vendor/globalize2/init.rb +10 -0
- data/vendor/globalize2/lib/globalize/backend/chain.rb +102 -0
- data/vendor/globalize2/lib/globalize/backend/pluralizing.rb +37 -0
- data/vendor/globalize2/lib/globalize/backend/static.rb +59 -0
- data/vendor/globalize2/lib/globalize/i18n/missing_translations_log_handler.rb +41 -0
- data/vendor/globalize2/lib/globalize/i18n/missing_translations_raise_handler.rb +27 -0
- data/vendor/globalize2/lib/globalize/load_path.rb +63 -0
- data/vendor/globalize2/lib/globalize/locale/fallbacks.rb +63 -0
- data/vendor/globalize2/lib/globalize/locale/language_tag.rb +81 -0
- data/vendor/globalize2/lib/globalize/model/active_record/adapter.rb +96 -0
- data/vendor/globalize2/lib/globalize/model/active_record/translated.rb +161 -0
- data/vendor/globalize2/lib/globalize/model/active_record.rb +38 -0
- data/vendor/globalize2/lib/globalize/translation.rb +32 -0
- data/vendor/globalize2/lib/locale/root.yml +3 -0
- data/vendor/globalize2/lib/rails_edge_load_path_patch.rb +40 -0
- data/vendor/globalize2/notes.textile +51 -0
- data/vendor/globalize2/test/backends/chained_test.rb +175 -0
- data/vendor/globalize2/test/backends/pluralizing_test.rb +63 -0
- data/vendor/globalize2/test/backends/static_test.rb +143 -0
- data/vendor/globalize2/test/data/locale/all.yml +2 -0
- data/vendor/globalize2/test/data/locale/de-DE.yml +2 -0
- data/vendor/globalize2/test/data/locale/en-US/module.yml +2 -0
- data/vendor/globalize2/test/data/locale/en-US.yml +2 -0
- data/vendor/globalize2/test/data/locale/fi-FI/module.yml +2 -0
- data/vendor/globalize2/test/data/locale/root.yml +0 -0
- data/vendor/globalize2/test/data/no_globalize_schema.rb +11 -0
- data/vendor/globalize2/test/data/post.rb +24 -0
- data/vendor/globalize2/test/data/schema.rb +39 -0
- data/vendor/globalize2/test/i18n/missing_translations_test.rb +36 -0
- data/vendor/globalize2/test/load_path_test.rb +49 -0
- data/vendor/globalize2/test/locale/fallbacks_test.rb +154 -0
- data/vendor/globalize2/test/locale/language_tag_test.rb +130 -0
- data/vendor/globalize2/test/model/active_record/migration_test.rb +78 -0
- data/vendor/globalize2/test/model/active_record/sti_translated_test.rb +75 -0
- data/vendor/globalize2/test/model/active_record/translated_test.rb +458 -0
- data/vendor/globalize2/test/test_helper.rb +36 -0
- data/vendor/globalize2/test/translation_test.rb +54 -0
- metadata +121 -0
@@ -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,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,161 @@
|
|
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
|
+
|
103
|
+
self.connection.add_index(
|
104
|
+
translation_table_name, "#{self.table_name.singularize}_id"
|
105
|
+
)
|
106
|
+
end
|
107
|
+
|
108
|
+
def drop_translation_table!
|
109
|
+
translation_table_name = self.name.underscore + '_translations'
|
110
|
+
self.connection.remove_index(
|
111
|
+
translation_table_name, "#{self.table_name.singularize}_id"
|
112
|
+
)
|
113
|
+
self.connection.drop_table translation_table_name
|
114
|
+
end
|
115
|
+
|
116
|
+
private
|
117
|
+
|
118
|
+
def i18n_attr(attribute_name)
|
119
|
+
self.base_class.name.underscore + "_translations.#{attribute_name}"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
module InstanceMethods
|
124
|
+
def reload(options = nil)
|
125
|
+
globalize.clear
|
126
|
+
|
127
|
+
# clear all globalized attributes
|
128
|
+
# TODO what's the best way to handle this?
|
129
|
+
self.class.globalize_options[:translated_attributes].each do |attr|
|
130
|
+
@attributes.delete attr.to_s
|
131
|
+
end
|
132
|
+
|
133
|
+
super options
|
134
|
+
end
|
135
|
+
|
136
|
+
def globalize
|
137
|
+
@globalize ||= Adapter.new self
|
138
|
+
end
|
139
|
+
|
140
|
+
def update_globalize_record
|
141
|
+
globalize.update_translations!
|
142
|
+
end
|
143
|
+
|
144
|
+
def translated_locales
|
145
|
+
globalize_translations.scoped(:select => 'DISTINCT locale').map {|gt| gt.locale.to_sym }
|
146
|
+
end
|
147
|
+
|
148
|
+
def set_translations options
|
149
|
+
options.keys.each do |key|
|
150
|
+
|
151
|
+
translation = globalize_translations.find_by_locale(key.to_s) ||
|
152
|
+
globalize_translations.build(:locale => key.to_s)
|
153
|
+
translation.update_attributes!(options[key])
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
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,32 @@
|
|
1
|
+
module Globalize
|
2
|
+
# Translations are simple value objects that carry some context information
|
3
|
+
# alongside the actual translation string.
|
4
|
+
|
5
|
+
class Translation < String
|
6
|
+
class Attribute < Translation
|
7
|
+
attr_accessor :requested_locale, :locale, :key
|
8
|
+
end
|
9
|
+
|
10
|
+
class Static < Translation
|
11
|
+
attr_accessor :requested_locale, :locale, :key, :options, :plural_key, :original
|
12
|
+
|
13
|
+
def initialize(string, meta = nil)
|
14
|
+
self.original = string
|
15
|
+
super
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(string, meta = nil)
|
20
|
+
set_meta meta
|
21
|
+
super string
|
22
|
+
end
|
23
|
+
|
24
|
+
def fallback?
|
25
|
+
locale.to_sym != requested_locale.to_sym
|
26
|
+
end
|
27
|
+
|
28
|
+
def set_meta(meta)
|
29
|
+
meta.each {|name, value| send :"#{name}=", value } if meta
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module I18n
|
2
|
+
@@load_path = nil
|
3
|
+
@@default_locale = :'en-US'
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def load_path
|
7
|
+
@@load_path ||= []
|
8
|
+
end
|
9
|
+
|
10
|
+
def load_path=(load_path)
|
11
|
+
@@load_path = load_path
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
I18n::Backend::Simple.module_eval do
|
17
|
+
def initialized?
|
18
|
+
@initialized ||= false
|
19
|
+
end
|
20
|
+
|
21
|
+
protected
|
22
|
+
|
23
|
+
def init_translations
|
24
|
+
load_translations(*I18n.load_path)
|
25
|
+
@initialized = true
|
26
|
+
end
|
27
|
+
|
28
|
+
def lookup(locale, key, scope = [])
|
29
|
+
return unless key
|
30
|
+
init_translations unless initialized?
|
31
|
+
keys = I18n.send :normalize_translation_keys, locale, key, scope
|
32
|
+
keys.inject(translations){|result, k| result[k.to_sym] or return nil }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
rails_dir = File.expand_path "#{File.dirname(__FILE__)}/../../../rails/"
|
37
|
+
paths = %w(actionpack/lib/action_view/locale/en-US.yml
|
38
|
+
activerecord/lib/active_record/locale/en-US.yml
|
39
|
+
activesupport/lib/active_support/locale/en-US.yml)
|
40
|
+
paths.each{|path| I18n.load_path << "#{rails_dir}/#{path}" }
|
@@ -0,0 +1,51 @@
|
|
1
|
+
Stopped DB Backend in the middle, here's where we left off:
|
2
|
+
|
3
|
+
h1. Some Notes
|
4
|
+
|
5
|
+
* Started doing the migration generator in generators/db_backend.rb
|
6
|
+
* Translation keys will be in dotted string format
|
7
|
+
* Question: Do we need a plural_key column, or can we build it in to the dotted key?
|
8
|
+
* We will probably have to code the following methods from scratch, to optimize db calls:
|
9
|
+
** translate
|
10
|
+
** localize
|
11
|
+
** pluralize
|
12
|
+
* We should refactor @interpolation@ code so that it can be included into backend code without inheriting SimpleBackend
|
13
|
+
** Rationale: interpolation is something done entirely after a string is fetched from the data store
|
14
|
+
** Alternately, it could be done from within the I18n module
|
15
|
+
|
16
|
+
h1. Schema
|
17
|
+
|
18
|
+
There will be two db tables.
|
19
|
+
|
20
|
+
# globalize_translations will have: locale, key, translation, created_at, updated_at.
|
21
|
+
# globalize_translations_map will have: key, translation_id.
|
22
|
+
|
23
|
+
globalize_translations_map will let us easily fetch entire sub-trees of namespaces.
|
24
|
+
However, this table may not be necessary, as it may be feasible to just use key LIKE "some.namespace.%".
|
25
|
+
|
26
|
+
h1. Caching
|
27
|
+
|
28
|
+
We'll almost certainly want to implement caching in the backend. Should probably be a customized
|
29
|
+
implementation based on the Rails caching mechanism, to support memcached, etc.
|
30
|
+
|
31
|
+
h1. Queries
|
32
|
+
|
33
|
+
We'll want to pull in lots of stuff at once and return a single translation based on some
|
34
|
+
quick Ruby selection. The query will look something like this:
|
35
|
+
|
36
|
+
<pre>
|
37
|
+
<code>
|
38
|
+
SELECT * FROM globalize_translations
|
39
|
+
WHERE locale in (<fallbacks>) AND
|
40
|
+
key IN (key, default_key)
|
41
|
+
</code>
|
42
|
+
</pre>
|
43
|
+
|
44
|
+
The Ruby code would then pick the first translation that satisfies a fallback, in fallback order.
|
45
|
+
Of course, the records with the supplied key would take precedence of those with the default key.
|
46
|
+
|
47
|
+
h1. Misc
|
48
|
+
|
49
|
+
We should revisit the :zero plural code. On the one hand it's certainly useful for
|
50
|
+
many apps in many languages. On the other hand it's not mentioned in CLDR, and not a real
|
51
|
+
concept in language pluralization. Right now, I'm feeling it's still a good idea to keep it in.
|
@@ -0,0 +1,175 @@
|
|
1
|
+
require File.join( File.dirname(__FILE__), '..', 'test_helper' )
|
2
|
+
require 'globalize/backend/chain'
|
3
|
+
|
4
|
+
module Globalize
|
5
|
+
module Backend
|
6
|
+
class Dummy
|
7
|
+
def translate(locale, key, options = {})
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class ChainedTest < ActiveSupport::TestCase
|
14
|
+
|
15
|
+
test "instantiates a chained backend and sets test as backend" do
|
16
|
+
assert_nothing_raised { I18n.chain_backends }
|
17
|
+
assert_instance_of Globalize::Backend::Chain, I18n.backend
|
18
|
+
end
|
19
|
+
|
20
|
+
test "passes all given arguments to the chained backends #initialize method" do
|
21
|
+
Globalize::Backend::Chain.expects(:new).with(:spec, :simple)
|
22
|
+
I18n.chain_backends :spec, :simple
|
23
|
+
end
|
24
|
+
|
25
|
+
test "passes all given arguments to #add assuming that they are backends" do
|
26
|
+
# no idea how to spec that
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class AddChainedTest < ActiveSupport::TestCase
|
31
|
+
def setup
|
32
|
+
I18n.backend = Globalize::Backend::Chain.new
|
33
|
+
end
|
34
|
+
|
35
|
+
test "accepts an instance of a backend" do
|
36
|
+
assert_nothing_raised { I18n.backend.add Globalize::Backend::Dummy.new }
|
37
|
+
assert_instance_of Globalize::Backend::Dummy, I18n.backend.send(:backends).first
|
38
|
+
end
|
39
|
+
|
40
|
+
test "accepts a class and instantiates the backend" do
|
41
|
+
assert_nothing_raised { I18n.backend.add Globalize::Backend::Dummy }
|
42
|
+
assert_instance_of Globalize::Backend::Dummy, I18n.backend.send(:backends).first
|
43
|
+
end
|
44
|
+
|
45
|
+
test "accepts a symbol, constantizes test as a backend class and instantiates the backend" do
|
46
|
+
assert_nothing_raised { I18n.backend.add :dummy }
|
47
|
+
assert_instance_of Globalize::Backend::Dummy, I18n.backend.send(:backends).first
|
48
|
+
end
|
49
|
+
|
50
|
+
test "accepts any number of backend instances, classes or symbols" do
|
51
|
+
assert_nothing_raised { I18n.backend.add Globalize::Backend::Dummy.new, Globalize::Backend::Dummy, :dummy }
|
52
|
+
assert_instance_of Globalize::Backend::Dummy, I18n.backend.send(:backends).first
|
53
|
+
assert_equal [ Globalize::Backend::Dummy, Globalize::Backend::Dummy, Globalize::Backend::Dummy ],
|
54
|
+
I18n.backend.send(:backends).map{|backend| backend.class }
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
class TranslateChainedTest < ActiveSupport::TestCase
|
60
|
+
def setup
|
61
|
+
I18n.locale = :en
|
62
|
+
I18n.backend = Globalize::Backend::Chain.new
|
63
|
+
@first_backend = I18n::Backend::Simple.new
|
64
|
+
@last_backend = I18n::Backend::Simple.new
|
65
|
+
I18n.backend.add @first_backend
|
66
|
+
I18n.backend.add @last_backend
|
67
|
+
end
|
68
|
+
|
69
|
+
test "delegates #translate to all backends in the order they were added" do
|
70
|
+
@first_backend.expects(:translate).with(:en, :foo, {})
|
71
|
+
@last_backend.expects(:translate).with(:en, :foo, {})
|
72
|
+
I18n.translate :foo
|
73
|
+
end
|
74
|
+
|
75
|
+
test "returns the result from #translate from the first backend if test's not nil" do
|
76
|
+
@first_backend.store_translations :en, {:foo => 'foo from first backend'}
|
77
|
+
@last_backend.store_translations :en, {:foo => 'foo from last backend'}
|
78
|
+
result = I18n.translate :foo
|
79
|
+
assert_equal 'foo from first backend', result
|
80
|
+
end
|
81
|
+
|
82
|
+
test "returns the result from #translate from the second backend if the first one returned nil" do
|
83
|
+
@first_backend.store_translations :en, {}
|
84
|
+
@last_backend.store_translations :en, {:foo => 'foo from last backend'}
|
85
|
+
result = I18n.translate :foo
|
86
|
+
assert_equal 'foo from last backend', result
|
87
|
+
end
|
88
|
+
|
89
|
+
test "looks up a namespace from all backends and merges them (if a result is a hash and no count option is present)" do
|
90
|
+
@first_backend.store_translations :en, {:foo => {:bar => 'bar from first backend'}}
|
91
|
+
@last_backend.store_translations :en, {:foo => {:baz => 'baz from last backend'}}
|
92
|
+
result = I18n.translate :foo
|
93
|
+
assert_equal( {:bar => 'bar from first backend', :baz => 'baz from last backend'}, result )
|
94
|
+
end
|
95
|
+
|
96
|
+
test "raises a MissingTranslationData exception if no translation was found" do
|
97
|
+
assert_raise( I18n::MissingTranslationData ) { I18n.translate :not_here, :raise => true }
|
98
|
+
end
|
99
|
+
|
100
|
+
test "raises an InvalidLocale exception if the locale is nil" do
|
101
|
+
assert_raise( I18n::InvalidLocale ) { Globalize::Backend::Chain.new.translate nil, :foo }
|
102
|
+
end
|
103
|
+
|
104
|
+
test "bulk translates a number of keys from different backends" do
|
105
|
+
@first_backend.store_translations :en, {:foo => 'foo from first backend'}
|
106
|
+
@last_backend.store_translations :en, {:bar => 'bar from last backend'}
|
107
|
+
result = I18n.translate [:foo, :bar]
|
108
|
+
assert_equal( ['foo from first backend', 'bar from last backend'], result )
|
109
|
+
end
|
110
|
+
|
111
|
+
test "still calls #translate on all the backends" do
|
112
|
+
@last_backend.expects :translate
|
113
|
+
I18n.translate :not_here, :default => 'default'
|
114
|
+
end
|
115
|
+
|
116
|
+
test "returns a given default string when no backend returns a translation" do
|
117
|
+
result = I18n.translate :not_here, :default => 'default'
|
118
|
+
assert_equal 'default', result
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
122
|
+
|
123
|
+
class CustomLocalizeBackend < I18n::Backend::Simple
|
124
|
+
def localize(locale, object, format = :default)
|
125
|
+
"result from custom localize backend" if locale == 'custom'
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
class LocalizeChainedTest < ActiveSupport::TestCase
|
130
|
+
def setup
|
131
|
+
I18n.locale = :en
|
132
|
+
I18n.backend = Globalize::Backend::Chain.new
|
133
|
+
@first_backend = CustomLocalizeBackend.new
|
134
|
+
@last_backend = I18n::Backend::Simple.new
|
135
|
+
I18n.backend.add @first_backend
|
136
|
+
I18n.backend.add @last_backend
|
137
|
+
@time = Time.now
|
138
|
+
end
|
139
|
+
|
140
|
+
test "delegates #localize to all backends in the order they were added" do
|
141
|
+
@first_backend.expects(:localize).with(:en, @time, :default)
|
142
|
+
@last_backend.expects(:localize).with(:en, @time, :default)
|
143
|
+
I18n.localize @time
|
144
|
+
end
|
145
|
+
|
146
|
+
test "returns the result from #localize from the first backend if test's not nil" do
|
147
|
+
@last_backend.expects(:localize).never
|
148
|
+
result = I18n.localize @time, :locale => 'custom'
|
149
|
+
assert_equal 'result from custom localize backend', result
|
150
|
+
end
|
151
|
+
|
152
|
+
test "returns the result from #localize from the second backend if the first one returned nil" do
|
153
|
+
@last_backend.expects(:localize).returns "value from last backend"
|
154
|
+
result = I18n.localize @time
|
155
|
+
assert_equal 'value from last backend', result
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
class NamespaceChainedTest < ActiveSupport::TestCase
|
160
|
+
def setup
|
161
|
+
@backend = Globalize::Backend::Chain.new
|
162
|
+
end
|
163
|
+
|
164
|
+
test "returns false if the given result is not a Hash" do
|
165
|
+
assert !@backend.send(:namespace_lookup?, 'foo', {})
|
166
|
+
end
|
167
|
+
|
168
|
+
test "returns false if a count option is present" do
|
169
|
+
assert !@backend.send(:namespace_lookup?, {:foo => 'foo'}, {:count => 1})
|
170
|
+
end
|
171
|
+
|
172
|
+
test "returns true if the given result is a Hash AND no count option is present" do
|
173
|
+
assert @backend.send(:namespace_lookup?, {:foo => 'foo'}, {})
|
174
|
+
end
|
175
|
+
end
|