phrase 0.3.1 → 0.3.2

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.
Files changed (38) hide show
  1. data/.gitignore +4 -1
  2. data/Guardfile +29 -0
  3. data/README.md +1 -1
  4. data/lib/phrase.rb +8 -9
  5. data/lib/phrase/adapters/fast_gettext.rb +22 -0
  6. data/lib/phrase/adapters/i18n.rb +11 -0
  7. data/lib/phrase/api/client.rb +1 -1
  8. data/lib/phrase/backend/phrase_service.rb +2 -2
  9. data/lib/phrase/delegate.rb +13 -165
  10. data/lib/phrase/delegate/fast_gettext.rb +27 -0
  11. data/lib/phrase/delegate/i18n.rb +159 -0
  12. data/lib/phrase/tool.rb +1 -1
  13. data/lib/phrase/tool/commands/base.rb +1 -1
  14. data/lib/phrase/tool/commands/init.rb +13 -5
  15. data/lib/phrase/tool/commands/pull.rb +11 -8
  16. data/lib/phrase/tool/commands/push.rb +11 -2
  17. data/lib/phrase/tool/config.rb +55 -1
  18. data/lib/phrase/tool/encoding_detector.rb +4 -2
  19. data/lib/phrase/tool/formats.rb +33 -4
  20. data/lib/phrase/tool/formats/base.rb +23 -1
  21. data/lib/phrase/tool/formats/custom.rb +38 -0
  22. data/lib/phrase/tool/formats/gettext.rb +14 -2
  23. data/lib/phrase/tool/formats/gettext_pot.rb +11 -0
  24. data/lib/phrase/tool/formats/ini.rb +4 -0
  25. data/lib/phrase/tool/formats/json.rb +4 -0
  26. data/lib/phrase/tool/formats/plist.rb +5 -1
  27. data/lib/phrase/tool/formats/properties.rb +4 -0
  28. data/lib/phrase/tool/formats/qt_phrase_book.rb +4 -0
  29. data/lib/phrase/tool/formats/qt_translation_source.rb +4 -0
  30. data/lib/phrase/tool/formats/resx.rb +4 -0
  31. data/lib/phrase/tool/formats/strings.rb +9 -1
  32. data/lib/phrase/tool/formats/xliff.rb +4 -0
  33. data/lib/phrase/tool/formats/xml.rb +7 -4
  34. data/lib/phrase/tool/formats/yaml.rb +4 -0
  35. data/lib/phrase/tool/options.rb +13 -52
  36. data/lib/phrase/tool/options_factory.rb +77 -0
  37. data/lib/phrase/version.rb +1 -1
  38. metadata +74 -21
data/.gitignore CHANGED
@@ -19,4 +19,7 @@ tmp
19
19
  .rvmrc
20
20
  .DS_Store
21
21
  /phrase
22
- .rspec
22
+ .rspec
23
+ /locales
24
+ /ru.lproj
25
+ /res
data/Guardfile ADDED
@@ -0,0 +1,29 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard 'bundler' do
5
+ watch('Gemfile')
6
+ # Uncomment next line if Gemfile contain `gemspec' command
7
+ # watch(/^.+\.gemspec/)
8
+ end
9
+
10
+ guard 'rspec' do
11
+ watch(%r{^spec/.+_spec\.rb$})
12
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
13
+ watch('spec/spec_helper.rb') { "spec" }
14
+
15
+ # Rails example
16
+ watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
17
+ watch(%r{^app/(.*)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
18
+ watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] }
19
+ watch(%r{^spec/support/(.+)\.rb$}) { "spec" }
20
+ watch('config/routes.rb') { "spec/routing" }
21
+ watch('app/controllers/application_controller.rb') { "spec/controllers" }
22
+
23
+ # Capybara features specs
24
+ watch(%r{^app/views/(.+)/.*\.(erb|haml)$}) { |m| "spec/features/#{m[1]}_spec.rb" }
25
+
26
+ # Turnip features and steps
27
+ watch(%r{^spec/acceptance/(.+)\.feature$})
28
+ watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) { |m| Dir[File.join("**/#{m[1]}.feature")][0] || 'spec/acceptance' }
29
+ end
data/README.md CHANGED
@@ -4,4 +4,4 @@ Powerful in-place translation management for your web applications.
4
4
 
