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 ADDED
@@ -0,0 +1,2 @@
1
+ source "http://rubygems.org"
2
+ gemspec
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env rake
2
+ require 'bundler/gem_tasks'
3
+
4
+ require 'rspec/core/rake_task'
5
+ RSpec::Core::RakeTask.new do |t|
6
+ t.pattern = "spec/**/*_spec.rb"
7
+ end
8
+
9
+ Bundler::GemHelper.install_tasks
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">&ldquo;<span id="original-phrase"></span><%= image_tag 'spinner.gif', :class => 'spinner' %>&rdquo;</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,7 @@
1
+ Rails.application.routes.draw do
2
+ match "air18n_ui(/:action)" => "air18n_ui", :as => :air18n_ui
3
+
4
+ # Add this to your routes file to get contextual translation to work in
5
+ # development.
6
+ # match "translate(/:action)" => "air18n_ui"
7
+ end
@@ -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
@@ -0,0 +1,6 @@
1
+ require 'rails'
2
+
3
+ module Air18nUi
4
+ class Engine < Rails::Engine
5
+ end
6
+ end
@@ -0,0 +1,3 @@
1
+ module Air18nUi
2
+ VERSION = "0.0.4"
3
+ end
data/lib/air18n_ui.rb ADDED
@@ -0,0 +1,5 @@
1
+ require "air18n_ui/version"
2
+ require "air18n_ui/controller_filters"
3
+
4
+ # Our engine
5
+ require "air18n_ui/engine"
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: []