air18n_ui 0.0.4
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.
- data/Gemfile +2 -0
- data/Rakefile +9 -0
- data/air18n-ui.gemspec +24 -0
- data/app/assets/javascripts/contextual-translations.js +174 -0
- data/app/assets/javascripts/simple_translator.js +68 -0
- data/app/assets/stylesheets/contextual-translations.scss +111 -0
- data/app/controllers/air18n_ui_controller.rb +154 -0
- data/app/views/shared/_contextual_translations.html.erb +39 -0
- data/config/routes.rb +7 -0
- data/lib/air18n_ui/controller_filters.rb +10 -0
- data/lib/air18n_ui/engine.rb +6 -0
- data/lib/air18n_ui/version.rb +3 -0
- data/lib/air18n_ui.rb +5 -0
- metadata +128 -0
data/Gemfile
ADDED
data/Rakefile
ADDED
data/air18n-ui.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "air18n_ui/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |gem|
|
6
|
+
gem.authors = ['Jason Katz-Brown']
|
7
|
+
gem.email = ['jason@airbnb.com']
|
8
|
+
gem.description = %q{Translation interface for air18n.}
|
9
|
+
gem.summary = %q{Translation interface for air18n}
|
10
|
+
gem.homepage = "http://www.github.com/airbnb/air18n-ui"
|
11
|
+
|
12
|
+
gem.add_runtime_dependency 'i18n', '>= 0.5.0'
|
13
|
+
gem.add_runtime_dependency 'air18n', '>= 0.1.21'
|
14
|
+
gem.add_runtime_dependency 'rails', '~> 3.0'
|
15
|
+
|
16
|
+
gem.add_development_dependency "rspec"
|
17
|
+
|
18
|
+
gem.files = `git ls-files`.split($\)
|
19
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
20
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
21
|
+
gem.name = "air18n_ui"
|
22
|
+
gem.require_paths = ["lib"]
|
23
|
+
gem.version = Air18nUi::VERSION
|
24
|
+
end
|
@@ -0,0 +1,174 @@
|
|
1
|
+
(function($){
|
2
|
+
ContextualTranslations = {
|
3
|
+
init: function(){
|
4
|
+
// cache panel elements
|
5
|
+
var $translationPanel = $('#contextual-translation-panel');
|
6
|
+
var $originalPhrase = $('#original-phrase');
|
7
|
+
var $translatedPhrase = $('#translated-phrase');
|
8
|
+
var $phraseId = $('#phrase_id');
|
9
|
+
var $phraseKey = $('#phrase_key');
|
10
|
+
var $targetLocale = $('#target_locale');
|
11
|
+
|
12
|
+
// grab the phrase IDs that appear on this page
|
13
|
+
var ids = {};
|
14
|
+
|
15
|
+
$('phrase').each(function(i, e){
|
16
|
+
var $el = $(e);
|
17
|
+
var ref = ids[$el.data('phrase-id')];
|
18
|
+
|
19
|
+
if(ref){
|
20
|
+
ref.push($el.attr('id'));
|
21
|
+
}
|
22
|
+
else {
|
23
|
+
ids[$el.data('phrase-id')] = [];
|
24
|
+
ids[$el.data('phrase-id')].push($el.attr('id'));
|
25
|
+
}
|
26
|
+
});
|
27
|
+
|
28
|
+
// go to the server for phrase and their translations
|
29
|
+
$.ajax({
|
30
|
+
url: '/translate/phrase',
|
31
|
+
dataType: 'json',
|
32
|
+
type: 'POST',
|
33
|
+
data: {locale: $translationPanel.data('locale'), mappings: ids},
|
34
|
+
success: function(phrases){
|
35
|
+
for(var i = 0, pLength = phrases.length; i < pLength; i++){
|
36
|
+
var phrase = phrases[i];
|
37
|
+
|
38
|
+
for(var j = 0, eLength = phrase.elements.length; j < eLength; j++){
|
39
|
+
var $el = $('#' + phrase.elements[j]);
|
40
|
+
$el.data('phrase-original', phrase.original);
|
41
|
+
$el.data('phrase-latest', phrase.latest);
|
42
|
+
$el.data('phrase-key', phrase.key);
|
43
|
+
|
44
|
+
$el.addClass(phrase.latest ? 'updated' : 'stale');
|
45
|
+
}
|
46
|
+
}
|
47
|
+
|
48
|
+
// set the delegate only after the phrase data has been added to the DOM
|
49
|
+
// this handles the case when a user clicks a contextually-translatable string...
|
50
|
+
$(document).delegate('phrase', 'mousedown', function(event){
|
51
|
+
if(event.button === 2){
|
52
|
+
var $this = $(this);
|
53
|
+
$translationPanel.data('ref', $this);
|
54
|
+
|
55
|
+
// grab the values
|
56
|
+
$translationPanel.addClass('active');
|
57
|
+
|
58
|
+
// position the translation panel
|
59
|
+
var elPos = $this.offset();
|
60
|
+
$translationPanel.css('top', elPos.top + $this.outerHeight() + 5).css('left', elPos.left);
|
61
|
+
$translatedPhrase.focus();
|
62
|
+
|
63
|
+
if($this.hasClass('updated') || $this.hasClass('stale')){
|
64
|
+
var original = $this.data('phrase-original');
|
65
|
+
var latest = $this.data('phrase-latest');
|
66
|
+
var id = $this.data('phrase-id');
|
67
|
+
var key = $this.data('phrase-key');
|
68
|
+
|
69
|
+
simple_format_original = original.split("\n").join("<br />");
|
70
|
+
$originalPhrase.html(simple_format_original);
|
71
|
+
|
72
|
+
$translatedPhrase.html(latest);
|
73
|
+
$translatedPhrase.val(latest);
|
74
|
+
$phraseId.val(id);
|
75
|
+
$phraseKey.val(key);
|
76
|
+
$targetLocale.val($translationPanel.data('locale'));
|
77
|
+
}
|
78
|
+
else {
|
79
|
+
var id = $this.data('phrase-id');
|
80
|
+
$translationPanel.addClass('pending');
|
81
|
+
|
82
|
+
$.getJSON('/translate/phrase', {id: id, locale: $translationPanel.data('locale')}, function(phrase){
|
83
|
+
$this.data('phrase-original', phrase.original);
|
84
|
+
$this.data('phrase-latest', phrase.latest);
|
85
|
+
$this.data('phrase-key', phrase.key);
|
86
|
+
|
87
|
+
$this.addClass(phrase.latest ? 'updated' : 'stale');
|
88
|
+
|
89
|
+
$originalPhrase.html(phrase.original);
|
90
|
+
$translatedPhrase.html(phrase.latest);
|
91
|
+
$translatedPhrase.val(phrase.latest);
|
92
|
+
$phraseId.val(id);
|
93
|
+
$phraseKey.val(phrase.key);
|
94
|
+
$targetLocale.val($translationPanel.data('locale'));
|
95
|
+
$translationPanel.removeClass('pending');
|
96
|
+
});
|
97
|
+
}
|
98
|
+
|
99
|
+
$('html').bind('click', closePanel);
|
100
|
+
}
|
101
|
+
});
|
102
|
+
}
|
103
|
+
});
|
104
|
+
|
105
|
+
|
106
|
+
// handler for when the panel gets closed
|
107
|
+
var closePanel = function(){
|
108
|
+
$translationPanel.removeClass('active');
|
109
|
+
$('html').unbind('click', closePanel);
|
110
|
+
return false;
|
111
|
+
};
|
112
|
+
|
113
|
+
// disable the context menu for the contextual translations
|
114
|
+
$(document).delegate('phrase', 'contextmenu', function(){ return false; });
|
115
|
+
|
116
|
+
// stop propagation of all click events on the panel, so that
|
117
|
+
// if users click on it, it doesn't close
|
118
|
+
$translationPanel.click(function(event){
|
119
|
+
event.stopPropagation();
|
120
|
+
event.preventDefault();
|
121
|
+
});
|
122
|
+
|
123
|
+
var t = new SimpleTranslator({
|
124
|
+
success: function(phrase){
|
125
|
+
$translatedPhrase.html(phrase);
|
126
|
+
$translatedPhrase.val(phrase);
|
127
|
+
}
|
128
|
+
});
|
129
|
+
// handle "google translate" button click
|
130
|
+
$('#copy').click(function(){
|
131
|
+
var originalValue = $translationPanel.data('ref').data('phrase-original');
|
132
|
+
$translatedPhrase.html(originalValue);
|
133
|
+
$translatedPhrase.val(originalValue);
|
134
|
+
});
|
135
|
+
|
136
|
+
$('#submit-translation').click(function(){ $('#translation-form').submit(); });
|
137
|
+
|
138
|
+
$('#translation-form').submit(function(){
|
139
|
+
var $this = $(this);
|
140
|
+
|
141
|
+
var $error = $('#translation-error');
|
142
|
+
var $phraseRef = $translationPanel.data('ref');
|
143
|
+
|
144
|
+
$error.addClass('hidden');
|
145
|
+
|
146
|
+
$.getJSON($this.attr('action'),
|
147
|
+
$this.serialize(),
|
148
|
+
function(data){
|
149
|
+
if(data.status === 'success'){
|
150
|
+
$phraseRef.html(data.value);
|
151
|
+
$phraseRef.addClass('updated');
|
152
|
+
closePanel();
|
153
|
+
}
|
154
|
+
else if(data.status === 'error'){
|
155
|
+
$error.html(data.message).removeClass('hidden');
|
156
|
+
}
|
157
|
+
});
|
158
|
+
|
159
|
+
return false;
|
160
|
+
});
|
161
|
+
|
162
|
+
$('#panel-close').click(function(){
|
163
|
+
closePanel();
|
164
|
+
});
|
165
|
+
|
166
|
+
$('.translated_phrase')
|
167
|
+
.mouseover(function() {$('.translation_link_' + this.getAttribute('name')).show();})
|
168
|
+
.mouseout(function() {setTimeout("$('.translation_link_' +'" + this.getAttribute('name') + "').hide();", 2000);});
|
169
|
+
$('input[value*="i18n-translatable"]').val('text');
|
170
|
+
$('button[value*="i18n-translatable"]').val('text');
|
171
|
+
$('option .i18n-translatable').val('text');
|
172
|
+
}
|
173
|
+
};
|
174
|
+
})(jQuery);
|
@@ -0,0 +1,68 @@
|
|
1
|
+
(function($){
|
2
|
+
SimpleTranslator = function(options){
|
3
|
+
this.init(options);
|
4
|
+
};
|
5
|
+
|
6
|
+
$.extend(SimpleTranslator.prototype, {
|
7
|
+
options: {
|
8
|
+
success: false,
|
9
|
+
error: false,
|
10
|
+
|
11
|
+
// convert from:
|
12
|
+
// %{user_name}
|
13
|
+
// to
|
14
|
+
// [[zzuser_namezz]]
|
15
|
+
encode: {
|
16
|
+
regex: /%{([a-z0-9_]+)}/ig,
|
17
|
+
output: "[[zz$1zz]]"
|
18
|
+
},
|
19
|
+
|
20
|
+
// convert from:
|
21
|
+
// [[zzuser_namezz]]
|
22
|
+
// to:
|
23
|
+
// %{user_name}
|
24
|
+
decode: {
|
25
|
+
regex: /\[\[zz([a-z0-9_]+)zz\]\]/ig,
|
26
|
+
output: "%{$1}"
|
27
|
+
}
|
28
|
+
},
|
29
|
+
|
30
|
+
init: function(options){
|
31
|
+
this.options = $.extend({}, this.options, options);
|
32
|
+
},
|
33
|
+
|
34
|
+
translate: function(phrase, locale, callback){
|
35
|
+
var ref = this;
|
36
|
+
var encode = ref.options.encode;
|
37
|
+
var decode = ref.options.decode;
|
38
|
+
|
39
|
+
var encodedPhrase = phrase.replace(encode.regex, encode.output);
|
40
|
+
|
41
|
+
jQuery.getJSON(
|
42
|
+
'/air18n_ui/ajax_google_translate',
|
43
|
+
{ source_lang: "en",
|
44
|
+
target_lang: locale,
|
45
|
+
text: encodedPhrase,
|
46
|
+
purpose: "SimpleTranslator" },
|
47
|
+
function(result){
|
48
|
+
if (result && result.translated_text){
|
49
|
+
result = result.translated_text.replace(decode.regex, decode.output);
|
50
|
+
|
51
|
+
if(callback){
|
52
|
+
callback(result);
|
53
|
+
}
|
54
|
+
|
55
|
+
if(ref.options.success){
|
56
|
+
ref.options.success(result);
|
57
|
+
}
|
58
|
+
}
|
59
|
+
else {
|
60
|
+
if(ref.options.error){
|
61
|
+
ref.options.error(phrase);
|
62
|
+
}
|
63
|
+
}
|
64
|
+
}
|
65
|
+
);
|
66
|
+
}
|
67
|
+
});
|
68
|
+
})(jQuery);
|
@@ -0,0 +1,111 @@
|
|
1
|
+
@import "helpers/colors";
|
2
|
+
@import "helpers/mixins";
|
3
|
+
|
4
|
+
phrase {
|
5
|
+
display: inline !important;
|
6
|
+
-webkit-transition: border-bottom linear 0.2s, background linear 0.2s;
|
7
|
+
-moz-transition: border-bottom linear 0.2s, background linear 0.2s;
|
8
|
+
padding: 0 !important;
|
9
|
+
margin: 0 !important;
|
10
|
+
background-image: none !important;
|
11
|
+
float: none !important;
|
12
|
+
|
13
|
+
border-bottom: 3px solid rgba(180, 180, 180, 0.3);
|
14
|
+
background: rgba(180, 180, 180, 0.1);
|
15
|
+
|
16
|
+
&.stale {
|
17
|
+
border-bottom: 3px solid rgba(255, 0, 0, 0.3);
|
18
|
+
background: rgba(255, 0, 0, 0.1);
|
19
|
+
}
|
20
|
+
|
21
|
+
&.updated {
|
22
|
+
border-bottom: 3px solid rgba(100, 255, 50, 0.3);
|
23
|
+
background: rgba(100, 255, 50, 0.1);
|
24
|
+
}
|
25
|
+
}
|
26
|
+
|
27
|
+
div#contextual-translation-panel {
|
28
|
+
-webkit-transition: opacity linear 0.1s;
|
29
|
+
-moz-transition: opacity linear 0.1s;
|
30
|
+
|
31
|
+
&.active { opacity: 1; z-index: 99999; }
|
32
|
+
&.pending {
|
33
|
+
textarea { color: #fff; }
|
34
|
+
span#original-phrase { display: none; }
|
35
|
+
div.phrase-container { color: transparent; }
|
36
|
+
img.spinner { display: inline; }
|
37
|
+
}
|
38
|
+
|
39
|
+
img.spinner { display: none; }
|
40
|
+
|
41
|
+
opacity: 0;
|
42
|
+
top: 0px;
|
43
|
+
left: -9999px;
|
44
|
+
z-index: -9999;
|
45
|
+
position: absolute;
|
46
|
+
width: 400px;
|
47
|
+
background: #eee;
|
48
|
+
border: 5px solid #ddd;
|
49
|
+
@include border-radius(5px);
|
50
|
+
@include box-shadow(0 3px 2px -2px rgba(0, 0, 0, 0.2));
|
51
|
+
padding: 15px;
|
52
|
+
|
53
|
+
a#panel-close {
|
54
|
+
position: absolute;
|
55
|
+
top: 10px;
|
56
|
+
right: 10px;
|
57
|
+
font-size: 1.1em;
|
58
|
+
font-weight: bold;
|
59
|
+
}
|
60
|
+
|
61
|
+
h3 { font-size: 1.5em; font-weight: bold; margin-bottom: 5px; }
|
62
|
+
div.phrase-container {
|
63
|
+
font-size: 1.2em;
|
64
|
+
color: #ccc;
|
65
|
+
font-weight: bold;
|
66
|
+
margin-bottom: 10px;
|
67
|
+
|
68
|
+
span#original-phrase { font-weight: normal; color: #333; margin: 0 2px; }
|
69
|
+
}
|
70
|
+
|
71
|
+
div.error {
|
72
|
+
border: 1px solid #FAA;
|
73
|
+
padding: 5px;
|
74
|
+
margin-bottom: 10px;
|
75
|
+
background: #FCC;
|
76
|
+
@include border-radius(5px);
|
77
|
+
@include box-shadow(0 2px 2px -2px rgba(0, 0, 0, 0.2), inset 0 0 4px rgba(255, 255, 255, 0.6));
|
78
|
+
color: #300;
|
79
|
+
opacity: 1.0;
|
80
|
+
overflow: hidden;
|
81
|
+
|
82
|
+
-moz-transition: height linear 0.3s,
|
83
|
+
opacity linear 0.3s,
|
84
|
+
padding linear 0.3s,
|
85
|
+
margin-bottom linear 0.3s;
|
86
|
+
|
87
|
+
-webkit-transition: height linear 0.3s,
|
88
|
+
opacity linear 0.3s,
|
89
|
+
padding linear 0.3s,
|
90
|
+
margin-bottom linear 0.3s;
|
91
|
+
|
92
|
+
&.hidden {
|
93
|
+
height: 0;
|
94
|
+
opacity: 0.0;
|
95
|
+
margin-bottom: 0;
|
96
|
+
padding: 0;
|
97
|
+
border: 0;
|
98
|
+
}
|
99
|
+
}
|
100
|
+
|
101
|
+
div.phrase-fields {
|
102
|
+
margin-top: 5px;
|
103
|
+
font-size: 1.2em;
|
104
|
+
textarea { margin-bottom: 5px; display: block; width: 390px; height: 80px; }
|
105
|
+
button { font-size: 1.0em; }
|
106
|
+
}
|
107
|
+
|
108
|
+
div.phrase-actions { font-size: 0.9em;
|
109
|
+
button#gTranslate { float: right; }
|
110
|
+
}
|
111
|
+
}
|
@@ -0,0 +1,154 @@
|
|
1
|
+
class Air18nUiController < ApplicationController
|
2
|
+
before_filter :only_allow_translators
|
3
|
+
|
4
|
+
def only_allow_translators
|
5
|
+
unless @current_user && @current_user.has_any_role?([:admin, :translator])
|
6
|
+
redirect_to '/'
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def phrase
|
11
|
+
# error checking
|
12
|
+
if params[:mappings]
|
13
|
+
params[:id] = params[:mappings].keys
|
14
|
+
end
|
15
|
+
|
16
|
+
phrases = Air18n::Phrase.find_all_by_id(params[:id])
|
17
|
+
locale = params[:locale] || 'en'
|
18
|
+
render :json => {:status => 'error'} and return if phrases.empty?
|
19
|
+
|
20
|
+
json = []
|
21
|
+
|
22
|
+
phrases.each do |phrase|
|
23
|
+
latest = phrase.latest_translation(locale)
|
24
|
+
latest = latest.nil? ? "" : latest.value
|
25
|
+
|
26
|
+
json << {:status => 'success',
|
27
|
+
:id => phrase.id,
|
28
|
+
:key => phrase.key,
|
29
|
+
:locale => locale,
|
30
|
+
:original => phrase.value,
|
31
|
+
:elements => params[:mappings] ? params[:mappings][phrase.id.to_s] : [],
|
32
|
+
:latest => latest}
|
33
|
+
end
|
34
|
+
|
35
|
+
render :json => (json.length == 1 ? json.first : json)
|
36
|
+
end
|
37
|
+
|
38
|
+
# TODO(JKB) Move bulk of create_translation and normalize_translation_value
|
39
|
+
# to air18n.
|
40
|
+
# xhr
|
41
|
+
def create_translation
|
42
|
+
pt = Air18n::PhraseTranslation.new
|
43
|
+
pt.user = @current_user
|
44
|
+
|
45
|
+
pt.phrase_id = params[:phrase_id]
|
46
|
+
pt.key = params[:phrase_key]
|
47
|
+
pt.locale = params[:target_locale]
|
48
|
+
|
49
|
+
# Let parameters specify "value", "phrase[value]", or "air18n_phrase[value]"
|
50
|
+
# We prefer to take directly from request.request_parameters or
|
51
|
+
# request.query_parameters because values in the params hash are sanitized
|
52
|
+
# and HTML goes missing!!
|
53
|
+
value = request.request_parameters['value'] ||
|
54
|
+
request.query_parameters['value'] ||
|
55
|
+
params[:value] ||
|
56
|
+
(request.request_parameters['phrase'] && request.request_parameters['phrase']['value']) ||
|
57
|
+
(request.query_parameters['phrase'] && request.query_parameters['phrase']['value']) ||
|
58
|
+
(request.request_parameters['air18n_phrase'] && request.request_parameters['air18n_phrase']['value']) ||
|
59
|
+
(request.query_parameters['air18n_phrase'] && request.query_parameters['air18n_phrase']['value'])
|
60
|
+
|
61
|
+
latest = pt.phrase.latest_translation(pt.locale)
|
62
|
+
|
63
|
+
pt.value = normalize_translation_value(value)
|
64
|
+
normalized_latest_value = latest.present? && normalize_translation_value(latest.value)
|
65
|
+
|
66
|
+
value_same_as_latest = pt.value == normalized_latest_value
|
67
|
+
|
68
|
+
if latest && value_same_as_latest && latest.is_stale && @current_user.id == latest.user_id
|
69
|
+
latest.is_stale = false
|
70
|
+
if latest.save
|
71
|
+
response_obj = {
|
72
|
+
:status => 'success',
|
73
|
+
:message => 'Translation marked as up-to-date.',
|
74
|
+
:phrase_id => latest.phrase_id,
|
75
|
+
:key => latest.key,
|
76
|
+
:locale => latest.locale,
|
77
|
+
:value => latest.value,
|
78
|
+
:is_verification => latest.is_verification
|
79
|
+
}
|
80
|
+
else
|
81
|
+
response_obj = { :status => 'error', :message => latest.errors.values.join('; ') }
|
82
|
+
end
|
83
|
+
else
|
84
|
+
do_save = false
|
85
|
+
message = ""
|
86
|
+
|
87
|
+
safeness = Air18n::XssDetector.safe?(pt.phrase.value, pt.value, I18n.default_locale, pt.locale)
|
88
|
+
|
89
|
+
if pt.value.empty?
|
90
|
+
message = "Translation empty; nothing saved."
|
91
|
+
elsif !params[:disable_xss_check] && !safeness[:safe]
|
92
|
+
message = safeness[:reason]
|
93
|
+
elsif (latest && value_same_as_latest) && (latest.user_id != 0)
|
94
|
+
if !TungstenSupport::International.locale_in_qa_stage?(pt.locale)
|
95
|
+
locale_name = TungstenSupport::International.decode_locale(pt.locale)
|
96
|
+
allowed, error = false, "Verification of non-stale phrases is currently disabled in #{locale_name}"
|
97
|
+
else
|
98
|
+
allowed, error = pt.verification_allowed?(latest)
|
99
|
+
end
|
100
|
+
if allowed
|
101
|
+
pt.is_verification = true
|
102
|
+
do_save = true
|
103
|
+
message = "Translation verified."
|
104
|
+
else
|
105
|
+
message = error
|
106
|
+
end
|
107
|
+
else
|
108
|
+
do_save = true
|
109
|
+
message = "Translation saved."
|
110
|
+
end
|
111
|
+
|
112
|
+
message = ERB::Util.html_escape(message)
|
113
|
+
|
114
|
+
if do_save
|
115
|
+
if pt.save
|
116
|
+
response_obj = {
|
117
|
+
:status => 'success',
|
118
|
+
:message => message,
|
119
|
+
:phrase_id => pt.phrase_id,
|
120
|
+
:key => pt.key,
|
121
|
+
:locale => pt.locale,
|
122
|
+
:value => pt.value,
|
123
|
+
:is_verification => pt.is_verification
|
124
|
+
}
|
125
|
+
else
|
126
|
+
response_obj = {:status => 'error', :message => pt.errors.values.join('; ')}
|
127
|
+
end
|
128
|
+
else
|
129
|
+
response_obj = {:status => 'error', :message => message}
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
respond_to do |format|
|
134
|
+
format.html do
|
135
|
+
render :text => response_obj[:message]
|
136
|
+
end
|
137
|
+
|
138
|
+
format.json do
|
139
|
+
render :json => response_obj
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
private
|
145
|
+
|
146
|
+
def normalize_translation_value(value)
|
147
|
+
# Translators like to add extra whitespace, and extra whitespace sometimes
|
148
|
+
# messes up things!
|
149
|
+
# We also remove alt="" because ckeditor likes to add that for no reason,
|
150
|
+
# and it adds it in different order arbitrarily which makes translations
|
151
|
+
# change even when they don't really change.
|
152
|
+
value.strip.gsub(' alt=""', '')
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
<% if I18n.contextual_translation %>
|
2
|
+
<div id="contextual-translation-panel" data-locale="<%= I18n.full_locale %>">
|
3
|
+
<%= form_tag('/translate/create_translation', {:id => 'translation-form'}) do %>
|
4
|
+
<h3>Translate to <%= TungstenSupport::International.decode_locale(I18n.full_locale) %></h3>
|
5
|
+
<div class="phrase-container">“<span id="original-phrase"></span><%= image_tag 'spinner.gif', :class => 'spinner' %>”</div>
|
6
|
+
<div class="error hidden" id="translation-error">Error: Variables do not match</div>
|
7
|
+
<div class="phrase-fields">
|
8
|
+
<%= text_area_tag 'value', nil, :id => 'translated-phrase', :placeholder => "Enter your translation here" %>
|
9
|
+
<%= hidden_field_tag 'phrase_id' %>
|
10
|
+
<%= hidden_field_tag 'phrase_key' %>
|
11
|
+
<%= hidden_field_tag 'target_locale' %>
|
12
|
+
</div>
|
13
|
+
<div class="phrase-params">
|
14
|
+
</div>
|
15
|
+
<div class="phrase-actions">
|
16
|
+
<button id="submit-translation" type="submit" class="button-glossy blue">
|
17
|
+
Save Translation
|
18
|
+
</button>
|
19
|
+
<button id="panel-close" type="button" class="button-glossy grey">
|
20
|
+
Close
|
21
|
+
</button>
|
22
|
+
<button id="copy" type="button" class="button-glossy goog-blue">
|
23
|
+
Copy
|
24
|
+
</button>
|
25
|
+
</div>
|
26
|
+
<% end %>
|
27
|
+
</div>
|
28
|
+
|
29
|
+
<%= stylesheet_link_tag "contextual-translations" %>
|
30
|
+
|
31
|
+
<%= javascript_include_tag "simple_translator" %>
|
32
|
+
<%= javascript_include_tag "contextual-translations" %>
|
33
|
+
|
34
|
+
<script type="text/javascript">
|
35
|
+
jQuery(document).ready(function(){
|
36
|
+
ContextualTranslations.init();
|
37
|
+
});
|
38
|
+
</script>
|
39
|
+
<% end %>
|
data/config/routes.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
module Air18nUi
|
2
|
+
module ControllerFilters
|
3
|
+
def set_contextual_translation
|
4
|
+
if params[:set_contextual_translation_for_session]
|
5
|
+
session[:contextual_translation] = params[:set_contextual_translation_for_session] == "true"
|
6
|
+
end
|
7
|
+
I18n.contextual_translation = @current_user && @current_user.has_any_role?([:admin, :translator]) && session[:contextual_translation]
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
data/lib/air18n_ui.rb
ADDED
metadata
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: air18n_ui
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.4
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Jason Katz-Brown
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-12-11 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: i18n
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 0.5.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 0.5.0
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: air18n
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 0.1.21
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 0.1.21
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rails
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ~>
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '3.0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: rspec
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
description: Translation interface for air18n.
|
79
|
+
email:
|
80
|
+
- jason@airbnb.com
|
81
|
+
executables: []
|
82
|
+
extensions: []
|
83
|
+
extra_rdoc_files: []
|
84
|
+
files:
|
85
|
+
- Gemfile
|
86
|
+
- Rakefile
|
87
|
+
- air18n-ui.gemspec
|
88
|
+
- app/assets/javascripts/contextual-translations.js
|
89
|
+
- app/assets/javascripts/simple_translator.js
|
90
|
+
- app/assets/stylesheets/contextual-translations.scss
|
91
|
+
- app/controllers/air18n_ui_controller.rb
|
92
|
+
- app/views/shared/_contextual_translations.html.erb
|
93
|
+
- config/routes.rb
|
94
|
+
- lib/air18n_ui.rb
|
95
|
+
- lib/air18n_ui/controller_filters.rb
|
96
|
+
- lib/air18n_ui/engine.rb
|
97
|
+
- lib/air18n_ui/version.rb
|
98
|
+
homepage: http://www.github.com/airbnb/air18n-ui
|
99
|
+
licenses: []
|
100
|
+
post_install_message:
|
101
|
+
rdoc_options: []
|
102
|
+
require_paths:
|
103
|
+
- lib
|
104
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
segments:
|
111
|
+
- 0
|
112
|
+
hash: -736853819515982295
|
113
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
114
|
+
none: false
|
115
|
+
requirements:
|
116
|
+
- - ! '>='
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: '0'
|
119
|
+
segments:
|
120
|
+
- 0
|
121
|
+
hash: -736853819515982295
|
122
|
+
requirements: []
|
123
|
+
rubyforge_project:
|
124
|
+
rubygems_version: 1.8.24
|
125
|
+
signing_key:
|
126
|
+
specification_version: 3
|
127
|
+
summary: Translation interface for air18n
|
128
|
+
test_files: []
|