air18n_ui 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|