phrase 0.2.3 → 0.2.4

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile.lock CHANGED
@@ -16,7 +16,7 @@ GEM
16
16
  crack (0.3.1)
17
17
  diff-lcs (1.1.3)
18
18
  i18n (0.6.0)
19
- json (1.7.4)
19
+ json (1.7.5)
20
20
  multi_json (1.3.6)
21
21
  rspec (2.8.0)
22
22
  rspec-core (~> 2.8.0)
@@ -26,6 +26,7 @@ GEM
26
26
  rspec-expectations (2.8.0)
27
27
  diff-lcs (~> 1.1.2)
28
28
  rspec-mocks (2.8.0)
29
+ timecop (0.4.5)
29
30
  vcr (2.2.1)
30
31
  webmock (1.8.7)
31
32
  addressable (>= 2.2.7)
@@ -38,5 +39,6 @@ DEPENDENCIES
38
39
  i18n
39
40
  phrase!
40
41
  rspec
42
+ timecop
41
43
  vcr
42
44
  webmock
@@ -23,7 +23,7 @@ class Phrase::Api::Client
23
23
  def fetch_locales
24
24
  result = perform_api_request("/locales", :get)
25
25
  locales = []
26
- JSON.parse(result).map do |locale|
26
+ parsed(result).map do |locale|
27
27
  locales << locale['name']
28
28
  end
29
29
  locales
@@ -32,7 +32,7 @@ class Phrase::Api::Client
32
32
  def fetch_blacklisted_keys
33
33
  result = perform_api_request("/blacklisted_keys", :get)
34
34
  blacklisted_keys = []
35
- JSON.parse(result).map do |blacklisted_key|
35
+ parsed(result).map do |blacklisted_key|
36
36
  blacklisted_keys << blacklisted_key['name']
37
37
  end
38
38
  blacklisted_keys
@@ -40,15 +40,14 @@ class Phrase::Api::Client
40
40
 
41
41
  def translate(key)
42
42
  raise "You must specify a key" if key.nil? or key.blank?
43
- keys = {}
44
- result = JSON.parse(perform_api_request("/translation_keys/translate", :get, {:key => key}))
43
+ keys = {}
44
+ result = parsed(perform_api_request("/translation_keys/translate", :get, {:key => key}))
45
45
  keys = extract_structured_object(result["translate"]) if result["translate"]
46
46
  keys
47
47
  end
48
48
 
49
49
  def find_keys_by_name(key_names=[])
50
- result = JSON.parse(perform_api_request("/translation_keys", :get, {:key_names => key_names}))
51
- result
50
+ parsed(perform_api_request("/translation_keys", :get, {:key_names => key_names}))
52
51
  end
53
52
 
54
53
  def create_locale(name)
@@ -128,11 +127,11 @@ private
128
127
  def api_error_message(response)
129
128
  message = ""
130
129
  begin
131
- error = JSON.parse(response.body)["error"]
130
+ error = parsed(response.body)["error"]
132
131
  if error.class == String
133
132
  message = error
134
133
  else
135
- message = JSON.parse(response.body)["message"]
134
+ message = parsed(response.body)["message"]
136
135
  end
137
136
  rescue JSON::ParserError
138
137
  end
@@ -212,4 +211,8 @@ private
212
211
  end.join(separator)
213
212
  request.content_type = 'application/x-www-form-urlencoded'
214
213
  end
214
+
215
+ def parsed(raw_data)
216
+ JSON.parse(raw_data)
217
+ end
215
218
  end
@@ -0,0 +1,34 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ require 'phrase'
4
+
5
+ class Phrase::Cache
6
+
7
+ attr_accessor :lifetime
8
+
9
+ def initialize(args={})
10
+ @store = {}
11
+ @lifetime = args.fetch(:lifetime, Phrase.cache_lifetime)
12
+ end
13
+
14
+ def cached?(cache_key)
15
+ @store.has_key?(cache_key) && !expired?(cache_key)
16
+ end
17
+
18
+ def get(cache_key)
19
+ begin
20
+ @store.fetch(cache_key)[:payload]
21
+ rescue
22
+ nil
23
+ end
24
+ end
25
+
26
+ def set(cache_key, value)
27
+ @store[cache_key] = {timestamp: Time.now, payload: value}
28
+ end
29
+
30
+ private
31
+ def expired?(cache_key)
32
+ @store.fetch(cache_key)[:timestamp] < (Time.now - @lifetime)
33
+ end
34
+ end
data/lib/phrase/config.rb CHANGED
@@ -67,4 +67,20 @@ class Phrase::Config
67
67
  def js_use_ssl=(js_use_ssl)
