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.
@@ -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
@@ -0,0 +1,3 @@
1
+ class Translation < ActiveRecord::Base
2
+ include Perfectline::T9n::Models::Translation
3
+ 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