5
5
  ## More Information
6
6
 
7
- https://phraseapp.com
7
+ https://phraseapp.com
data/lib/phrase.rb CHANGED
@@ -42,19 +42,18 @@ module Phrase
42
42
  end
43
43
  end
44
44
 
45
+ def self.configure
46
+ yield(self.config)
47
+ end
48
+
45
49
  autoload :ViewHelpers, 'phrase/view_helpers'
46
50
 
51
+ require 'phrase/api'
47
52
  require 'phrase/version'
48
53
  require 'phrase/engine'
49
54
  require 'phrase/backend'
55
+ require 'phrase/delegate'
50
56
  end
51
57
 
52
- module I18n
53
- class << self
54
- def translate_with_phrase(*args)
55
- Phrase.backend.translate(*args)
56
- end
57
- alias_method_chain :translate, :phrase
58
- alias_method :t, :translate
59
- end
60
- end
58
+ require 'phrase/adapters/i18n'
59
+ require 'phrase/adapters/fast_gettext'
@@ -0,0 +1,22 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ require 'phrase/delegate/fast_gettext'
4
+
5
+ module FastGettext
6
+ module Translation
7
+ def __with_phrase(*args)
8
+ Phrase::Delegate::FastGettext.new(:_, *args)
9
+ end
10
+ alias_method_chain :_, :phrase
11
+
12
+ def n__with_phrase(*args)
13
+ Phrase::Delegate::FastGettext.new(:n_, *args)
14
+ end
15
+ alias_method_chain :n_, :phrase
16
+
17
+ def s__with_phrase(*args)
18
+ Phrase::Delegate::FastGettext.new(:s_, *args)
19
+ end
20
+ alias_method_chain :s_, :phrase
21
+ end
22
+ end if defined? FastGettext::Translation
@@ -0,0 +1,11 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ module I18n
4
+ class << self
5
+ def translate_with_phrase(*args)
6
+ Phrase.backend.translate(*args)
7
+ end
8
+ alias_method_chain :translate, :phrase
9
+ alias_method :t, :translate
10
+ end
11
+ end if defined?(I18n)
@@ -218,4 +218,4 @@ private
218
218
  def escaped(value)
219
219
  CGI::escape(value)
220
220
  end
221
- end
221
+ end
@@ -1,6 +1,6 @@
1
1
  # -*- encoding : utf-8 -*-
2
2
 
3
- require 'phrase/delegate'
3
+ require 'phrase/delegate/i18n'
4
4
 
5
5
  class Phrase::Backend::PhraseService
6
6
 
@@ -55,7 +55,7 @@ protected
55
55
  key = given_key_from_args(args)
56
56
  return nil unless present?(key)
57
57
  options = args[1].nil? ? {} : args[1]
58
- Phrase::Delegate.new(key, options)
58
+ Phrase::Delegate::I18n.new(key, options)
59
59
  end
60
60
 
61
61
  def extract_normalized_key_from_args(args)
@@ -1,175 +1,23 @@
1
1
  # -*- encoding : utf-8 -*-
2
2
 
