phraseapp-in-context-editor-ruby 1.0.0rc1
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.
- 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
|