air18n_ui 0.0.4

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