3
- require 'phrase/api'
4
- require 'phrase/cache'
5
- require 'phrase/hash_flattener'
6
- require 'set'
7
-
8
- class Phrase::Delegate < String
9
- attr_accessor :key, :display_key, :options, :api_client, :fallback_keys
10
-
11
- def initialize(key, options={})
12
- @display_key = @key = key
13
- @options = options
14
-
15
- @fallback_keys = []
16
-
17
- extract_fallback_keys
18
- identify_key_to_display if @fallback_keys.any?
19
- super(decorated_key_name)
20
- end
21
-
22
- def to_s
23
- "#{decorated_key_name}"
24
- end
25
-
26
- def method_missing(*args, &block)
27
- if @key.respond_to?(args.first)
28
- to_s.send(*args)
29
- else
30
- data = translation_or_subkeys
31
- if data.is_a?(String)
32
- to_s
33
- elsif data.respond_to?(args.first)
34
- data.send(*args, &block)
35
- else
36
- log "You are trying to execute the method ##{args.first} on a translation key which is not supported. Please make sure you treat your translations as strings only."
37
- nil
38
- end
39
- end
40
- end
41
-
42
- private
43
- def identify_key_to_display
44
- key_names = [@key] | @fallback_keys
45
- available_key_names = find_keys_within_phrase(key_names)
46
- @display_key = @key
47
- key_names.each do |item|
48
- if available_key_names.include?(item)
49
- @display_key = item
50
- break
51
- end
3
+ module Phrase::Delegate
4
+ class Base < String
5
+ def to_s
6
+ "#{decorated_key_name}"
52
7
  end
53
- end
54
-
55
- def find_keys_within_phrase(key_names)
56
- key_names_to_check_against_api = key_names - pre_fetched(key_names)
57
- pre_cached(key_names) | key_names_returned_from_api_for(key_names_to_check_against_api)
58
- end
59
-
60
- def pre_cached(key_names)
61
- warm_translation_key_names_cache unless cache.cached?(:translation_key_names)
62
- pre_cached_key_names = key_names.select { |key_name| key_name_precached?(key_name) }
63
- pre_cached_key_names
64
- end
65
-
66
- def pre_fetched(key_names)
67
- key_names.select { |key_name| covered_by_initial_caching?(key_name) }
68
- end
69
-
70
- def key_name_precached?(key_name)
71
- covered = covered_by_initial_caching?(key_name)
72
- in_cache = key_name_is_in_cache?(key_name)
73
- covered && in_cache
74
- end
75
-
76
- def key_names_returned_from_api_for(key_names)
77
- if key_names.size > 0
78
- api_client.find_keys_by_name(key_names).map { |key| key["name"] }
79
- else
80
- []
81
- end
82
- end
83
-
84
- def key_name_is_in_cache?(key_name)
85
- cache.get(:translation_key_names).include?(key_name)
86
- end
87
-
88
- def covered_by_initial_caching?(key_name)
89
- key_name.start_with?(*Phrase.cache_key_segments_initial)
90
- end
91
-
92
- def extract_fallback_keys
93
- fallback_items = []
94
- if @options.has_key?(:default)
95
- if @options[:default].kind_of?(Array)
96
- fallback_items = @options[:default]
8
+
9
+ def self.log(message)
10
+ message = "phrase: #{message}"
11
+ if defined?(Rails)
12
+ Rails.logger.warn(message)
97
13
  else
98
- fallback_items << @options[:default]
14
+ $stderr.puts message
99
15
  end
100
16
  end
101
17
 
102
- fallback_items.each do |item|
103
- process_fallback_item(item)
104
- end
105
- end
106
-
107
- def scoped(item)
108
- @options.has_key?(:scope) ? "#{@options[:scope]}.#{item}" : item
109
- end
110
-
111
- def process_fallback_item(item)
112
- if item.kind_of?(Symbol)
113
- entry = scoped(item.to_s)
114
- @fallback_keys << entry
115
- if @key == "helpers.label.#{entry}" # http://apidock.com/rails/v3.1.0/ActionView/Helpers/FormHelper/label
116
- @fallback_keys << "activerecord.attributes.#{entry}"
117
- end
118
- end
119
- end
120
-
121
- def decorated_key_name
122
- "#{Phrase.prefix}phrase_#{@display_key}#{Phrase.suffix}"
123
- end
124
-
125
- def translation_or_subkeys
126
- begin
127
- api_client.translate(@key)
128
- rescue Exception => e
129
- log "Server Error: #{e.message}"
18
+ protected
19
+ def decorated_key_name
20
+ "#{Phrase.prefix}phrase_#{@display_key}#{Phrase.suffix}"
130
21
  end
