zlocalize 4.1.0

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