nulogy-gettext_i18n_rails 0.4.6.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. data/Gemfile +16 -0
  2. data/Gemfile.lock +126 -0
  3. data/Rakefile +24 -0
  4. data/Readme.md +226 -0
  5. data/VERSION +1 -0
  6. data/gettext_i18n_rails.gemspec +65 -0
  7. data/init.rb +14 -0
  8. data/lib/gettext_i18n_rails.rb +33 -0
  9. data/lib/gettext_i18n_rails/action_controller.rb +8 -0
  10. data/lib/gettext_i18n_rails/active_record.rb +19 -0
  11. data/lib/gettext_i18n_rails/backend.rb +67 -0
  12. data/lib/gettext_i18n_rails/base_parser.rb +41 -0
  13. data/lib/gettext_i18n_rails/haml_parser.rb +15 -0
  14. data/lib/gettext_i18n_rails/hamlet_parser.rb +16 -0
  15. data/lib/gettext_i18n_rails/html_safe_translations.rb +29 -0
  16. data/lib/gettext_i18n_rails/i18n_hacks.rb +25 -0
  17. data/lib/gettext_i18n_rails/model_attributes_finder.rb +108 -0
  18. data/lib/gettext_i18n_rails/railtie.rb +22 -0
  19. data/lib/gettext_i18n_rails/ruby_gettext_extractor.rb +144 -0
  20. data/lib/gettext_i18n_rails/slim_parser.rb +15 -0
  21. data/lib/gettext_i18n_rails/string_interpolate_fix.rb +20 -0
  22. data/lib/gettext_i18n_rails/tasks.rb +127 -0
  23. data/lib/tasks/gettext_rails_i18n.rake +1 -0
  24. data/spec/gettext_i18n_rails/action_controller_spec.rb +54 -0
  25. data/spec/gettext_i18n_rails/active_record_spec.rb +85 -0
  26. data/spec/gettext_i18n_rails/backend_spec.rb +56 -0
  27. data/spec/gettext_i18n_rails/haml_parser_spec.rb +39 -0
  28. data/spec/gettext_i18n_rails/hamlet_parser_spec.rb +33 -0
  29. data/spec/gettext_i18n_rails/slim_parser_spec.rb +40 -0
  30. data/spec/gettext_i18n_rails/string_interpolate_fix_spec.rb +32 -0
  31. data/spec/gettext_i18n_rails_spec.rb +84 -0
  32. data/spec/spec_helper.rb +39 -0
  33. metadata +110 -0
