phraseapp-in-context-editor-ruby 1.0.0rc1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +25 -0
- data/.travis.yml +6 -0
- data/Gemfile +3 -0
- data/Guardfile +29 -0
- data/LICENSE +22 -0
- data/README.md +72 -0
- data/cacert.pem +3366 -0
- data/lib/generators/phraseapp_in_context_editor/install_generator.rb +21 -0
- data/lib/generators/phraseapp_in_context_editor/templates/README +10 -0
- data/lib/generators/phraseapp_in_context_editor/templates/phraseapp_in_context_editor.rb +27 -0
- data/lib/phraseapp-in-context-editor-ruby.rb +92 -0
- data/lib/phraseapp-in-context-editor-ruby/adapters/fast_gettext.rb +25 -0
- data/lib/phraseapp-in-context-editor-ruby/adapters/i18n.rb +12 -0
- data/lib/phraseapp-in-context-editor-ruby/api_collection.rb +41 -0
- data/lib/phraseapp-in-context-editor-ruby/api_wrapper.rb +59 -0
- data/lib/phraseapp-in-context-editor-ruby/backend_service.rb +167 -0
- data/lib/phraseapp-in-context-editor-ruby/cache.rb +37 -0
- data/lib/phraseapp-in-context-editor-ruby/config.rb +110 -0
- data/lib/phraseapp-in-context-editor-ruby/delegate.rb +40 -0
- data/lib/phraseapp-in-context-editor-ruby/delegate/fast_gettext.rb +33 -0
- data/lib/phraseapp-in-context-editor-ruby/delegate/i18n.rb +192 -0
- data/lib/phraseapp-in-context-editor-ruby/engine.rb +20 -0
- data/lib/phraseapp-in-context-editor-ruby/hash_flattener.rb +53 -0
- data/lib/phraseapp-in-context-editor-ruby/version.rb +6 -0
- data/lib/phraseapp-in-context-editor-ruby/view_helpers.rb +23 -0
- data/phraseapp-in-context-editor-ruby.gemspec +31 -0
- metadata +183 -0
@@ -0,0 +1,37 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
|
3
|
+
require 'phraseapp-in-context-editor-ruby'
|
4
|
+
|
5
|
+
module PhraseApp
|
6
|
+
module InContextEditor
|
7
|
+
class Cache
|
8
|
+
attr_accessor :lifetime
|
9
|
+
|
10
|
+
def initialize(args={})
|
11
|
+
@store = {}
|
12
|
+
@lifetime = args.fetch(:lifetime, PhraseApp::InContextEditor.cache_lifetime)
|
13
|
+
end
|
14
|
+
|
15
|
+
def cached?(cache_key)
|
16
|
+
@store.has_key?(cache_key) && !expired?(cache_key)
|
17
|
+
end
|
18
|
+
|
19
|
+
def get(cache_key)
|
20
|
+
begin
|
21
|
+
@store.fetch(cache_key)[:payload]
|
22
|
+
rescue
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def set(cache_key, value)
|
28
|
+
@store[cache_key] = {timestamp: Time.now, payload: value}
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
def expired?(cache_key)
|
33
|
+
@store.fetch(cache_key)[:timestamp] < (Time.now - @lifetime)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
|
3
|
+
module PhraseApp
|
4
|
+
module InContextEditor
|
5
|
+
class Config
|
6
|
+
def project_id
|
7
|
+
@@project_id = "" if !defined? @@project_id or @@project_id.nil?
|
8
|
+
@@project_id
|
9
|
+
end
|
10
|
+
|
11
|
+
def project_id=(project_id)
|
12
|
+
@@project_id = project_id
|
13
|
+
end
|
14
|
+
|
15
|
+
def access_token
|
16
|
+
@@access_token = "" if !defined? @@access_token or @@access_token.nil?
|
17
|
+
@@access_token
|
18
|
+
end
|
19
|
+
|
20
|
+
def access_token=(access_token)
|
21
|
+
@@access_token = access_token
|
22
|
+
end
|
23
|
+
|
24
|
+
def enabled
|
25
|
+
@@enabled = false if !defined? @@enabled or @@enabled.nil?
|
26
|
+
@@enabled
|
27
|
+
end
|
28
|
+
|
29
|
+
def enabled=(enabled)
|
30
|
+
@@enabled = enabled
|
31
|
+
end
|
32
|
+
|
33
|
+
def backend
|
34
|
+
@@backend ||= PhraseApp::InContextEditor::BackendService.new
|
35
|
+
end
|
36
|
+
|
37
|
+
def backend=(backend)
|
38
|
+
@@backend = backend
|
39
|
+
end
|
40
|
+
|
41
|
+
def api_client
|
42
|
+
@@api_client ||= authorized_api_client
|
43
|
+
end
|
44
|
+
|
45
|
+
def prefix
|
46
|
+
@@prefix ||= "{{__"
|
47
|
+
end
|
48
|
+
|
49
|
+
def prefix=(prefix)
|
50
|
+
@@prefix = prefix
|
51
|
+
end
|
52
|
+
|
53
|
+
def suffix
|
54
|
+
@@suffix ||= "__}}"
|
55
|
+
end
|
56
|
+
|
57
|
+
def suffix=(suffix)
|
58
|
+
@@suffix = suffix
|
59
|
+
end
|
60
|
+
|
61
|
+
def js_host
|
62
|
+
@@js_host ||= 'phraseapp.com'
|
63
|
+
end
|
64
|
+
|
65
|
+
def js_host=(js_host)
|
66
|
+
@@js_host = js_host
|
67
|
+
end
|
68
|
+
|
69
|
+
def js_use_ssl
|
70
|
+
@@js_use_ssl = true if !defined? @@js_use_ssl or @@js_use_ssl.nil?
|
71
|
+
@@js_use_ssl
|
72
|
+
end
|
73
|
+
|
74
|
+
def js_use_ssl=(js_use_ssl)
|
75
|
+
@@js_use_ssl = js_use_ssl
|
76
|
+
end
|
77
|
+
|
78
|
+
def cache_key_segments_initial
|
79
|
+
@@cache_key_segments_initial ||= ["simple_form"]
|
80
|
+
end
|
81
|
+
|
82
|
+
def cache_key_segments_initial=(cache_key_segments_initial=[])
|
83
|
+
@@cache_key_segments_initial = cache_key_segments_initial
|
84
|
+
end
|
85
|
+
|
86
|
+
def cache_lifetime
|
87
|
+
@@cache_lifetime ||= 300
|
88
|
+
end
|
89
|
+
|
90
|
+
def cache_lifetime=(cache_lifetime)
|
91
|
+
@@cache_lifetime = cache_lifetime
|
92
|
+
end
|
93
|
+
|
94
|
+
def ignored_keys
|
95
|
+
@@ignored_keys ||= []
|
96
|
+
end
|
97
|
+
|
98
|
+
def ignored_keys=(ignored_keys)
|
99
|
+
@@ignored_keys = ignored_keys
|
100
|
+
end
|
101
|
+
|
102
|
+
protected
|
103
|
+
def authorized_api_client
|
104
|
+
auth_handler = PhraseApp::Auth::AuthHandler.new(token: PhraseApp::InContextEditor.access_token)
|
105
|
+
PhraseApp::Auth.register_auth_handler(auth_handler)
|
106
|
+
PhraseApp::Client
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
|
3
|
+
module PhraseApp
|
4
|
+
module InContextEditor
|
5
|
+
module Delegate
|
6
|
+
class Base < String
|
7
|
+
def to_s
|
8
|
+
"#{decorated_key_name}"
|
9
|
+
end
|
10
|
+
alias :camelize :to_s
|
11
|
+
alias :underscore :to_s
|
12
|
+
alias :classify :to_s
|
13
|
+
alias :dasherize :to_s
|
14
|
+
alias :tableize :to_s
|
15
|
+
|
16
|
+
def self.log(message)
|
17
|
+
message = "phrase: #{message}"
|
18
|
+
if defined?(Rails) and Rails.respond_to?(:logger)
|
19
|
+
Rails.logger.warn(message)
|
20
|
+
else
|
21
|
+
$stderr.puts message
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
protected
|
26
|
+
def decorated_key_name
|
27
|
+
"#{PhraseApp::InContextEditor.prefix}phrase_#{normalized_display_key}#{PhraseApp::InContextEditor.suffix}"
|
28
|
+
end
|
29
|
+
|
30
|
+
def normalized_display_key
|
31
|
+
unless @display_key.nil?
|
32
|
+
@display_key.gsub("<", "[[[[[[html_open]]]]]]").gsub(">", "[[[[[[html_close]]]]]]")
|
33
|
+
else
|
34
|
+
@display_key
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
|
3
|
+
require 'phraseapp-in-context-editor-ruby/delegate'
|
4
|
+
|
5
|
+
module PhraseApp
|
6
|
+
module InContextEditor
|
7
|
+
module Delegate
|
8
|
+
class FastGettext < Base
|
9
|
+
def initialize(method, *args)
|
10
|
+
@method = method
|
11
|
+
params = params_from_args(args)
|
12
|
+
@display_key = params[:msgid]
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
def params_from_args(args)
|
17
|
+
params = case @method
|
18
|
+
when :_
|
19
|
+
{msgid: args.first}
|
20
|
+
when :n_
|
21
|
+
{msgid: args.first, msgid_plural: args[1], count: args.last}
|
22
|
+
when :s_
|
23
|
+
{msgid: args.first}
|
24
|
+
else
|
25
|
+
self.class.log("Unsupported FastGettext method #{@method}")
|
26
|
+
{}
|
27
|
+
end
|
28
|
+
params
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,192 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
|
3
|
+
require 'phraseapp-in-context-editor-ruby/cache'
|
4
|
+
require 'phraseapp-in-context-editor-ruby/hash_flattener'
|
5
|
+
require 'phraseapp-in-context-editor-ruby/delegate'
|
6
|
+
require 'phraseapp-in-context-editor-ruby/api_wrapper'
|
7
|
+
require 'set'
|
8
|
+
|
9
|
+
module PhraseApp
|
10
|
+
module InContextEditor
|
11
|
+
module Delegate
|
12
|
+
class I18n < Base
|
13
|
+
attr_accessor :key, :display_key, :options, :api_client, :fallback_keys, :original_args
|
14
|
+
|
15
|
+
def initialize(key, options={}, original_args=nil)
|
16
|
+
@display_key = @key = key
|
17
|
+
@options = options
|
18
|
+
@original_args = original_args
|
19
|
+
|
20
|
+
@fallback_keys = []
|
21
|
+
|
22
|
+
extract_fallback_keys
|
23
|
+
identify_key_to_display if @fallback_keys.any?
|
24
|
+
super(decorated_key_name)
|
25
|
+
end
|
26
|
+
|
27
|
+
def method_missing(*args, &block)
|
28
|
+
self.class.log "Trying to execute missing method ##{args.first} on key #{@key}"
|
29
|
+
if @key.respond_to?(args.first)
|
30
|
+
to_s.send(*args)
|
31
|
+
else
|
32
|
+
data = translation_or_subkeys
|
33
|
+
if data.respond_to?(args.first)
|
34
|
+
data.send(*args, &block)
|
35
|
+
else
|
36
|
+
self.class.log "You tried to execute the missing method ##{args.first} on key #{@key} which is not supported. Please make sure you treat your translations as strings only."
|
37
|
+
original_translation = I18n.translate_without_phraseapp(*@original_args)
|
38
|
+
original_translation.send(*args, &block)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
def identify_key_to_display
|
45
|
+
key_names = [@key] | @fallback_keys
|
46
|
+
available_key_names = find_keys_within_phraseapp(key_names)
|
47
|
+
@display_key = @key
|
48
|
+
key_names.each do |item|
|
49
|
+
if available_key_names.include?(item)
|
50
|
+
@display_key = item
|
51
|
+
break
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def find_keys_within_phraseapp(key_names)
|
57
|
+
key_names_to_check_against_api = key_names - pre_fetched(key_names)
|
58
|
+
pre_cached(key_names) | key_names_returned_from_api_for(key_names_to_check_against_api)
|
59
|
+
end
|
60
|
+
|
61
|
+
def pre_cached(key_names)
|
62
|
+
warm_translation_key_names_cache unless cache.cached?(:translation_key_names)
|
63
|
+
pre_cached_key_names = key_names.select { |key_name| key_name_precached?(key_name) }
|
64
|
+
pre_cached_key_names
|
65
|
+
end
|
66
|
+
|
67
|
+
def pre_fetched(key_names)
|
68
|
+
key_names.select { |key_name| covered_by_initial_caching?(key_name) }
|
69
|
+
end
|
70
|
+
|
71
|
+
def key_name_precached?(key_name)
|
72
|
+
covered = covered_by_initial_caching?(key_name)
|
73
|
+
in_cache = key_name_is_in_cache?(key_name)
|
74
|
+
covered && in_cache
|
75
|
+
end
|
76
|
+
|
77
|
+
def key_names_returned_from_api_for(key_names)
|
78
|
+
if key_names.size > 0
|
79
|
+
api_wrapper.keys_by_names(key_names).map{ |key| key.name }
|
80
|
+
else
|
81
|
+
[]
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def key_name_is_in_cache?(key_name)
|
86
|
+
cache.get(:translation_key_names).include?(key_name)
|
87
|
+
end
|
88
|
+
|
89
|
+
def covered_by_initial_caching?(key_name)
|
90
|
+
key_name.start_with?(*PhraseApp::InContextEditor.cache_key_segments_initial)
|
91
|
+
end
|
92
|
+
|
93
|
+
def extract_fallback_keys
|
94
|
+
fallback_items = []
|
95
|
+
if @options.has_key?(:default)
|
96
|
+
if @options[:default].kind_of?(Array)
|
97
|
+
fallback_items = @options[:default]
|
98
|
+
else
|
99
|
+
fallback_items << @options[:default]
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
fallback_items.each do |item|
|
104
|
+
process_fallback_item(item)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def scoped(item)
|
109
|
+
@options.has_key?(:scope) ? "#{@options[:scope]}.#{item}" : item
|
110
|
+
end
|
111
|
+
|
112
|
+
def process_fallback_item(item)
|
113
|
+
if item.kind_of?(Symbol)
|
114
|
+
entry = scoped(item.to_s)
|
115
|
+
@fallback_keys << entry
|
116
|
+
if @key == "helpers.label.#{entry}" # http://apidock.com/rails/v3.1.0/ActionView/Helpers/FormHelper/label
|
117
|
+
@fallback_keys << "activerecord.attributes.#{entry}"
|
118
|
+
end
|
119
|
+
|
120
|
+
if @key.start_with?("simple_form.") # special treatment for simple form
|
121
|
+
@fallback_keys << "activerecord.attributes.#{item.to_s}"
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def translation_or_subkeys
|
127
|
+
keys = api_wrapper.keys_with_prefix(@key)
|
128
|
+
return nil unless keys.present?
|
129
|
+
|
130
|
+
if keys.size == 1
|
131
|
+
translation_content_for_key(keys.first)
|
132
|
+
else
|
133
|
+
translation_hash = keys.inject({}) do |hash, key|
|
134
|
+
hash[key.name] = translation_content_for_key(key)
|
135
|
+
hash
|
136
|
+
end
|
137
|
+
|
138
|
+
PhraseApp::InContextEditor::HashFlattener.expand_flat_hash(translation_hash, @key)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def cache
|
143
|
+
Thread.current[:phraseapp_cache] ||= build_cache
|
144
|
+
end
|
145
|
+
|
146
|
+
def build_cache
|
147
|
+
cache = PhraseApp::InContextEditor::Cache.new
|
148
|
+
end
|
149
|
+
|
150
|
+
def warm_translation_key_names_cache
|
151
|
+
cache.set(:translation_key_names, prefetched_key_names)
|
152
|
+
end
|
153
|
+
|
154
|
+
def prefetched_key_names
|
155
|
+
prefetched = Set.new
|
156
|
+
PhraseApp::InContextEditor.cache_key_segments_initial.each do |prefix|
|
157
|
+
api_wrapper.keys_with_prefix(prefix).each do |key|
|
158
|
+
prefetched.add(key.name)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
prefetched
|
162
|
+
end
|
163
|
+
|
164
|
+
def key_names_from_nested(segment, data)
|
165
|
+
key_names = Set.new
|
166
|
+
PhraseApp::InContextEditor::HashFlattener.flatten(data, nil) do |key, value|
|
167
|
+
key_names.add("#{segment}.#{key}") unless value.is_a?(Hash)
|
168
|
+
end unless (data.is_a?(String) || data.nil?)
|
169
|
+
key_names
|
170
|
+
end
|
171
|
+
|
172
|
+
def translation_content_for_key(key)
|
173
|
+
default_translations = api_wrapper.default_translation(key)
|
174
|
+
return unless default_translations.present?
|
175
|
+
|
176
|
+
if key.plural
|
177
|
+
default_translations.inject({}) do |hash, translation|
|
178
|
+
hash[translation.plural_suffix.to_sym] = translation.content
|
179
|
+
hash
|
180
|
+
end
|
181
|
+
else
|
182
|
+
default_translations.first.content
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def api_wrapper
|
187
|
+
@api_wrapper ||= InContextEditor::ApiWrapper.new
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
require 'phraseapp-in-context-editor-ruby'
|
3
|
+
require 'i18n'
|
4
|
+
|
5
|
+
if defined? Rails
|
6
|
+
module PhraseApp
|
7
|
+
module InContextEditor
|
8
|
+
class Engine < Rails::Engine
|
9
|
+
initializer 'phraseapp-in-context-editor-ruby', after: :disable_dependency_loading do |app|
|
10
|
+
if PhraseApp::InContextEditor.enabled?
|
11
|
+
require 'phraseapp-in-context-editor-ruby/adapters/i18n'
|
12
|
+
require 'phraseapp-in-context-editor-ruby/adapters/fast_gettext'
|
13
|
+
end
|
14
|
+
|
15
|
+
ActionView::Base.send :include, PhraseApp::InContextEditor::ViewHelpers
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
module PhraseApp
|
3
|
+
module InContextEditor
|
4
|
+
module HashFlattener
|
5
|
+
FLATTEN_SEPARATOR = "."
|
6
|
+
SEPARATOR_ESCAPE_CHAR = "\001"
|
7
|
+
|
8
|
+
def self.flatten(hash, escape, previous_key=nil, &block)
|
9
|
+
hash.each_pair do |key, value|
|
10
|
+
key = escape_default_separator(key) if escape
|
11
|
+
current_key = [previous_key, key].compact.join(FLATTEN_SEPARATOR).to_sym
|
12
|
+
yield current_key, value
|
13
|
+
flatten(value, escape, current_key, &block) if value.is_a?(Hash)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.expand_flat_hash(flat_hash, prefix=nil)
|
18
|
+
flat_hash ||= []
|
19
|
+
result = flat_hash.map do |key, value|
|
20
|
+
key = key.gsub(/#{prefix}[\.]?/, '') if prefix
|
21
|
+
to_nested_hash(key, value)
|
22
|
+
end
|
23
|
+
|
24
|
+
result = result.inject({}) { |hash, subhash| hash.deep_merge!(subhash) }
|
25
|
+
result
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.to_nested_hash key, value
|
29
|
+
if contains_only_dots?(key) or starts_with_dot?(key) or ends_with_dot?(key)
|
30
|
+
{key.to_sym => value}
|
31
|
+
else
|
32
|
+
key.to_s.split(".").reverse.inject(value) { |hash, part| {part.to_sym => hash} }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.contains_only_dots?(string)
|
37
|
+
string.to_s.gsub(/\./, "").length == 0
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.starts_with_dot?(string)
|
41
|
+
string.to_s.start_with?(".")
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.ends_with_dot?(string)
|
45
|
+
string.to_s.end_with?(".")
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.escape_default_separator(key)
|
49
|
+
key.to_s.tr(FLATTEN_SEPARATOR, SEPARATOR_ESCAPE_CHAR)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|