68
68
  @@js_use_ssl = js_use_ssl
69
69
  end
70
- end
70
+
71
+ def cache_key_segments_initial
72
+ @@cache_key_segments_initial ||= ["simple_form"]
73
+ end
74
+
75
+ def cache_key_segments_initial=(cache_key_segments_initial=[])
76
+ @@cache_key_segments_initial = cache_key_segments_initial
77
+ end
78
+
79
+ def cache_lifetime
80
+ @@cache_lifetime ||= 300
81
+ end
82
+
83
+ def cache_lifetime=(cache_lifetime)
84
+ @@cache_lifetime = cache_lifetime
85
+ end
86
+ end
@@ -1,6 +1,9 @@
1
1
  # -*- encoding : utf-8 -*-
2
2
 
3
3
  require 'phrase/api'
4
+ require 'phrase/cache'
5
+ require 'phrase/hash_flattener'
6
+ require 'set'
4
7
 
5
8
  class Phrase::Delegate < String
6
9
  attr_accessor :key, :display_key, :options, :api_client, :fallback_keys
@@ -8,6 +11,7 @@ class Phrase::Delegate < String
8
11
  def initialize(key, options={})
9
12
  @display_key = @key = key
10
13
  @options = options
14
+
11
15
  @fallback_keys = []
12
16
 
13
17
  extract_fallback_keys
@@ -38,7 +42,7 @@ class Phrase::Delegate < String
38
42
  private
39
43
  def identify_key_to_display
40
44
  key_names = [@key] | @fallback_keys
41
- available_key_names = find_keys_from_service(key_names).map { |key| key["name"] }
45
+ available_key_names = find_keys_within_phrase(key_names)
42
46
  @display_key = @key
43
47
  key_names.each do |item|
44
48
  if available_key_names.include?(item)
@@ -48,8 +52,41 @@ private
48
52
  end
49
53
  end
50
54
 
51
- def find_keys_from_service(key_names)
52
- api_client.find_keys_by_name(key_names)
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)
53
90
  end
54
91
 
55
92
  def extract_fallback_keys
@@ -61,16 +98,22 @@ private
61
98
  fallback_items << @options[:default]
62
99
  end
63
100
  end
101
+
64
102
  fallback_items.each do |item|
65
103
  process_fallback_item(item)
66
104
  end
67
105
  end
68
106
 
107
+ def scoped(item)
108
+ @options.has_key?(:scope) ? "#{@options[:scope]}.#{item}" : item
109
+ end
110
+
69
111
  def process_fallback_item(item)
70
112
  if item.kind_of?(Symbol)
71
- @fallback_keys << item.to_s
72
- if @key == "helpers.label.#{item.to_s}" # http://apidock.com/rails/v3.1.0/ActionView/Helpers/FormHelper/label
73
- @fallback_keys << "activerecord.attributes.#{item.to_s}"
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}"
74
117
  end
75
118
  end
76
119
  end
@@ -99,4 +142,34 @@ private
99
142
  $stderr.puts message
100
143
  end
101
144
  end
102
- 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
+ end
@@ -0,0 +1,20 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ module Phrase::HashFlattener
4
+
5
+ FLATTEN_SEPARATOR = "."
6
+ SEPARATOR_ESCAPE_CHAR = "\001"
7
+
8
+ def self.escape_default_separator(key)
9
+ key.to_s.tr(FLATTEN_SEPARATOR, SEPARATOR_ESCAPE_CHAR)
10
+ end
11
+
12
+ def self.flatten(hash, escape, previous_key=nil, &block)
13
+ hash.each_pair do |key, value|
14
+ key = escape_default_separator(key) if escape
15
+ current_key = [previous_key, key].compact.join(FLATTEN_SEPARATOR).to_sym
16
+ yield current_key, value
17
+ flatten(value, escape, current_key, &block) if value.is_a?(Hash)
18
+ end
19
+ end
20
+ end
@@ -20,7 +20,8 @@ class Phrase::Tool::Options
20
20
  recursive: false
21
21
  },
22
22
  pull: {
23
- format: "yml"
23
+ format: "yml",
24
+ target: "./phrase/locales/"
24
25
  }