data/init.rb ADDED
@@ -0,0 +1,14 @@
1
+ begin
2
+ require 'config/initializers/session_store'
3
+ rescue LoadError
4
+ # weird bug, when run with rake rails reports error that session
5
+ # store is not configured, this fixes it somewhat...
6
+ end
7
+
8
+ if Rails::VERSION::MAJOR > 2
9
+ require 'gettext_i18n_rails'
10
+ else
11
+ #requires fast_gettext to be present.
12
+ #We give rails a chance to install it using rake gems:install, by loading it later.
13
+ config.after_initialize { require 'gettext_i18n_rails' }
14
+ end
@@ -0,0 +1,33 @@
1
+ module GettextI18nRails
2
+ VERSION = File.read( File.join(File.dirname(__FILE__),'..','VERSION') ).strip
3
+
4
+ extend self
5
+ end
6
+
7
+ require 'fast_gettext'
8
+ if Gem::Version.new(FastGettext::VERSION) < Gem::Version.new("0.4.8")
9
+ raise "Please upgrade fast_gettext"
10
+ end
11
+
12
+ # include translations into all the places it needs to go...
13
+ Object.send(:include, FastGettext::Translation)
14
+
15
+ # make translations html_safe if possible and wanted
16
+ if "".respond_to?(:html_safe?)
17
+ require 'gettext_i18n_rails/html_safe_translations'
18
+ Object.send(:include, GettextI18nRails::HtmlSafeTranslations)
19
+ end
20
+
21
+ require 'gettext_i18n_rails/backend'
22
+ I18n.backend = GettextI18nRails::Backend.new
23
+
24
+ require 'gettext_i18n_rails/i18n_hacks'
25
+
26
+ require 'gettext_i18n_rails/active_record'
27
+ # If configuration via Railties is not available force activerecord extensions
28
+ if not defined?(Rails::Railtie) and defined?(ActiveRecord)
29
+ ActiveRecord::Base.extend GettextI18nRails::ActiveRecord
30
+ end
31
+
32
+ require 'gettext_i18n_rails/action_controller' if defined?(ActionController) # so that bundle console can work in a rails project
33
+ require 'gettext_i18n_rails/railtie'
@@ -0,0 +1,8 @@
1
+ class ActionController::Base
2
+ def set_gettext_locale
3
+ requested_locale = params[:locale] || session[:locale] || cookies[:locale] || request.env['HTTP_ACCEPT_LANGUAGE'] || I18n.default_locale
4
+ locale = FastGettext.set_locale(requested_locale)
5
+ session[:locale] = locale
6
+ I18n.locale = locale # some weird overwriting in action-controller makes this necessary ... see I18nProxy
7
+ end
8
+ end
@@ -0,0 +1,19 @@
1
+ module GettextI18nRails::ActiveRecord
2
+ # CarDealer.sales_count -> s_('CarDealer|Sales count') -> 'Sales count' if no translation was found
3
+ def human_attribute_name(attribute, *args)
4
+ s_(gettext_translation_for_attribute_name(attribute))
5
+ end
6
+
7
+ # CarDealer -> _('car dealer')
8
+ def human_name(*args)
9
+ _(self.human_name_without_translation)
10
+ end
11
+
12
+ def human_name_without_translation
13
+ self.to_s.underscore.gsub('_',' ')
14
+ end
15
+
16
+ def gettext_translation_for_attribute_name(attribute)
17
+ "#{self}|#{attribute.to_s.split('.').map! {|a| a.humanize }.join('|')}"
18
+ end
19
+ end
@@ -0,0 +1,67 @@
1
+ module GettextI18nRails
2
+ #translates i18n calls to gettext calls
3
+ class Backend
4
+ @@translate_defaults = true
5
+ cattr_accessor :translate_defaults
6
+ attr_accessor :backend
7
+
8
+ def initialize(*args)
9
+ self.backend = I18n::Backend::Simple.new(*args)
10
+ end
11
+
12
+ def available_locales
13
+ FastGettext.available_locales || []
14
+ end
15
+
16
+ def translate(locale, key, options)
17
+ if gettext_key = gettext_key(key, options)
18
+ translation = FastGettext._(gettext_key)
19
+ interpolate(translation, options)
20
+ else
21
+ backend.translate locale, key, options
22
+ end
23
+ end
24
+
25
+ def method_missing(method, *args)
26
+ backend.send(method, *args)
27
+ end
28
+
29
+ protected
30
+
31
+ def gettext_key(key, options)
32
+ flat_key = flatten_key key, options
33
+ if FastGettext.key_exist?(flat_key)
34
+ flat_key
35
+ elsif self.class.translate_defaults
36
+ [*options[:default]].each do |default|
37
+ #try the scoped(more specific) key first e.g. 'activerecord.errors.my custom message'
38
+ flat_key = flatten_key default, options
39
+ return flat_key if FastGettext.key_exist?(flat_key)
40
+
41
+ #try the short key thereafter e.g. 'my custom message'
42
+ return default if FastGettext.key_exist?(default)
43
+ end
44
+ return nil
45
+ end
46
+ end
47
+
48
+ def interpolate(string, values)
49
+ if string.respond_to?(:%)
50
+ reserved_keys = if defined?(I18n::RESERVED_KEYS) # rails 3+
51
+ I18n::RESERVED_KEYS
52
+ else
53
+ I18n::Backend::Base::RESERVED_KEYS
54
+ end
55
+
56
+ string % values.except(*reserved_keys)
57
+ else
58
+ string
59
+ end
60
+ end
61
+
62
+ def flatten_key key, options
63
+ scope = [*(options[:scope] || [])]
64
+ scope.empty? ? key.to_s : "#{scope*'.'}.#{key}"
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,41 @@
1
+ require 'gettext/utils'
2
+ begin
3
+ require 'gettext/tools/rgettext'
4
+ rescue LoadError #version prior to 2.0
5
+ require 'gettext/rgettext'
6
+ end
7
+
8
+ module GettextI18nRails
9
+ class BaseParser
10
+ def self.target?(file)
11
+ File.extname(file) == ".#{extension}"
12
+ end
13
+
14
+ def self.parse(file, msgids = [])
15
+ return msgids unless load_library
16
+ code = convert_to_code(File.read(file))
17
+ RubyGettextExtractor.parse_string(code, file, msgids)
18
+ rescue Racc::ParseError => e
19
+ $stderr.puts "file ignored: ruby_parser cannot read #{extension} files with 1.9 syntax --- (#{e.message})"
20
+ return msgids
21
+ end
22
+
23
+ def self.load_library
24
+ return true if @library_loaded
25
+
26
+ begin
27
+ require "#{::Rails.root.to_s}/vendor/plugins/#{extension}/lib/#{extension}"
28
+ rescue LoadError
29
+ begin
30
+ require extension # From gem
31
+ rescue LoadError
32
+ puts "A #{extension} file was found, but #{extension} library could not be found, so nothing will be parsed..."
33
+ return false
34
+ end
35
+ end
36
+
37
+ require 'gettext_i18n_rails/ruby_gettext_extractor'
38
+ @library_loaded = true
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,15 @@
1
+ require 'gettext_i18n_rails/base_parser'
2
+
3
+ module GettextI18nRails
4
+ class HamlParser < BaseParser
5
+ def self.extension
6
+ "haml"
7
+ end
8
+
9
+ def self.convert_to_code(text)
10
+ Haml::Engine.new(text).precompiled()
11
+ end
12
+ end
13
+ end
14
+
15
+ GetText::RGetText.add_parser(GettextI18nRails::HamlParser)
@@ -0,0 +1,16 @@
1
+ require 'gettext_i18n_rails/base_parser'
2
+
3
+ module GettextI18nRails
4
+ class HamletParser < BaseParser
5
+ def self.extension
6
+ "hamlet"
7
+ end
8
+
9
+ def self.convert_to_code(text)
10
+ Hamlet::Engine.new.call(text)
11
+ end
12
+ end
13
+ end
14
+
15
+ GetText::RGetText.add_parser(GettextI18nRails::HamletParser)
16
+
@@ -0,0 +1,29 @@
1
+ module GettextI18nRails
2
+ mattr_accessor :translations_are_html_safe
3
+
4
+ module HtmlSafeTranslations
5
+ # also make available on class methods
6
+ def self.included(base)
7
+ base.extend self
8
+ end
9
+
10
+ def _(*args)
11
+ html_safe_if_wanted super
12
+ end
13
+
14
+ def n_(*args)
15
+ html_safe_if_wanted super
16
+ end
17
+
18
+ def s_(*args)
19
+ html_safe_if_wanted super
20
+ end
21
+
22
+ private
23
+
24
+ def html_safe_if_wanted(text)
25
+ return text unless GettextI18nRails.translations_are_html_safe
26
+ text.to_s.html_safe
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,25 @@
1
+ I18n::Config # autoload
2
+
3
+ module I18n
4
+ class Config
5
+ def locale
6
+ FastGettext.locale.to_sym
7
+ end
8
+
9
+ def locale=(new_locale)
10
+ FastGettext.locale=(new_locale)
11
+ end
12
+ end
13
+
14
+ # backport I18n.with_locale if it does not exist
15
+ # Executes block with given I18n.locale set.
16
+ def self.with_locale(tmp_locale = nil)
17
+ if tmp_locale
18
+ current_locale = self.locale
19
+ self.locale = tmp_locale
20
+ end
21
+ yield
22
+ ensure
23
+ self.locale = current_locale if tmp_locale
24
+ end unless defined? I18n.with_locale
25
+ end
@@ -0,0 +1,108 @@
1
+ module GettextI18nRails
2
+ #write all found models/columns to a file where GetTexts ruby parser can find them
3
+ def store_model_attributes(options)
4
+ file = options[:to] || 'locale/model_attributes.rb'
5
+ begin
6
+ File.open(file,'w') do |f|
7
+ f.puts "#DO NOT MODIFY! AUTOMATICALLY GENERATED FILE!"
8
+ ModelAttributesFinder.new.find(options).each do |table_name,column_names|
9
+ #model name
10
+ model = table_name_to_namespaced_model(table_name)
11
+ if model == nil
12
+ # Some tables are not models, for example: translation tables created by globalize2.
13
+ puts "[Warning] Model not found for table '#{table_name}'"
14
+ next
15
+ end
16
+ f.puts("_('#{model.human_name_without_translation}')")
17
+
18
+ #all columns namespaced under the model
19
+ column_names.each do |attribute|
20
+ translation = model.gettext_translation_for_attribute_name(attribute)
21
+ f.puts("_('#{translation}')")
22
+ end
23
+ end
24
+ f.puts "#DO NOT MODIFY! AUTOMATICALLY GENERATED FILE!"
25
+ end
26
+ rescue
27
+ puts "[Error] Attribute extraction failed. Removing incomplete file (#{file})"
28
+ File.delete(file)
29
+ raise
30
+ end
31
+ end
32
+ module_function :store_model_attributes
33
+
34
+ class ModelAttributesFinder
35
+ # options:
36
+ # :ignore_tables => ['cars',/_settings$/,...]
37
+ # :ignore_columns => ['id',/_id$/,...]
38
+ # current connection ---> {'cars'=>['model_name','type'],...}
39
+ def find(options)
40
+ found = Hash.new([])
41
+
42
+ connection = ::ActiveRecord::Base.connection
43
+ connection.tables.each do |table_name|
44
+ next if ignored?(table_name,options[:ignore_tables])
45
+ connection.columns(table_name).each do |column|
46
+ found[table_name] += [column.name] unless ignored?(column.name,options[:ignore_columns])
47
+ end
48
+ end
49
+
50
+ found
51
+ end
52
+
53
+ def ignored?(name,patterns)
54
+ return false unless patterns
55
+ patterns.detect{|p|p.to_s==name.to_s or (p.is_a?(Regexp) and name=~p)}
56
+ end
57
+ end
58
+
59
+ private
60
+ # Tries to find the model class corresponding to specified table name.
61
+ # Takes into account that the model can be defined in a namespace.
62
+ # Searches only up to one level deep - won't find models nested in two
63
+ # or more modules.
64
+ #
65
+ # Note that if we allow namespaces, the conversion can be ambiguous, i.e.
66
+ # if the table is named "aa_bb_cc" and AaBbCc, Aa::BbCc and AaBb::Cc are
67
+ # all defined there's no absolute rule that tells us which one to use.
68
+ # This method prefers the less nested one and, if there are two at
69
+ # the same level, the one with shorter module name.
70
+ def table_name_to_namespaced_model(table_name)
71
+ # First assume that there are no namespaces
72
+ model = to_class(table_name.singularize.camelcase)
73
+ return model if model != nil
74
+
75
+ # If you were wrong, assume that the model is in a namespace.
76
+ # Iterate over the underscores and try to substitute each of them
77
+ # for a slash that camelcase() replaces with the scope operator (::).
78
+ underscore_position = table_name.index('_')
79
+ while underscore_position != nil
80
+ namespaced_table_name = table_name.dup
81
+ namespaced_table_name[underscore_position] = '/'
82
+ model = to_class(namespaced_table_name.singularize.camelcase)
83
+ return model if model != nil
84
+
85
+ underscore_position = table_name.index('_', underscore_position + 1)
86
+ end
87
+
88
+ # The model either is not defined or is buried more than one level
89
+ # deep in a module hierarchy
90
+ return nil
91
+ end
92
+
93
+ # Checks if there is a class of specified name and if so, returns
94
+ # the class object. Otherwise returns nil.
95
+ def to_class(name)
96
+ # I wanted to use Module.const_defined?() here to avoid relying
97
+ # on exceptions for normal program flow but it's of no use.
98
+ # If class autoloading is enabled, the constant may be undefined
99
+ # but turn out to be present when we actually try to use it.
100
+ begin
101
+ constant = name.constantize
102
+ rescue NameError
103
+ return nil
104
+ end
105
+
106
+ return constant.is_a?(Class) ? constant : nil
107
+ end
108
+ end
@@ -0,0 +1,22 @@
1
+ # add rake tasks if we are inside Rails
2
+ if defined?(Rails::Railtie)
3
+ module GettextI18nRails
4
+ class Railtie < ::Rails::Railtie
5
+ config.gettext_i18n_rails = ActiveSupport::OrderedOptions.new
6
+ config.gettext_i18n_rails.msgmerge = %w[--sort-output --no-location --no-wrap]
7
+ config.gettext_i18n_rails.use_for_active_record_attributes = true
8
+
9
+ rake_tasks do
10
+ require 'gettext_i18n_rails/tasks'
11
+ end
12
+
13
+ config.after_initialize do |app|
14
+ if app.config.gettext_i18n_rails.use_for_active_record_attributes
15
+ ActiveSupport.on_load :active_record do
16
+ extend GettextI18nRails::ActiveRecord
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,144 @@
1
+ # new ruby parser from retoo, that should help extracting "#{_('xxx')}", which is needed especially when parsing haml files
2
+ #
3
+ #!/usr/bin/ruby
4
+ # parser/ruby.rb - look for gettext msg strings in ruby files
5
+
6
+ require 'rubygems'
7
+ require 'ruby_parser'
8
+
9
+ begin
10
+ require 'gettext/tools/rgettext'
11
+ rescue LoadError #version prior to 2.0
12
+ require 'gettext/rgettext'
13
+ end
14
+
15
+ module RubyGettextExtractor
16
+ extend self
17
+
18
+ def parse(file, targets = []) # :nodoc:
19
+ content = File.read(file)
20
+ parse_string(content, file, targets)
21
+ end
22
+
23
+ def parse_string(content, file, targets=[])
24
+ # file is just for information in error messages
25
+ parser = Extractor.new(file, targets)
26
+ parser.run(content)
27
+ end
28
+
29
+ def target?(file) # :nodoc:
30
+ return file =~ /\.rb$/
31
+ end
32
+
33
+ class Extractor < RubyParser
34
+ def initialize(filename, targets)
35
+ @filename = filename
36
+ @targets = Hash.new
37
+ @results = []
38
+
39
+ targets.each do |a|
40
+ k, v = a
41
+ # things go wrong if k already exists, but this
42
+ # should not happen (according to the gettext doc)
43
+ @targets[k] = a
44
+ @results << a
45
+ end
46
+
47
+ super()
48
+ end
49
+
50
+ def run(content)
51
+ # ruby parser has an ugly bug which causes that several \000's take
52
+ # ages to parse. This avoids this probelm by stripping them away (they probably wont appear in keys anyway)
53
+ # See bug report: http://rubyforge.org/tracker/index.php?func=detail&aid=26898&group_id=439&atid=1778
54
+ safe_content = content.gsub(/\\\d\d\d/, '')
55
+ self.parse(safe_content)
56
+ return @results
57
+ end
58
+
59
+ def extract_string(node)
60
+ if node.first == :str
61
+ return node.last
62
+ elsif node.first == :call
63
+ type, recv, meth, args = node
64
+
65
+ # node has to be in form of "string"+("other_string")
66
+ return nil unless recv && meth == :+
67
+
68
+ # descent recurrsivly to determine the 'receiver' of the string concatination
69
+ # "foo" + "bar" + baz" is
70
+ # ("foo".+("bar")).+("baz")
71
+ first_part = extract_string(recv)
72
+
73
+ if args.first == :arglist && args.size == 2
74
+ second_part = extract_string(args.last)
75
+
76
+ return nil if second_part.nil?
77
+
78
+ return first_part.to_s + second_part.to_s
79
+ else
80
+ raise "uuh?"
81
+ end
82
+ else
83
+ return nil
84
+ end
85
+ end
86
+
87
+ def extract_key(args, seperator)
88
+ key = nil
89
+ if args.size == 2
90
+ key = extract_string(args.value)
91
+ else
92
+ # this could be n_("aaa","aaa2",1)
93
+ # all strings arguemnts are extracted and joined with \004 or \000
94
+
95
+ arguments = args[1..(-1)]
96
+
97
+ res = []
98
+ arguments.each do |a|
99
+ str = extract_string(a)
100
+ # only add strings
101
+ res << str if str
102
+ end
103
+
104
+ return nil if res.empty?
105
+ key = res.join(seperator)
106
+ end
107
+
108
+ return nil unless key
109
+
110
+ key.gsub!("\n", '\n')
111
+ key.gsub!("\t", '\t')
112
+ key.gsub!("\0", '\0')
113
+
114
+ return key
115
+ end
116
+
117
+ def new_call recv, meth, args = nil
118
+ # we dont care if the method is called on a a object
119
+ if recv.nil?
120
+ if (meth == :_ || meth == :p_ || meth == :N_ || meth == :pgettext || meth == :s_)
121
+ key = extract_key(args, "\004")
122
+ elsif meth == :n_
123
+ key = extract_key(args, "\000")
124
+ else
125
+ # skip
126
+ end
127
+
128
+ if key
129
+ res = @targets[key]
130
+
131
+ unless res
132
+ res = [key]
133
+ @results << res
134
+ @targets[key] = res
135
+ end
136
+
137
+ res << "#{@filename}:#{lexer.lineno}"
138
+ end
139
+ end
140
+
141
+ super recv, meth, args
142
+ end
143
+ end
144
+ end