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: []
         |