t9n 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.
- data/MIT-LICENSE +20 -0
- data/README.markdown +1 -0
- data/generators/t9n/t9n_generator.rb +57 -0
- data/generators/t9n/templates/initializer.rb +8 -0
- data/generators/t9n/templates/language.rb +3 -0
- data/generators/t9n/templates/migrations/create_languages.rb +941 -0
- data/generators/t9n/templates/migrations/create_translations.rb +20 -0
- data/generators/t9n/templates/translation.rb +3 -0
- data/init.rb +1 -0
- data/install.rb +1 -0
- data/lib/t9n.rb +16 -0
- data/lib/t9n/active_record/translatable_attribute.rb +120 -0
- data/lib/t9n/backend/active_record.rb +179 -0
- data/lib/t9n/backend/exception_handler.rb +24 -0
- data/lib/t9n/backend/extension.rb +25 -0
- data/lib/t9n/models/language.rb +41 -0
- data/lib/t9n/models/translation.rb +84 -0
- data/lib/t9n/util.rb +95 -0
- data/lib/tasks/t9n.rake +18 -0
- data/rails/init.rb +1 -0
- metadata +86 -0
@@ -0,0 +1,20 @@
|
|
1
|
+
class CreateTranslations < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :translations do |t|
|
4
|
+
t.references :translatable, :polymorphic => true
|
5
|
+
t.column :locale, :string, :null => false, :limit => 2
|
6
|
+
t.column :key, :string, :null => false, :limit => 255
|
7
|
+
t.column :value, :text
|
8
|
+
t.timestamps
|
9
|
+
end
|
10
|
+
|
11
|
+
add_index :translations, [:translatable_id, :translatable_type], :name => "translations_translatable_index"
|
12
|
+
add_index :translations, [:locale, :key], :name => "translations_locale_key_index"
|
13
|
+
add_index :translations, [:translatable_id, :translatable_type, :locale, :key], :unique => true,
|
14
|
+
:name => "translations_translatable_locale_key_unique_index"
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.down
|
18
|
+
drop_table :translations
|
19
|
+
end
|
20
|
+
end
|
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), "rails/init.rb")
|
data/install.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
puts IO.read(File.join(File.dirname(__FILE__), "README.markdown"))
|
data/lib/t9n.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require "t9n/active_record/translatable_attribute"
|
2
|
+
|
3
|
+
# include translatable attribute
|
4
|
+
ActiveRecord::Base.__send__(:include, Perfectline::T9n::TranslatableAttribute)
|
5
|
+
|
6
|
+
require "t9n/backend/active_record"
|
7
|
+
require "t9n/backend/extension"
|
8
|
+
require "t9n/backend/exception_handler"
|
9
|
+
|
10
|
+
# include hoptoad notifier
|
11
|
+
I18n.__send__(:include, Perfectline::T9n::Backend::ExceptionHandler)
|
12
|
+
I18n.__send__(:include, Perfectline::T9n::Backend::Extension)
|
13
|
+
|
14
|
+
# include needed model logic modules
|
15
|
+
require "t9n/models/language"
|
16
|
+
require "t9n/models/translation"
|
@@ -0,0 +1,120 @@
|
|
1
|
+
module Perfectline
|
2
|
+
module T9n
|
3
|
+
module TranslatableAttribute
|
4
|
+
|
5
|
+
def self.included(base)
|
6
|
+
base.__send__(:extend, ClassMethods)
|
7
|
+
base.__send__(:include, InstanceMethods)
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
|
12
|
+
def translated_attributes
|
13
|
+
@translated_attributes ||= {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def translate_attribute(attribute, options = {})
|
17
|
+
if columns_hash.keys.include?(attribute.to_s)
|
18
|
+
raise ArgumentError, "#{self.name} already has an attribute named :#{attribute}"
|
19
|
+
end
|
20
|
+
|
21
|
+
build_translated_attribute(attribute, options, false)
|
22
|
+
end
|
23
|
+
|
24
|
+
def translate_column(column, options = {})
|
25
|
+
unless columns_hash.keys.include?(column.to_s)
|
26
|
+
raise ArgumentError, "#{self.name} does not respond to column: :#{column}"
|
27
|
+
end
|
28
|
+
|
29
|
+
build_translated_attribute(column, options, true)
|
30
|
+
end
|
31
|
+
|
32
|
+
def build_translated_attribute(attribute, options, real_attribute)
|
33
|
+
|
34
|
+
# build key
|
35
|
+
key = "#{self.table_name}.#{((options.delete(:to) || attribute).to_s.gsub(/_/, "."))}"
|
36
|
+
default = options.delete(:default) || "#{attribute}_default"
|
37
|
+
|
38
|
+
# add the attribute name, default and type
|
39
|
+
translated_attributes.store(attribute, {
|
40
|
+
:key => key,
|
41
|
+
:default => default,
|
42
|
+
:real_attribute => real_attribute
|
43
|
+
})
|
44
|
+
|
45
|
+
# create a separate method for accessing the original column
|
46
|
+
if real_attribute
|
47
|
+
define_method(default) do
|
48
|
+
self[attribute.to_sym]
|
49
|
+
end
|
50
|
+
|
51
|
+
define_method("#{default}=") do |value|
|
52
|
+
self[attribute.to_sym] = value
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
define_method(attribute) do
|
57
|
+
read_translatable_attribute(attribute, I18n.locale.to_s)
|
58
|
+
end
|
59
|
+
|
60
|
+
define_method("#{attribute}=") do |value|
|
61
|
+
write_translatable_attribute(attribute, value, I18n.locale.to_s)
|
62
|
+
end
|
63
|
+
|
64
|
+
unless self.base_class.reflect_on_association(:translations)
|
65
|
+
has_many :translations,
|
66
|
+
:as => :translatable,
|
67
|
+
:dependent => :destroy,
|
68
|
+
:autosave => true
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
module InstanceMethods
|
75
|
+
|
76
|
+
def write_translatable_attribute(attribute, value, locale)
|
77
|
+
options = self.class.translated_attributes.fetch(attribute.to_sym)
|
78
|
+
|
79
|
+
if options.fetch(:real_attribute) && I18n.default_locale.to_s == locale
|
80
|
+
__send__("#{options.fetch(:default)}=", value)
|
81
|
+
else
|
82
|
+
translation = translations.detect{ |t| t.key == options.fetch(:key) && t.locale == locale }
|
83
|
+
translation = translations.build(:key => options.fetch(:key), :locale => locale) if translation.nil?
|
84
|
+
translation.value = value
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def read_translatable_attribute(attribute, locale)
|
89
|
+
options = self.class.translated_attributes.fetch(attribute.to_sym)
|
90
|
+
|
91
|
+
if options.fetch(:real_attribute) && I18n.default_locale.to_s == locale
|
92
|
+
__send__(options.fetch(:default))
|
93
|
+
else
|
94
|
+
translation = translations.detect{ |t| t.key == options.fetch(:key) && t.locale == locale}
|
95
|
+
translation.nil? ? nil : translation.value
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def method_missing(method, *args, &block)
|
100
|
+
if method.to_s.match(/^(#{self.class.translated_attributes.keys.map(&:to_s).join("|")})_([a-z]{2})(=)?$/)
|
101
|
+
if $3 == "="
|
102
|
+
write_translatable_attribute($1, args.first, $2)
|
103
|
+
else
|
104
|
+
read_translatable_attribute($1, $2)
|
105
|
+
end
|
106
|
+
else
|
107
|
+
super
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def respond_to?(method, *args, &block)
|
112
|
+
!method.to_s.match(/^(#{self.class.translated_attributes.keys.map(&:to_s).join("|")})_([a-z]{2})(=)?$/).nil? || super
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
@@ -0,0 +1,179 @@
|
|
1
|
+
module Perfectline
|
2
|
+
module T9n
|
3
|
+
module Backend
|
4
|
+
|
5
|
+
class ActiveRecord < ::I18n::Backend::Simple
|
6
|
+
INTERPOLATION_RESERVED_KEYS = %w(scope default object)
|
7
|
+
|
8
|
+
attr_accessor :default_backend
|
9
|
+
attr_accessor :cache_storage
|
10
|
+
|
11
|
+
def cache_enabled?
|
12
|
+
return !@cache_storage.nil?
|
13
|
+
end
|
14
|
+
|
15
|
+
def cache_storage=(store)
|
16
|
+
@cache_storage = ActiveSupport::Cache.lookup_store(store, {:namespace => "i18n"})
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize
|
20
|
+
@default_backend = I18n::Backend::Simple.new
|
21
|
+
@cache_storage = ActiveSupport::Cache.lookup_store(:mem_cache_store, {:namespace => "i18n"})
|
22
|
+
end
|
23
|
+
|
24
|
+
def reload!
|
25
|
+
# don't do anything as passenger with multiple rails instances will screw up memcached
|
26
|
+
end
|
27
|
+
|
28
|
+
def available_locales
|
29
|
+
begin
|
30
|
+
::Translation.available_locales.map(&:to_sym)
|
31
|
+
rescue
|
32
|
+
@default_backend.available_locales
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def store_translation(*args)
|
37
|
+
raise MethodNotAllowed, "Create translation not allowed for Perfectline::I18n::Backend"
|
38
|
+
end
|
39
|
+
|
40
|
+
def translate(locale, key, options = {})
|
41
|
+
if locale.nil?
|
42
|
+
raise InvalidLocale.new(locale)
|
43
|
+
end
|
44
|
+
|
45
|
+
if key.is_a?(Array)
|
46
|
+
return key.map{ |k| translate(locale, k, options) }
|
47
|
+
end
|
48
|
+
|
49
|
+
count = options[:count]
|
50
|
+
scope = options.delete(:count)
|
51
|
+
default = options.delete(:default)
|
52
|
+
object = options.delete(:object)
|
53
|
+
values = options.reject{ |name, value| INTERPOLATION_RESERVED_KEYS.include?(name.to_s) }
|
54
|
+
entry = lookup(locale, key, scope, object)
|
55
|
+
|
56
|
+
if entry.nil?
|
57
|
+
entry = default(locale, default, key, options) unless default === false
|
58
|
+
|
59
|
+
if entry.nil?
|
60
|
+
raise I18n::MissingTranslationData.new(locale, key, options)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
entry = pluralize(locale, entry, count)
|
65
|
+
entry = interpolate(locale, entry, values)
|
66
|
+
entry
|
67
|
+
end
|
68
|
+
|
69
|
+
protected
|
70
|
+
|
71
|
+
def lookup(locale, key, scope = [], object = nil)
|
72
|
+
# bail out instantly if key is nil
|
73
|
+
return unless key
|
74
|
+
|
75
|
+
scope_key = scope_key(scope, key)
|
76
|
+
cache_key = cache_key(locale, key, scope, object)
|
77
|
+
|
78
|
+
# check cache and ignore errors (string keys with spaces for AR error override messages)
|
79
|
+
begin
|
80
|
+
if result = cache_read(cache_key)
|
81
|
+
return result
|
82
|
+
end
|
83
|
+
rescue => exception
|
84
|
+
return nil
|
85
|
+
end
|
86
|
+
|
87
|
+
result = @default_backend.lookup(locale, key, scope)
|
88
|
+
|
89
|
+
if result.nil?
|
90
|
+
if object.kind_of?(::ActiveRecord::Base)
|
91
|
+
result = ::Translation.locale(locale).for(object).lookup(scope_key)
|
92
|
+
else
|
93
|
+
result = ::Translation.orphans.locale(locale).lookup(scope_key)
|
94
|
+
end
|
95
|
+
|
96
|
+
if result.empty?
|
97
|
+
result = nil
|
98
|
+
elsif result.first.key == scope_key
|
99
|
+
result = result.first.value
|
100
|
+
else
|
101
|
+
chop_range = (key.size + 1)..-1
|
102
|
+
result = result.inject({}) do |hash, range|
|
103
|
+
hash[range.key.slice(chop_range)] = range.value
|
104
|
+
hash
|
105
|
+
end
|
106
|
+
|
107
|
+
result = deep_symbolize_keys(unwind_keys(result))
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# write to cache
|
112
|
+
cache_write(cache_key, result)
|
113
|
+
|
114
|
+
return result
|
115
|
+
end
|
116
|
+
|
117
|
+
def default(locale, default, key, options = {})
|
118
|
+
begin
|
119
|
+
case default
|
120
|
+
when String then
|
121
|
+
default
|
122
|
+
when Symbol then
|
123
|
+
translate(locale, default, options)
|
124
|
+
when Array then
|
125
|
+
default.map{ |obj| default(locale, obj, key, options.dup) }.compact!.first
|
126
|
+
else
|
127
|
+
# set default to false to avoid endless recursion
|
128
|
+
unless I18n.default_locale?(locale)
|
129
|
+
translate(I18n.default_locale, key, options.merge(:default => false))
|
130
|
+
end
|
131
|
+
end
|
132
|
+
rescue I18n::MissingTranslationData
|
133
|
+
nil
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def cache_read(key)
|
138
|
+
if cache_enabled?
|
139
|
+
return @cache_storage.fetch(key)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def cache_write(key, value)
|
144
|
+
if cache_enabled? && (not value.blank?)
|
145
|
+
@cache_storage.write(key, value)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def cache_clear!
|
150
|
+
if cache_enabled?
|
151
|
+
@cache_storage.clear
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def cache_key(locale, key, scope, object)
|
156
|
+
::Translation.cache_key(locale, scope_key(scope, key), object)
|
157
|
+
end
|
158
|
+
|
159
|
+
def scope_key(scope, key)
|
160
|
+
(Array(scope) + Array(key)).join(".")
|
161
|
+
end
|
162
|
+
|
163
|
+
# >> { "a.b.c" => "d", "a.b.e" => "f", "a.g" => "h", "i" => "j" }.unwind
|
164
|
+
# => { "a" => { "b" => { "c" => "d", "e" => "f" }, "g" => "h" }, "i" => "j"}
|
165
|
+
def unwind_keys(hash)
|
166
|
+
result = {}
|
167
|
+
hash.each do |key, value|
|
168
|
+
keys = key.split(".")
|
169
|
+
curr = result
|
170
|
+
curr = curr[keys.shift] ||= {} while keys.size > 1
|
171
|
+
curr[keys.shift] = value
|
172
|
+
end
|
173
|
+
result
|
174
|
+
end
|
175
|
+
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Perfectline
|
2
|
+
module T9n
|
3
|
+
module Backend
|
4
|
+
module ExceptionHandler
|
5
|
+
|
6
|
+
def self.hoptoad_exception_handler(exception, locale, key, scope)
|
7
|
+
unless defined?(HoptoadNotifier)
|
8
|
+
throw RuntimeError, "HoptoadNotifier is not present, please get it from http://hoptoadapp.com/"
|
9
|
+
end
|
10
|
+
|
11
|
+
case exception
|
12
|
+
when MissingTranslationData
|
13
|
+
HoptoadNotifier.notify(exception)
|
14
|
+
else
|
15
|
+
raise exception
|
16
|
+
end
|
17
|
+
|
18
|
+
return exception.message
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Perfectline
|
2
|
+
module T9n
|
3
|
+
module Backend
|
4
|
+
module Extension
|
5
|
+
|
6
|
+
def self.available_languages
|
7
|
+
|
8
|
+
unless defined?(Language)
|
9
|
+
throw RuntimeError, "Language model is not defined"
|
10
|
+
end
|
11
|
+
|
12
|
+
if self.backend.respond_to?(:available_languages)
|
13
|
+
self.backend.available_languages
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.default_locale?(locale = nil)
|
19
|
+
locale.nil? ? default_locale == self.locale : default_locale == locale.to_sym
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Perfectline
|
2
|
+
module T9n
|
3
|
+
module Models
|
4
|
+
module Language
|
5
|
+
|
6
|
+
def self.included(base)
|
7
|
+
base.__send__(:include, Scopes)
|
8
|
+
base.__send__(:include, Validations)
|
9
|
+
end
|
10
|
+
|
11
|
+
module Scopes
|
12
|
+
|
13
|
+
def self.included(base)
|
14
|
+
base.class_eval do
|
15
|
+
named_scope :code, lambda{ |code| {:conditions => {:code => code.to_s}} }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
module Validations
|
22
|
+
|
23
|
+
def self.included(base)
|
24
|
+
base.class_eval do
|
25
|
+
validates_presence_of :code
|
26
|
+
validates_presence_of :name
|
27
|
+
|
28
|
+
validates_length_of :code, :maximum => 5
|
29
|
+
validates_length_of :name, :maximum => 200
|
30
|
+
validates_length_of :native, :maximum => 200, :allow_blank => true
|
31
|
+
|
32
|
+
validates_uniqueness_of :code
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|