131
22
  end
132
-
133
- def api_client
134
- @api_client ||= Phrase::Api::Client.new(Phrase.auth_token)
135
- end
136
-
137
- def log(message)
138
- message = "phrase: #{message}"
139
- if defined?(Rails)
140
- Rails.logger.warn(message)
141
- else
142
- $stderr.puts message
143
- end
144
- end
145
-
146
- def cache
147
- Thread.current[:phrase_cache] ||= build_cache
148
- end
149
-
150
- def build_cache
151
- cache = Phrase::Cache.new
152
- end
153
-
154
- def warm_translation_key_names_cache
155
- cache.set(:translation_key_names, prefetched_key_names)
156
- end
157
-
158
- def prefetched_key_names
159
- prefetched = Set.new
160
- Phrase.cache_key_segments_initial.each do |segment|
161
- result = api_client.translate(segment)
162
- prefetched.add(segment) if result.is_a?(String)
163
- prefetched = prefetched.merge(key_names_from_nested(segment, result))
164
- end
165
- prefetched
166
- end
167
-
168
- def key_names_from_nested(segment, data)
169
- key_names = Set.new
170
- Phrase::HashFlattener.flatten(data, nil) do |key, value|
171
- key_names.add("#{segment}.#{key}") unless value.is_a?(Hash)
172
- end unless (data.is_a?(String) || data.nil?)
173
- key_names
174
- end
175
23
  end
