zlocalize 4.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 3ce7804e560ece2fb93d685c350d22beea6e1fdf
4
+ data.tar.gz: 0cc0ca863ae4fec576455a364a76c2a5840eba27
5
+ SHA512:
6
+ metadata.gz: a362c71b9dbb6d90cb37c60005066a85f92360879aef6a734622c7a70ff14723c9e14f6b84c513c2472fb5b2228ab9eec18f26e3949b4078b111930b3ca7d1cd
7
+ data.tar.gz: 81ea38ffc8fbf24ee4ed99f6b011e33af4a2d2e52fc7ddb6e3f8248d022f0738eff2a87424588dea9a38bde47f99832fb12d030ed949c1d8ee5e3c95260ba8a3
@@ -0,0 +1,246 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ require 'yaml'
4
+ require 'zlocalize/translation_file'
5
+ require 'zlocalize/config'
6
+ require 'i18n'
7
+
8
+ module ZLocalize
9
+
10
+ class MissingTranslationDataError < StandardError
11
+
12
+ def initialize(locale, key, options)
13
+ super "No translation exists in '#{locale}' for key \"#{key}\""
14
+ end
15
+
16
+ end
17
+
18
+ @@load_mutex = Mutex.new
19
+
20
+ class << self
21
+
22
+ INTERPOLATE_MATCH = /(\\)?\{\{([^\}]+)\}\}/
23
+ SCOPE_MATCH = /(.*?)(\\)?::(.+)/
24
+
25
+ DEFAULT_PLURAL_SELECT_PROC = lambda { |n| n <= 0 ? 0 : (n > 1 ? 2 : 1) }
26
+ DEFAULT_TITLEIZE_PROC = lambda { |s| s.to_s.titleize }
27
+ DEFAULT_CONVERT_FLOAT_PROC = lambda { |s| s.to_s.gsub(",", "") }
28
+
29
+ def config
30
+ @config ||= ZLocalize::Config.new
31
+ end
32
+
33
+ def locale
34
+ I18n.locale
35
+ end
36
+
37
+ def locale=(value)
38
+ I18n.locale = value
39
+ end
40
+
41
+ def default_locale
42
+ I18n.default_locale
43
+ end
44
+
45
+ def default_locale=(value)
46
+ I18n.default_locale = value
47
+ end
48
+
49
+ def translate(key, options = {})
50
+ loc = options[:locale] || self.locale
51
+ translate_with_locale(loc,key,options)
52
+ end
53
+ alias :t :translate
54
+
55
+ def pluralize(key,count,options = {})
56
+ loc = options[:locale] || self.locale
57
+ pluralize_with_locale(loc,key,count,options)
58
+ end
59
+ alias :p :pluralize
60
+
61
+ # Change locale inside a block
62
+ def switch_locale(new_locale,&block)
63
+ @@switch_locale_stack ||= []
64
+ if block_given?
65
+ @@switch_locale_stack.push self.locale
66
+ self.locale = new_locale
67
+ yield
68
+ self.locale = @@switch_locale_stack.pop
69
+ else
70
+ self.locale = new_locale
71
+ end
72
+ end
73
+
74
+ def translate_with_locale(locale, key, options = {})
75
+ entry = lookup(locale,"#{key}")
76
+ if entry.nil?
77
+ if (default = options.delete(:default))
78
+ entry = default
79
+ elsif (options.delete(:return_source_on_missing) || @return_source_on_missing) == true
80
+ entry = remove_scope(key)
81
+ else
82
+ raise ZLocalize::MissingTranslationDataError.new(locale, key, options)
83
+ end
84
+ end
85
+ return interpolate(locale, entry, options).html_safe
86
+ end
87
+
88
+ # n_(["...","...","..."],count)
89
+ def pluralize_with_locale(locale, key, count, options = {})
90
+
91
+ entry = lookup(locale,key)
92
+ if entry.nil?
93
+ if (default = options.delete(:default))
94
+ entry = default
95
+ elsif (options.delete(:return_source_on_missing) || @return_source_on_missing) == true
96
+ entry = key
97
+ else
98
+ raise ZLocalize::MissingTranslationDataError.new(locale,key,options)
99
+ end
100
+ end
101
+ n = translation_procs[locale.to_sym][:plural_select].call(count)
102
+ options[:count] ||= count
103
+ return interpolate(locale, entry[n], options).html_safe
104
+ end
105
+
106
+ def titleize(locale,s)
107
+ translation_procs[locale.to_sym][:titleize].call(s).html_safe
108
+ end
109
+
110
+ def convert_float(locale,s)
111
+ translation_procs[locale.to_sym][:convert_float].call(s)
112
+ end
113
+
114
+ def multi_lookup(locale,values,options = {})
115
+ result = {}
116
+ values.each do |val|
117
+ s = lookup(locale,"#{val}")
118
+ result[val] = s.html_safe unless s.nil?
119
+ end
120
+ result
121
+ end
122
+
123
+ # Interpolates values into a given string.
124
+ #
125
+ # interpolate "file {{file}} opened by \\{{user}}", :file => 'test.txt', :user => 'Mr. X'
126
+ # # => "file test.txt opened by {{user}}"
127
+ #
128
+ # Note that you have to double escape the <tt>\\</tt> when you want to escape
129
+ # the <tt>{{...}}</tt> key in a string (once for the string and once for the
130
+ # interpolation).
131
+ def interpolate(locale, string, values = {})
132
+ return string unless string.is_a?(String)
133
+
134
+ result = string.gsub(INTERPOLATE_MATCH) do
135
+ escaped, pattern, key = $1, $2, $2.to_sym
136
+
137
+ if escaped
138
+ "{{#{pattern}}}"
139
+ elsif !values.include?(key)
140
+ raise ArgumentError, "The #{key} argument is missing in the interpolation of '#{string}'"
141
+ else
142
+ values[key].to_s
143
+ end
144
+ end
145
+
146
+ result
147
+ end
148
+
149
+ def initialized?
150
+ @initialized ||= false
151
+ end
152
+
153
+ def reload!(force = false)
154
+ if force || !@initialized || (self.config.reload_per_request[Rails.env.to_sym] == true)
155
+ @initialized = false
156
+ @translations = nil
157
+ @translation_procs = nil
158
+ end
159
+ end
160
+
161
+ def locales
162
+ init_translations unless initialized?
163
+ translations.keys.map { |k| k.to_s }
164
+ end
165
+
166
+ alias :available_locales :locales
167
+
168
+ def translations
169
+ init_translations unless initialized?
170
+ @translations
171
+ end
172
+
173
+ def translation_procs
174
+ init_translations unless initialized?
175
+ @translation_procs
176
+ end
177
+
178
+ protected
179
+ # init_translations manipulates the translation data and is triggered by lazy-loading, meaning
180
+ # that on a busy multi-threaded environment, you might get 2 different threads trying to load the
181
+ # data concurrently. Therefore, this method is wrapped inside a mutex, which effectively ensures
182
+ # only 1 Thread will initialize the translation data Hash.
183
+ # The rest of all operations on translation data is read-only, so no concurrency issues except here
184
+ def init_translations
185
+ @@load_mutex.synchronize do
186
+ unless @initialized
187
+ @translation_procs = {}
188
+ @translations = {}
189
+
190
+ config.locales.keys.each do |locale|
191
+ cfg = self.config.locales[locale]
192
+ loc = locale.to_sym
193
+ @translation_procs[loc] = {
194
+ :plural_select => cfg[:plural_select].is_a?(Proc) ? cfg[:plural_select] : DEFAULT_PLURAL_SELECT_PROC,
195
+ :titleize => cfg[:titleize].is_a?(Proc) ? cfg[:titleize] : DEFAULT_TITLEIZE_PROC,
196
+ :convert_float => cfg[:convert_float].is_a?(Proc) ? cfg[:convert_float] : DEFAULT_CONVERT_FLOAT_PROC,
197
+ }
198
+ files = [cfg[:translations]].flatten.compact
199
+ load_translations(loc,files)
200
+ end
201
+
202
+ @return_source_on_missing = config.return_source_on_missing[Rails.env.to_sym]
203
+
204
+ @initialized = true
205
+ end
206
+ end
207
+ end
208
+
209
+ def load_translations(locale,filenames)
210
+ @translations ||= {}
211
+ @translations[locale] ||= {}
212
+ filenames.each do |file|
213
+ unless file.nil?
214
+ tf = ZLocalize::TranslationFile.new
215
+ tf.load(file)
216
+ tf.entries.each do |key,entry|
217
+ @translations[locale][entry.source] = entry.translation unless entry.ignore || entry.translation.to_s.empty?
218
+ end
219
+ end
220
+ end
221
+ end
222
+
223
+ def lookup(locale, key)
224
+ return unless key
225
+ init_translations unless @initialized
226
+ return @translations[locale.to_sym][key]
227
+ end
228
+
229
+ def remove_scope(key)
230
+ return key unless key.is_a?(String)
231
+
232
+ result = key.gsub(SCOPE_MATCH) do
233
+ scope, escaped, unscoped = $1, $2, $3
234
+ if $2 # escaped with \\::
235
+ # $~[0]
236
+ scope + '::' + $3
237
+ else
238
+ $3
239
+ end
240
+ end
241
+ end
242
+
243
+ end # class << self
244
+
245
+ end # module ZLocalize
246
+
@@ -0,0 +1,62 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module ZLocalize
3
+
4
+ class Config
5
+
6
+ attr_reader :reload_per_request
7
+ attr_reader :return_source_on_missing
8
+ attr_reader :reload_per_request
9
+ attr_reader :locales
10
+ attr_reader :define_gettext_methods
11
+
12
+ def initialize
13
+ @reload_per_request = { :development => true, :test => false, :production => false, :staging => false }
14
+ @return_source_on_missing = { :development => true, :test => false, :production => false, :staging => false }
15
+ @locales = {}
16
+ @use_global_gettext_methods = true
17
+ end
18
+
19
+ def reload_per_request=(value)
20
+ if value === true || value === false
21
+ [:development,:test,:production,:staging].each do |env|
22
+ @reload_per_request[env] = value
23
+ end
24
+ elsif value.is_a?(Hash)
25
+ @reload_per_request.merge!(value)
26
+ end
27
+ end
28
+
29
+ def return_source_on_missing=(value)
30
+ if value === true || value === false
31
+ [:development,:test,:production,:staging].each do |env|
32
+ @return_source_on_missing[env] = value
33
+ end
34
+ elsif value.is_a?(Hash)
35
+ @return_source_on_missing.merge!(value)
36
+ end
37
+ end
38
+
39
+ def locales=(value)
40
+ @locales = value
41
+ end
42
+
43
+ def define_gettext_methods=(value)
44
+ @define_gettext_methods = value ? true : false
45
+ please_define_gettext_methods if @define_gettext_methods
46
+ end
47
+
48
+ def please_define_gettext_methods
49
+ Object.class_eval <<-EOV
50
+ def _(key,options = {})
51
+ ZLocalize.translate(key, options)
52
+ end
53
+
54
+ def n_(key,count,options = {})
55
+ ZLocalize.pluralize(key,count,options)
56
+ end
57
+ EOV
58
+ end
59
+
60
+ end # class Config
61
+
62
+ end # module ZLocalize
@@ -0,0 +1,123 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ require 'rubygems'
4
+ require 'erb'
5
+ require File.join(File.dirname(__FILE__),'source_parser')
6
+ require File.join(File.dirname(__FILE__),'translation_file')
7
+
8
+ module ZLocalize
9
+
10
+ INDENT_STEP = 3
11
+
12
+ class HarvesterError < StandardError
13
+ end
14
+
15
+ # Harvester will parse and extract all calls to translation methods in "app/models/",
16
+ # "app/controllers/" and "app/views/"
17
+ class Harvester
18
+
19
+ attr_accessor :rails_root
20
+
21
+ DEFAULT_HARVEST_OPTIONS = { :models => true, :controllers => true,
22
+ :views => true, :helpers => true,
23
+ :output => 'config/locales/app-strings.yml',
24
+ :overwrite_existing => false,
25
+ :add_paths => [],
26
+ :silent => false,
27
+ :purge => false }
28
+
29
+ HARVEST_SECTIONS = { :models => [ "app/models/**/*.rb" ],
30
+ :controllers => [ "app/controllers/**/*.rb" ],
31
+ :helpers => [ "app/helpers/**/*.rb" ],
32
+ :views => [ "app/views/**/*.erb", "app/views/**/*.rhtml" ],
33
+ :lib => [ "lib/**/*.rb"]
34
+ }
35
+
36
+ def initialize(rails_root, options = {})
37
+ @rails_root = rails_root
38
+ @options = DEFAULT_HARVEST_OPTIONS.merge(options)
39
+ end
40
+
41
+ def progress(msg)
42
+ print msg unless @options[:silent]
43
+ end
44
+
45
+ def harvest_section(section,filespecs)
46
+ progress("Harvesting localizable strings from #{section}:\n")
47
+ filespecs.each do |spec|
48
+ progress("#{spec}: ")
49
+ Dir.glob(File.join(@rails_root,spec)) do |f|
50
+ progress('.')
51
+ collect_entries(f,@rails_root,is_erb?(f))
52
+ end
53
+ progress("\n")
54
+ end
55
+ end
56
+
57
+ def is_erb?(filename)
58
+ ['.erb','.rhtml'].include?(File.extname(filename))
59
+ end
60
+
61
+ def harvest
62
+ @new_entries = TranslationEntryCollection.new
63
+ HARVEST_SECTIONS.each_pair do |section,paths|
64
+ harvest_section(section,paths)
65
+ progress("\n")
66
+ end
67
+ if @options[:add_paths].is_a?(Array) && @options.size > 0
68
+ harvest_section("additional paths",@options[:add_paths])
69
+ progress("\n")
70
+ end
71
+
72
+ @translation_file = ZLocalize::TranslationFile.new
73
+ unless @options[:overwrite_existing]
74
+ progress("Merging existing translations from #{@options[:output]}...\n")
75
+ begin
76
+ @translation_file.load(File.join(@rails_root,@options[:output]))
77
+ rescue
78
+ end
79
+ end
80
+
81
+ @translation_file.entries.synchronize_with(@new_entries,@options[:purge])
82
+
83
+ progress("Writing new translation file...\n")
84
+ File.open(File.join(@rails_root,@options[:output]),"w") do |f|
85
+ f.write(@translation_file.to_yaml)
86
+ end
87
+
88
+ progress("Done!\n\n")
89
+ end
90
+
91
+ def harvest_file(filename)
92
+ @new_entries = TranslationEntryCollection.new
93
+ collect_entries(filename,@rails_root,false)
94
+
95
+ @translation_file = ZLocalize::TranslationFile.new
96
+ # merge
97
+ @translation_file.load(File.join(@rails_root,@options[:output]))
98
+ @translation_file.entries.synchronize_with(@new_entries,@options[:purge])
99
+ File.open(File.join(@rails_root,@options[:output]),"w") do |f|
100
+ f.write(@translation_file.to_yaml)
101
+ end
102
+ end
103
+
104
+ # collect entries in a source file
105
+ def collect_entries(filename,root,is_erb = false)
106
+ sp = ZLocalize::SourceParser.new(filename,root,is_erb)
107
+ sp.parse
108
+ sp.translation_entries.each do |key,te|
109
+ if @new_entries[key]
110
+ te.references.each do |ref|
111
+ @new_entries[key].add_reference(ref)
112
+ end
113
+ else
114
+ @new_entries[key] = te.dup
115
+ end
116
+ end
117
+ end
118
+
119
+ end
120
+
121
+ end # module Harvester
122
+
123
+
@@ -0,0 +1,11 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ require 'zlocalize/rails/attached_translations'
4
+ ActiveRecord::Base.send(:include, ZLocalize::Translatable::AttachedTranslations)
5
+
6
+ require 'zlocalize/rails/translated_columns'
7
+ ActiveRecord::Base.send(:include, ZLocalize::Translatable::TranslatedColumns)
8
+
9
+ require 'zlocalize/rails/decimal_attributes'
10
+ ActiveRecord::Base.send(:include, ZLocalize::Translatable::LocalizedDecimalAttributes)
11
+
@@ -0,0 +1,122 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ #
4
+ # === Translation of attributes values for ActiveRecord ===
5
+ #
6
+ # Allows a model to have multiple values (one for each given locale) stored for one or more attributes
7
+ #
8
+ # Uses a Translation model, declared as a polymorphic association
9
+ #
10
+ # Example use:
11
+ #
12
+ # class Article < ActiveRecord::Base
13
+ # ....
14
+ # has_translations
15
+ # ....
16
+ # end
17
+ #
18
+ #
19
+ # > @article = Article.new
20
+ # > @article.insert_translations(:fr => { :title => "L'année en revue" }, :en => { :title => "The Year in Review" })
21
+ # > @article.translate('title','fr')
22
+ # => "L'année en revue"
23
+ # > @article.translate('title','en')
24
+ # => "The year in review"
25
+ #
26
+ # It is also possible to declare the translations as nested_attributes:
27
+ #
28
+ #
29
+ # has_translations
30
+ # accepts_nested_attributes_for :translations, :allow_destroy => true
31
+ #
32
+ # which would allow to assign translations from within a form (using fields_for helper).
33
+ #
34
+ # params[:article][:translations_attributes] = [ { :locale => :fr, :name => 'title', :value => "L'année en revue" },
35
+ # { :locale => :en, :name => 'title', :value => "The Year in Review" } ]
36
+ #
37
+ module ZLocalize
38
+ module Translatable #:nodoc:
39
+
40
+ class TranslationError < StandardError #:nodoc:
41
+ end
42
+
43
+ module AttachedTranslations #:nodoc:
44
+ def self.included(base)
45
+ base.extend(ClassMethods)
46
+ end
47
+
48
+ module ClassMethods
49
+
50
+ def has_translations
51
+ has_many :translations, :as => :translated, :dependent => :destroy
52
+ include ZLocalize::Translatable::AttachedTranslations::InstanceMethods
53
+ end
54
+
55
+ end
56
+
57
+ module InstanceMethods
58
+
59
+ def translate(attr_name,locale = nil)
60
+ locale ||= ZLocalize.locale
61
+ if tr = find_translation(attr_name,locale)
62
+ tr.value
63
+ else
64
+ ''
65
+ end
66
+ end
67
+
68
+ def add_translation(name,locale,value)
69
+ if tr = find_translation(name,locale)
70
+ tr.value = value.to_s
71
+ else
72
+ tr = translations.build(:translated => self, :name => name.to_s, :locale => locale.to_s, :value => value.to_s)
73
+ end
74
+ tr
75
+ end
76
+
77
+ # convenience method to accept a Hash containing the translations for multiple columns and
78
+ # locales. +locales+ must have the locales as keys and its value is another Hash of name-value pairs.
79
+ # Example:
80
+ # @article.insert_translations(
81
+ # { 'en' => { 'title' => "What's new this week",
82
+ # 'synopsis' => "Learn what has happened this week in our little world"},
83
+ # 'fr' => { 'title' => "Quoi de neuf cette semaine",
84
+ # 'synopsis' => "Apprenez tout sur ce qui s'est passé cette semaine dans notre petit monde" }
85
+ #
86
+ # If you have user-generated content, for example, you can quickly
87
+ # create a form to edit the translatable content as follows:
88
+ # in the view:
89
+ # <label>Title in English</label>
90
+ # <%= text_field 'translations[en][title]', @article.translate('title','en') %>
91
+ # <label>Title in French</label>
92
+ # <%= text_field 'translations[fr][title]', @article.translate('title','fr') %>
93
+ #
94
+ # in the controller:
95
+ #
96
+ # def update
97
+ # ...
98
+ # @article.insert_translations(params['translations'])
99
+ # @article.save
100
+ # end
101
+ #
102
+ # make sure your content is sanitized!
103
+ #
104
+ def insert_translations(locales = {})
105
+ Translation.transaction do
106
+ locales.each do |locale,terms|
107
+ terms.each do |name,value|
108
+ add_translation(name,locale,value)
109
+ end
110
+ end
111
+ end
112
+ end
113
+
114
+ protected
115
+ def find_translation(name,locale)
116
+ translations.detect { |t| (t.name == name.to_s) && (t.locale == locale.to_s) }
117
+ end
118
+ end
119
+
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,76 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ # == Localized Decimal Attributes ==
4
+ #
5
+ # Provides a way to ensure Float attributes on ActiveRecord are converted to the internal format
6
+ # used by Ruby and Rails.
7
+ #
8
+ # Each locale defines a method to convert a String representing a float in a given locale into an actual Float
9
+ #
10
+ # Example with :fr (French) locale:
11
+ #
12
+ # 1) configuration (somewhere in an initializer):
13
+ #
14
+ # ZLocalize.config.locales = {
15
+ # :fr => {
16
+ # :plural_select => lambda { |n| n <= 0 ? 0 : (n > 1 ? 2 : 1) },
17
+ # .....
18
+ # :titleize => lambda { |s| s.capitalize.to_s },
19
+ # :convert_float => lambda { |s| s.to_s.gsub(' ','').gsub(',','.') }
20
+ # },
21
+ #
22
+ # 2) An Order model:
23
+ #
24
+ # class Order < ActiveRecord::Base
25
+ # ....
26
+ # localize_decimal_attributes [:sub_total, :taxes]
27
+ # ....
28
+ # end
29
+ #
30
+ #
31
+ # 3) This allows to assign attributes with their French representation of a decimal number:
32
+ #
33
+ # > order.attributes = {:sub_total => "1 222,54", :taxes => "1,23" }
34
+ # > order.sub_total # => 1222.54
35
+ # > order.taxes # => 1.23
36
+ #
37
+ #
38
+ module ZLocalize
39
+ module Translatable #:nodoc:
40
+
41
+ module LocalizedDecimalAttributes #:nodoc:
42
+
43
+ def self.included(base)
44
+ base.extend(ClassMethods)
45
+ end
46
+
47
+ module ClassMethods
48
+
49
+ def localize_decimal_attributes(column_names)
50
+
51
+ [column_names].flatten.each do |col_name|
52
+ class_eval "def #{col_name}=(value)
53
+ write_localized_decimal_attribute('#{col_name}',value)
54
+ end"
55
+ end
56
+
57
+ include ZLocalize::Translatable::LocalizedDecimalAttributes::InstanceMethods
58
+ end
59
+ end
60
+
61
+ module InstanceMethods
62
+
63
+ def write_localized_decimal_attribute(col_name,value,locale = nil)
64
+ s = value.nil? ? nil : ZLocalize.convert_float(locale || ZLocalize.locale,value)
65
+ self.send(:write_attribute, col_name,s)
66
+ end
67
+
68
+ end # module InstanceMethods
69
+
70
+ end # module TranslatedColumns
71
+ end # module Translatable
72
+ end # module ZLocalize
73
+
74
+
75
+
76
+
@@ -0,0 +1,21 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ require 'rails/generators/base'
4
+
5
+ # module here is deliberately named Zlocalize (as opposed to ZLocalize), for consistency
6
+ # between the namespacing of rake tasks and rails generators
7
+ module Zlocalize
8
+ module Generators
9
+ class InitializerGenerator < Rails::Generators::Base
10
+
11
+ desc "Create ZLocalize initializer for your Rails application"
12
+
13
+ source_root File.join(File.dirname(__FILE__),'templates')
14
+
15
+ def create_initializer_file
16
+ template "initializer_template.rb", "config/initializers/zlocalize.rb"
17
+ end
18
+
19
+ end
20
+ end
21
+ end