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