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,21 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
|
3
|
+
module PhraseappInContextEditor
|
4
|
+
module Generators
|
5
|
+
class InstallGenerator < Rails::Generators::Base
|
6
|
+
source_root File.expand_path("../templates", __FILE__)
|
7
|
+
|
8
|
+
desc "Creates a PhraseApp In-Context-Editor initializer for your application."
|
9
|
+
class_option :access_token, type: :string, desc: "Your PhraseApp access token", required: true
|
10
|
+
class_option :project_id, type: :string, desc: "Your PhraseApp project id", required: true
|
11
|
+
|
12
|
+
def copy_initializer
|
13
|
+
template "phraseapp_in_context_editor.rb", "config/initializers/phraseapp_in_context_editor.rb"
|
14
|
+
end
|
15
|
+
|
16
|
+
def show_readme
|
17
|
+
readme "README" if behavior == :invoke
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
===============================================================================
|
2
|
+
Welcome to PhraseApp!
|
3
|
+
|
4
|
+
PhraseApp is the translation management solution for web and mobile applications. Collaborate with your team, find professional translators and stay on top of the process.
|
5
|
+
|
6
|
+
Our In-Context Editor offers just that. It provides translators with useful contextual information which improves overall translation quality.
|
7
|
+
|
8
|
+
Login: https://phraseapp.com/account/login
|
9
|
+
Documentation: http://docs.phraseapp.com/guides/in-context-editor/
|
10
|
+
===============================================================================
|
@@ -0,0 +1,27 @@
|
|
1
|
+
PhraseApp::InContextEditor.configure do |config|
|
2
|
+
# Enable or disable the In-Context-Editor in general
|
3
|
+
config.enabled = true
|
4
|
+
|
5
|
+
# Fetch your project id after creating your first project
|
6
|
+
# in Translation Center.
|
7
|
+
# You can find the project id in your project settings
|
8
|
+
# page (https://phraseapp.com/projects)
|
9
|
+
config.project_id = "<%= options[:project_id] %>"
|
10
|
+
|
11
|
+
# You can create and manage access tokens in your profile settings
|
12
|
+
# in Translation Center or via the Authorizations API
|
13
|
+
# (http://docs.phraseapp.com/api/v2/authorizations/).
|
14
|
+
config.access_token = "<%= options[:access_token] %>"
|
15
|
+
|
16
|
+
# Configure an array of key names that should not be handled
|
17
|
+
# by the In-Context-Editor.
|
18
|
+
config.ignored_keys = ["number.*", "breadcrumb.*"]
|
19
|
+
|
20
|
+
# PhraseApp uses decorators to generate a unique identification key
|
21
|
+
# in context of your document. However, this might result in conflicts
|
22
|
+
# with other libraries (e.g. client-side template engines) that use a similar syntax.
|
23
|
+
# If you encounter this problem, you might want to change this decorator pattern.
|
24
|
+
# More information: http://docs.phraseapp.com/guides/in-context-editor/configure/
|
25
|
+
# config.prefix = "{{__"
|
26
|
+
# config.suffix = "__}}"
|
27
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
require 'phraseapp-ruby'
|
3
|
+
|
4
|
+
module PhraseApp
|
5
|
+
module InContextEditor
|
6
|
+
autoload :Config, 'phraseapp-in-context-editor-ruby/config'
|
7
|
+
|
8
|
+
class << self
|
9
|
+
def config
|
10
|
+
Thread.current[:phraseapp_config] ||= PhraseApp::InContextEditor::Config.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def config=(value)
|
14
|
+
Thread.current[:phraseapp_config] = value
|
15
|
+
end
|
16
|
+
|
17
|
+
def backend
|
18
|
+
config.backend
|
19
|
+
end
|
20
|
+
|
21
|
+
def suffix
|
22
|
+
config.suffix
|
23
|
+
end
|
24
|
+
|
25
|
+
def prefix
|
26
|
+
config.prefix
|
27
|
+
end
|
28
|
+
|
29
|
+
def project_id
|
30
|
+
config.project_id
|
31
|
+
end
|
32
|
+
|
33
|
+
def access_token
|
34
|
+
config.access_token
|
35
|
+
end
|
36
|
+
|
37
|
+
def cache_key_segments_initial
|
38
|
+
config.cache_key_segments_initial
|
39
|
+
end
|
40
|
+
|
41
|
+
def cache_lifetime
|
42
|
+
config.cache_lifetime
|
43
|
+
end
|
44
|
+
|
45
|
+
def ignored_keys
|
46
|
+
config.ignored_keys
|
47
|
+
end
|
48
|
+
|
49
|
+
def enabled=(value)
|
50
|
+
config.enabled = value
|
51
|
+
end
|
52
|
+
|
53
|
+
def enabled?
|
54
|
+
config.enabled
|
55
|
+
end
|
56
|
+
|
57
|
+
def disabled?
|
58
|
+
!config.enabled
|
59
|
+
end
|
60
|
+
|
61
|
+
def js_use_ssl
|
62
|
+
config.js_use_ssl
|
63
|
+
end
|
64
|
+
|
65
|
+
def js_host
|
66
|
+
config.js_host
|
67
|
+
end
|
68
|
+
|
69
|
+
def api_client
|
70
|
+
config.api_client
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.configure
|
75
|
+
yield(self.config)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
autoload :ViewHelpers, 'phraseapp-in-context-editor-ruby/view_helpers'
|
80
|
+
|
81
|
+
require 'phraseapp-in-context-editor-ruby/version'
|
82
|
+
require 'phraseapp-in-context-editor-ruby/engine'
|
83
|
+
require 'phraseapp-in-context-editor-ruby/delegate'
|
84
|
+
require 'phraseapp-in-context-editor-ruby/backend_service'
|
85
|
+
require 'phraseapp-in-context-editor-ruby/view_helpers'
|
86
|
+
end
|
87
|
+
|
88
|
+
# Only load adapters directly if non-rails app, otherwise use engine
|
89
|
+
unless defined? Rails
|
90
|
+
require 'phraseapp-in-context-editor-ruby/adapters/i18n'
|
91
|
+
require 'phraseapp-in-context-editor-ruby/adapters/fast_gettext'
|
92
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
|
3
|
+
require 'phraseapp-in-context-editor-ruby/delegate/fast_gettext'
|
4
|
+
|
5
|
+
module FastGettext
|
6
|
+
module Translation
|
7
|
+
def __with_phraseapp(*args)
|
8
|
+
PhraseApp::InContextEditor::Delegate::FastGettext.new(:_, *args)
|
9
|
+
end
|
10
|
+
alias_method :__without_phraseapp, :_
|
11
|
+
alias_method :_, :__with_phraseapp
|
12
|
+
|
13
|
+
def n__with_phraseapp(*args)
|
14
|
+
PhraseApp::InContextEditor::Delegate::FastGettext.new(:n_, *args)
|
15
|
+
end
|
16
|
+
alias_method :n__without_phraseapp, :n_
|
17
|
+
alias_method :n_, :n__with_phraseapp
|
18
|
+
|
19
|
+
def s__with_phraseapp(*args)
|
20
|
+
PhraseApp::InContextEditor::Delegate::FastGettext.new(:s_, *args)
|
21
|
+
end
|
22
|
+
alias_method :s__without_phraseapp, :s_
|
23
|
+
alias_method :s_, :s__with_phraseapp
|
24
|
+
end
|
25
|
+
end if defined? FastGettext::Translation
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
|
3
|
+
module I18n
|
4
|
+
class << self
|
5
|
+
def translate_with_phraseapp(*args)
|
6
|
+
PhraseApp::InContextEditor.backend.translate(*args)
|
7
|
+
end
|
8
|
+
alias_method :translate_without_phraseapp, :translate
|
9
|
+
alias_method :translate, :translate_with_phraseapp
|
10
|
+
alias_method :t, :translate
|
11
|
+
end
|
12
|
+
end if defined?(I18n)
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module PhraseApp
|
2
|
+
module InContextEditor
|
3
|
+
class ApiCollection
|
4
|
+
def initialize(api_client, action, ids=[], query=nil)
|
5
|
+
raise "PhraseApp API client can't handle action #{action}" unless api_client.respond_to?(action)
|
6
|
+
|
7
|
+
@api_client = api_client
|
8
|
+
@action = action
|
9
|
+
@ids = ids
|
10
|
+
@query = query
|
11
|
+
end
|
12
|
+
|
13
|
+
def collection
|
14
|
+
results = []
|
15
|
+
page = 1
|
16
|
+
per_page = 100
|
17
|
+
paginated, err = send_request(page, per_page)
|
18
|
+
results << paginated
|
19
|
+
|
20
|
+
while paginated.size == per_page
|
21
|
+
break if page > 100
|
22
|
+
|
23
|
+
page = page + 1
|
24
|
+
paginated, err = send_request(page, per_page)
|
25
|
+
results << paginated if paginated.present?
|
26
|
+
end
|
27
|
+
|
28
|
+
results.flatten.uniq
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
def send_request(page, per_page)
|
33
|
+
if @query.present?
|
34
|
+
@api_client.send(@action, *@ids, page, per_page, @query)
|
35
|
+
else
|
36
|
+
@api_client.send(@action, *@ids, page, per_page)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'phraseapp-in-context-editor-ruby/api_collection'
|
2
|
+
|
3
|
+
module PhraseApp
|
4
|
+
module InContextEditor
|
5
|
+
class ApiWrapper
|
6
|
+
def initialize
|
7
|
+
@api_client = PhraseApp::InContextEditor.api_client
|
8
|
+
end
|
9
|
+
|
10
|
+
def default_locale
|
11
|
+
@default_locale ||= select_default_locale
|
12
|
+
end
|
13
|
+
|
14
|
+
def default_translation(key)
|
15
|
+
params = PhraseApp::RequestParams::TranslationsByKeyParams.new
|
16
|
+
translations = PhraseApp::InContextEditor::ApiCollection.new(@api_client, "translations_by_key", project_and_key_id(key), params).collection
|
17
|
+
return unless translations.present?
|
18
|
+
|
19
|
+
translations.select{ |translation| translation.locale["id"] == default_locale.id }
|
20
|
+
end
|
21
|
+
|
22
|
+
def keys_with_prefix(prefix)
|
23
|
+
params = PhraseApp::RequestParams::KeysListParams.new(q:"#{prefix}*")
|
24
|
+
keys_list(params)
|
25
|
+
end
|
26
|
+
|
27
|
+
def keys_by_names(names)
|
28
|
+
names = names.join(',')
|
29
|
+
params = PhraseApp::RequestParams::KeysListParams.new(:q => "name:#{names}")
|
30
|
+
keys_list(params)
|
31
|
+
end
|
32
|
+
|
33
|
+
def keys_list(params)
|
34
|
+
PhraseApp::InContextEditor::ApiCollection.new(@api_client, "keys_list", project_id, params).collection
|
35
|
+
end
|
36
|
+
|
37
|
+
def blacklisted_keys
|
38
|
+
PhraseApp::InContextEditor::ApiCollection.new(@api_client, "exclude_rules_index", project_id).collection.map{ |rule| rule.name }
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def select_default_locale
|
44
|
+
locales = PhraseApp::InContextEditor::ApiCollection.new(@api_client, "locales_list", project_id).collection
|
45
|
+
return unless locales.present?
|
46
|
+
|
47
|
+
locales.select{ |loc| loc.default }.first
|
48
|
+
end
|
49
|
+
|
50
|
+
def project_id
|
51
|
+
[PhraseApp::InContextEditor.project_id]
|
52
|
+
end
|
53
|
+
|
54
|
+
def project_and_key_id(key)
|
55
|
+
project_id << key.id
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
require 'phraseapp-in-context-editor-ruby/api_collection'
|
3
|
+
require 'phraseapp-in-context-editor-ruby/delegate/i18n'
|
4
|
+
|
5
|
+
module PhraseApp
|
6
|
+
module InContextEditor
|
7
|
+
class BackendService
|
8
|
+
|
9
|
+
attr_accessor :blacklisted_keys
|
10
|
+
|
11
|
+
def initialize(args = {})
|
12
|
+
self
|
13
|
+
end
|
14
|
+
|
15
|
+
def translate(*args)
|
16
|
+
if to_be_translated_without_phraseapp?(args)
|
17
|
+
I18n.translate_without_phraseapp(*args)
|
18
|
+
else
|
19
|
+
phraseapp_delegate_for(args)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
protected
|
24
|
+
def to_be_translated_without_phraseapp?(args)
|
25
|
+
PhraseApp::InContextEditor.disabled? or has_been_given_blacklisted_key?(args) or has_been_given_ignored_key?(args) or has_been_forced_to_resolve_with_phraseapp?(args)
|
26
|
+
end
|
27
|
+
|
28
|
+
def has_been_given_blacklisted_key?(args)
|
29
|
+
key = given_key_from_args(args)
|
30
|
+
has_blacklist_entry_for_key?(key)
|
31
|
+
end
|
32
|
+
|
33
|
+
def has_been_given_ignored_key?(args)
|
34
|
+
key = given_key_from_args(args)
|
35
|
+
key_is_ignored?(key)
|
36
|
+
end
|
37
|
+
|
38
|
+
def has_been_forced_to_resolve_with_phraseapp?(args)
|
39
|
+
(args.last.is_a?(Hash) and args.last[:resolve] == false)
|
40
|
+
end
|
41
|
+
|
42
|
+
def given_key_from_args(args)
|
43
|
+
extract_normalized_key_from_args(args)
|
44
|
+
end
|
45
|
+
|
46
|
+
def has_blacklist_entry_for_key?(key)
|
47
|
+
blacklisted_keys.each do |blacklisted_key|
|
48
|
+
return true if present?(key.to_s[/\A#{blacklisted_key.gsub("*", ".*")}\Z/])
|
49
|
+
end
|
50
|
+
false
|
51
|
+
end
|
52
|
+
|
53
|
+
def key_is_ignored?(key)
|
54
|
+
PhraseApp::InContextEditor.ignored_keys.each do |ignored_key|
|
55
|
+
return true if present?(key.to_s[/\A#{ignored_key.gsub("*", ".*")}\Z/])
|
56
|
+
end
|
57
|
+
false
|
58
|
+
end
|
59
|
+
|
60
|
+
def blacklisted_keys
|
61
|
+
@blacklisted_keys ||= api_wrapper.blacklisted_keys
|
62
|
+
end
|
63
|
+
|
64
|
+
def phraseapp_delegate_for(args)
|
65
|
+
key = given_key_from_args(args)
|
66
|
+
return nil unless present?(key)
|
67
|
+
options = args[1].nil? ? {} : args[1]
|
68
|
+
PhraseApp::InContextEditor::Delegate::I18n.new(key, options, args)
|
69
|
+
end
|
70
|
+
|
71
|
+
def extract_normalized_key_from_args(args)
|
72
|
+
transformed_args = transform_args(args)
|
73
|
+
normalized_key(transformed_args)
|
74
|
+
end
|
75
|
+
|
76
|
+
def transform_args(args)
|
77
|
+
duped_args = args.map { |item| (item.is_a?(Symbol) or item.nil?) ? item : item.dup }
|
78
|
+
transform_args_based_on_caller(duped_args)
|
79
|
+
end
|
80
|
+
|
81
|
+
def normalized_key(duped_args)
|
82
|
+
splitted_args = split_args(duped_args)
|
83
|
+
key = I18n::Backend::Flatten.normalize_flat_keys(*splitted_args)
|
84
|
+
key.gsub!("..", ".")
|
85
|
+
key.gsub!(/^\./, '')
|
86
|
+
key
|
87
|
+
end
|
88
|
+
|
89
|
+
def split_args(args)
|
90
|
+
options = options_from_args(args)
|
91
|
+
key ||= args.shift
|
92
|
+
locale = options.delete(:locale) || I18n.locale
|
93
|
+
return [locale, key, options[:scope], nil]
|
94
|
+
end
|
95
|
+
|
96
|
+
def options_from_args(args)
|
97
|
+
args.last.is_a?(Hash) ? args.pop : {}
|
98
|
+
end
|
99
|
+
|
100
|
+
def transform_args_based_on_caller(args)
|
101
|
+
translation_caller = identify_caller
|
102
|
+
|
103
|
+
if translation_caller and args.first =~ /^\./
|
104
|
+
options = options_from_args(args)
|
105
|
+
|
106
|
+
if not present?(options[:scope]) and present?(translation_caller)
|
107
|
+
options[:scope] = translation_caller
|
108
|
+
end
|
109
|
+
|
110
|
+
args.push(options)
|
111
|
+
parts = args.first.to_s.split(".").select { |e| not blank?(e) }
|
112
|
+
args[0] = parts[0] if parts.size == 1
|
113
|
+
end
|
114
|
+
|
115
|
+
args
|
116
|
+
end
|
117
|
+
|
118
|
+
def identify_caller
|
119
|
+
translation_caller = nil
|
120
|
+
send(:caller)[0..6].each do |intermediate_caller|
|
121
|
+
translation_caller = calling_template(intermediate_caller) unless translation_caller
|
122
|
+
end
|
123
|
+
|
124
|
+
if present?(translation_caller)
|
125
|
+
find_lookup_scope(translation_caller)
|
126
|
+
else
|
127
|
+
nil
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def calling_template(string)
|
132
|
+
string.match(/(views)(\/.+)(?>:[0-9]+:in)/)
|
133
|
+
end
|
134
|
+
|
135
|
+
def blank?(str)
|
136
|
+
raise "blank?(str) can only be given a String or nil" unless str.is_a?(String) or str.nil?
|
137
|
+
str.nil? or str == ''
|
138
|
+
end
|
139
|
+
|
140
|
+
def present?(str)
|
141
|
+
raise "present?(str) can only be given a String or nil" unless str.is_a?(String) or str.nil?
|
142
|
+
not blank?(str)
|
143
|
+
end
|
144
|
+
|
145
|
+
def find_lookup_scope(caller)
|
146
|
+
split_path = caller[2][1..-1].split(".")[0].split("/")
|
147
|
+
|
148
|
+
template_or_partial = remove_underscore_form_partial(split_path[-1])
|
149
|
+
split_path[-1] = template_or_partial
|
150
|
+
|
151
|
+
split_path.map!(&:to_sym)
|
152
|
+
end
|
153
|
+
|
154
|
+
def remove_underscore_form_partial(template_or_partial)
|
155
|
+
if template_or_partial.to_s[0,1] == "_"
|
156
|
+
template_or_partial.to_s[1..-1]
|
157
|
+
else
|
158
|
+
template_or_partial.to_s
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def api_wrapper
|
163
|
+
@api_wrapper ||= PhraseApp::InContextEditor::ApiWrapper.new
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|