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,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
|