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 +3 -1
- data/lib/phrase/api/client.rb +11 -8
- data/lib/phrase/cache.rb +34 -0
- data/lib/phrase/config.rb +17 -1
- data/lib/phrase/delegate.rb +80 -7
- data/lib/phrase/hash_flattener.rb +20 -0
- data/lib/phrase/tool/options.rb +6 -1
- data/lib/phrase/tool.rb +18 -7
- data/lib/phrase/version.rb +1 -1
- data/lib/phrase.rb +1 -1
- data/phrase.gemspec +1 -0
- metadata +29 -16
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.
|
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
|
data/lib/phrase/api/client.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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 =
|
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
|
-
|
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 =
|
130
|
+
error = parsed(response.body)["error"]
|
132
131
|
if error.class == String
|
133
132
|
message = error
|
134
133
|
else
|
135
|
-
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
|
data/lib/phrase/cache.rb
ADDED
@@ -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
|
-
|
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
|
data/lib/phrase/delegate.rb
CHANGED
@@ -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 =
|
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
|
52
|
-
|
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
|
-
|
72
|
-
|
73
|
-
|
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
|
-
|
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
|
data/lib/phrase/tool/options.rb
CHANGED
@@ -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
|
-
|
197
|
-
|
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
|
|
data/lib/phrase/version.rb
CHANGED
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
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.
|
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-
|
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: &
|
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: *
|
24
|
+
version_requirements: *70298691089780
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: addressable
|
27
|
-
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: *
|
35
|
+
version_requirements: *70298691089320
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: json
|
38
|
-
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: *
|
46
|
+
version_requirements: *70298691088940
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: rspec
|
49
|
-
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: *
|
57
|
+
version_requirements: *70298691088480
|
58
58
|
- !ruby/object:Gem::Dependency
|
59
59
|
name: i18n
|
60
|
-
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: *
|
68
|
+
version_requirements: *70298691088060
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: webmock
|
71
|
-
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: *
|
79
|
+
version_requirements: *70298691087640
|
80
80
|
- !ruby/object:Gem::Dependency
|
81
81
|
name: vcr
|
82
|
-
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: *
|
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
|