25
26
  }
26
27
  options.parse!(args)
@@ -61,6 +62,10 @@ private
61
62
  opts.on("--format=yml", String, "Allowed formats: #{Phrase::Tool::ALLOWED_DOWNLOAD_FORMATS.join(", ")}") do |format|
62
63
  @data[command_name][:format] = format
63
64
  end
65
+
66
+ opts.on("--target=./phrase/locales", String, "Target folder to store locale files") do |target|
67
+ @data[command_name][:target] = target
68
+ end
64
69
  end
65
70
  else
66
71
  OptionParser.new do |opts|
data/lib/phrase/tool.rb CHANGED
@@ -12,6 +12,7 @@ class Phrase::Tool
12
12
  ALLOWED_FILE_TYPES = %w(yml pot po)
13
13
  ALLOWED_DOWNLOAD_FORMATS = %w(yml po)
14
14
  DEFAULT_DOWNLOAD_FORMAT = "yml"
15
+ DEFAULT_TARGET_FOLDER = "phrase/locales/"
15
16
 
16
17
  attr_accessor :config, :options
17
18
 
@@ -90,6 +91,8 @@ protected
90
91
  end
91
92
 
92
93
  format = @options.get(:format) || DEFAULT_DOWNLOAD_FORMAT
94
+ target = @options.get(:target) || DEFAULT_TARGET_FOLDER
95
+
93
96
  unless ALLOWED_DOWNLOAD_FORMATS.include?(format)
94
97
  print_error "Invalid format: #{format}"
95
98
  exit(43)
@@ -97,7 +100,7 @@ protected
97
100
 
98
101
  locales.each do |locale_name|
99
102
  print "Downloading phrase.#{locale_name}.#{format}..."
100
- fetch_translations_for_locale(locale_name, format)
103
+ fetch_translations_for_locale(locale_name, format, target)
101
104
  end
102
105
  end
103
106
 
@@ -110,7 +113,7 @@ usage: phrase <command> [<args>]
110
113
  phrase push FILE [--tags=<tags>]
111
114
  phrase push DIRECTORY [--tags=<tags>]
112
115
 
113
- phrase pull [LOCALE]
116
+ phrase pull [LOCALE] [--target=<target-folder>]
114
117
 
115
118
  phrase --version
116
119
  USAGE
@@ -181,20 +184,28 @@ private
181
184
  end
182
185
  end
183
186
 
184
- def fetch_translations_for_locale(name, format=DEFAULT_DOWNLOAD_FORMAT)
187
+ def fetch_translations_for_locale(name, format=DEFAULT_DOWNLOAD_FORMAT, target=DEFAULT_TARGET_FOLDER)
185
188
  begin
186
189
  content = api_client.download_translations_for_locale(name, format)
187
190
  print_message "OK"
188
- store_translations_file(name, content, format)
191
+ store_translations_file(name, content, format, target)
189
192
  rescue Exception => e
190
193
  print_error "Failed"
191
194
  print_server_error(e.message)
192
195
  end
193
196
  end
194
197
 
195
- def store_translations_file(name, content, format=DEFAULT_DOWNLOAD_FORMAT)
196
- File.open("phrase/locales/phrase.#{name}.#{format}", "w") do |file|
197
- file.write(content)
198
+ def store_translations_file(name, content, format=DEFAULT_DOWNLOAD_FORMAT, target=DEFAULT_TARGET_FOLDER)
199
+ directory = target
200
+ directory << "/" unless directory.end_with?("/")
201
+
202
+ if File.directory?(directory)
203
+ File.open("#{directory}phrase.#{name}.#{format}", "w") do |file|
204
+ file.write(content)
205
+ end
206
+ else
207
+ print_error("Cannot write file to target folder (#{directory})")
208
+ exit(101)
198
209
  end
199
210
  end
200
211
 
@@ -1,3 +1,3 @@
1
1
  module Phrase
2
- VERSION = "0.2.3"
2
+ VERSION = "0.2.4"
3
3
  end
data/lib/phrase.rb CHANGED
@@ -18,7 +18,7 @@ module Phrase
18
18
  Thread.current[:phrase_config] = value
19
19
  end
20
20
 
21
- %w(enabled backend prefix suffix auth_token client_version js_host js_use_ssl).each do |method|
21
+ %w(enabled backend prefix suffix auth_token client_version js_host js_use_ssl cache_key_segments_initial cache_lifetime).each do |method|
22
22
  module_eval <<-DELEGATORS, __FILE__, __LINE__ + 1
