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