@@ -0,0 +1,27 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ require 'phrase/delegate'
4
+
5
+ class Phrase::Delegate::FastGettext < Phrase::Delegate::Base
6
+ def initialize(method, *args)
7
+ @method = method
8
+ params = params_from_args(args)
9
+ @display_key = params[:msgid]
10
+ end
11
+
12
+ private
13
+ def params_from_args(args)
14
+ params = case @method
15
+ when :_
16
+ {msgid: args.first}
17
+ when :n_
18
+ {msgid: args.first, msgid_plural: args.second, count: args.last}
19
+ when :s_
20
+ {msgid: args.first}
21
+ else
22
+ self.class.log("Unsupported FastGettext method #{@method}")
23
+ {}
24
+ end
25
+ params
26
+ end
27
+ end
@@ -0,0 +1,159 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ require 'phrase/api'
4
+ require 'phrase/cache'
5
+ require 'phrase/hash_flattener'
6
+ require 'phrase/delegate'
7
+ require 'set'
8
+
9
+ class Phrase::Delegate::I18n < Phrase::Delegate::Base
10
+ attr_accessor :key, :display_key, :options, :api_client, :fallback_keys
11
+
12
+ def initialize(key, options={})
13
+ @display_key = @key = key
14
+ @options = options
15
+
16
+ @fallback_keys = []
17
+
18
+ extract_fallback_keys
19
+ identify_key_to_display if @fallback_keys.any?
20
+ super(decorated_key_name)
21
+ end
22
+
23
+ def method_missing(*args, &block)
24
+ if @key.respond_to?(args.first)
25
+ to_s.send(*args)
26
+ else
27
+ data = translation_or_subkeys
28
+ if data.is_a?(String)
29
+ to_s
30
+ elsif data.respond_to?(args.first)
31
+ data.send(*args, &block)
32
+ else
33
+ self.class.log "You are trying to execute the method ##{args.first} on a translation key which is not supported. Please make sure you treat your translations as strings only."
34
+ nil
35
+ end
36
+ end
37
+ end
38
+
39
+ private
40
+ def identify_key_to_display
41
+ key_names = [@key] | @fallback_keys
42
+ available_key_names = find_keys_within_phrase(key_names)
43
+ @display_key = @key
44
+ key_names.each do |item|
45
+ if available_key_names.include?(item)
46
+ @display_key = item
47
+ break
48
+ end
49
+ end
50
+ end
51
+
52
+ def find_keys_within_phrase(key_names)
53
+ key_names_to_check_against_api = key_names - pre_fetched(key_names)
54
+ pre_cached(key_names) | key_names_returned_from_api_for(key_names_to_check_against_api)
55
+ end
56
+
57
+ def pre_cached(key_names)
58
+ warm_translation_key_names_cache unless cache.cached?(:translation_key_names)
59
+ pre_cached_key_names = key_names.select { |key_name| key_name_precached?(key_name) }
60
+ pre_cached_key_names
61
+ end
62
+
63
+ def pre_fetched(key_names)
64
+ key_names.select { |key_name| covered_by_initial_caching?(key_name) }
65
+ end
66
+
67
+ def key_name_precached?(key_name)
68
+ covered = covered_by_initial_caching?(key_name)
69
+ in_cache = key_name_is_in_cache?(key_name)
70
+ covered && in_cache
71
+ end
72
+
73
+ def key_names_returned_from_api_for(key_names)
74
+ if key_names.size > 0
75
+ api_client.find_keys_by_name(key_names).map { |key| key["name"] }
76
+ else
77
+ []
78
+ end
79
+ end
80
+
81
+ def key_name_is_in_cache?(key_name)
82
+ cache.get(:translation_key_names).include?(key_name)
83
+ end
84
+
85
+ def covered_by_initial_caching?(key_name)
86
+ key_name.start_with?(*Phrase.cache_key_segments_initial)
87
+ end
88
+
89
+ def extract_fallback_keys
90
+ fallback_items = []
91
+ if @options.has_key?(:default)
92
+ if @options[:default].kind_of?(Array)
93
+ fallback_items = @options[:default]
94
+ else
95
+ fallback_items << @options[:default]
96
+ end
97
+ end
98
+
99
+ fallback_items.each do |item|
100
+ process_fallback_item(item)
101
+ end
102
+ end
103
+
104
+ def scoped(item)
105
+ @options.has_key?(:scope) ? "#{@options[:scope]}.#{item}" : item
106
+ end
107
+
108
+ def process_fallback_item(item)
109
+ if item.kind_of?(Symbol)
110
+ entry = scoped(item.to_s)
111
+ @fallback_keys << entry
112
+ if @key == "helpers.label.#{entry}" # http://apidock.com/rails/v3.1.0/ActionView/Helpers/FormHelper/label
113
+ @fallback_keys << "activerecord.attributes.#{entry}"
114
+ end
115
+ end
116
+ end
117
+
118
+ def translation_or_subkeys
119
+ begin
120
+ api_client.translate(@key)
121
+ rescue Exception => e
122
+ self.class.log "Server Error: #{e.message}"
123
+ end
124
+ end
125
+
126
+ def api_client
127
+ @api_client ||= Phrase::Api::Client.new(Phrase.auth_token)
128
+ end
129
+
130
+ def cache
131
+ Thread.current[:phrase_cache] ||= build_cache
132
+ end
133
+
134
+ def build_cache
135
+ cache = Phrase::Cache.new
136
+ end
137
+
138
+ def warm_translation_key_names_cache
139
+ cache.set(:translation_key_names, prefetched_key_names)
140
+ end
141
+
142
+ def prefetched_key_names
143
+ prefetched = Set.new
144
+ Phrase.cache_key_segments_initial.each do |segment|
145
+ result = api_client.translate(segment)
146
+ prefetched.add(segment) if result.is_a?(String)
147
+ prefetched = prefetched.merge(key_names_from_nested(segment, result))
148
+ end
149
+ prefetched
150
+ end
151
+
152
+ def key_names_from_nested(segment, data)
153
+ key_names = Set.new
154
+ Phrase::HashFlattener.flatten(data, nil) do |key, value|
155
+ key_names.add("#{segment}.#{key}") unless value.is_a?(Hash)
156
+ end unless (data.is_a?(String) || data.nil?)
157
+ key_names
158
+ end
159
+ end