phrase 0.2.3 → 0.2.4

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.
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