23
23
  def #{method}
24
24
  config.#{method}
data/phrase.gemspec CHANGED
@@ -27,4 +27,5 @@ Gem::Specification.new do |s|
27
27
  s.add_development_dependency('i18n')
28
28
  s.add_development_dependency('webmock')
29
29
  s.add_development_dependency('vcr')
30
+ s.add_development_dependency('timecop')
30
31
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: phrase
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.2.4
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-08-22 00:00:00.000000000 Z
12
+ date: 2012-10-09 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport
16
- requirement: &70265216292960 !ruby/object:Gem::Requirement
16
+ requirement: &70298691089780 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ~>
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: '3.0'
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *70265216292960
24
+ version_requirements: *70298691089780
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: addressable
27
- requirement: &70265216292500 !ruby/object:Gem::Requirement
27
+ requirement: &70298691089320 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ~>
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: 2.2.8
33
33
  type: :runtime
34
34
  prerelease: false
35
- version_requirements: *70265216292500
35
+ version_requirements: *70298691089320
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: json
38
- requirement: &70265216292120 !ruby/object:Gem::Requirement
38
+ requirement: &70298691088940 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ! '>='
@@ -43,10 +43,10 @@ dependencies:
43
43
  version: '0'
44
44
  type: :runtime
45
45
  prerelease: false
46
- version_requirements: *70265216292120
46
+ version_requirements: *70298691088940
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: rspec
49
- requirement: &70265216291660 !ruby/object:Gem::Requirement
49
+ requirement: &70298691088480 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - ! '>='
@@ -54,10 +54,10 @@ dependencies:
54
54
  version: '0'
55
55
  type: :development
56
56
  prerelease: false
57
- version_requirements: *70265216291660
57
+ version_requirements: *70298691088480
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: i18n
60
- requirement: &70265216291240 !ruby/object:Gem::Requirement
60
+ requirement: &70298691088060 !ruby/object:Gem::Requirement
61
61
  none: false
62
62
  requirements:
63
63
  - - ! '>='
@@ -65,10 +65,10 @@ dependencies:
65
65
  version: '0'
66
66
  type: :development
67
67
  prerelease: false
68
- version_requirements: *70265216291240
68
+ version_requirements: *70298691088060
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: webmock
71
- requirement: &70265216290820 !ruby/object:Gem::Requirement
71
+ requirement: &70298691087640 !ruby/object:Gem::Requirement
72
72
  none: false
73
73
  requirements:
74
74
  - - ! '>='
@@ -76,10 +76,10 @@ dependencies:
76
76
  version: '0'
77
77
  type: :development
78
78
  prerelease: false
79
- version_requirements: *70265216290820
79
+ version_requirements: *70298691087640
80
80
  - !ruby/object:Gem::Dependency
81
81
  name: vcr
82
- requirement: &70265216290400 !ruby/object:Gem::Requirement
82
+ requirement: &70298691087220 !ruby/object:Gem::Requirement
83
83
  none: false
84
84
  requirements:
85
85
  - - ! '>='
@@ -87,7 +87,18 @@ dependencies:
87
87
  version: '0'
88
88
  type: :development
89
89
  prerelease: false
90
- version_requirements: *70265216290400
90
+ version_requirements: *70298691087220
91
+ - !ruby/object:Gem::Dependency
92
+ name: timecop
93
+ requirement: &70298691086800 !ruby/object:Gem::Requirement
94
+ none: false
95
+ requirements:
96
+ - - ! '>='
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ type: :development
100
+ prerelease: false
101
+ version_requirements: *70298691086800
91
102
  description: phrase allows you to edit translations in-place on the page itself. More
92
103
  information at phraseapp.com
93
104
  email:
@@ -113,9 +124,11 @@ files:
113
124
  - lib/phrase/backend.rb
114
125
  - lib/phrase/backend/base.rb
115
126
  - lib/phrase/backend/phrase_service.rb
127
+ - lib/phrase/cache.rb
116
128
  - lib/phrase/config.rb
117
129
  - lib/phrase/delegate.rb
118
130
  - lib/phrase/engine.rb
131
+ - lib/phrase/hash_flattener.rb
119
132
  - lib/phrase/tool.rb
120
133
  - lib/phrase/tool/config.rb
121
134
  - lib/phrase/tool/options.rb