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