lit 1.0.2 → 1.1.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4c5dfa0728bf37d5d8ec30cf4e41f35fdbb3b983435dc7bfb13f32dfb6f7a158
4
- data.tar.gz: 30790191f771ca56d7e0fa4437a8dc24ef2cabb93ba001165ad1ad1f6d134b6e
3
+ metadata.gz: ecbd506c6be8279728e5da85a4857dd542c75cdc752efac9745dd7e2ba022a5c
4
+ data.tar.gz: 45bf91076203d8be22b2da854886657ddb85f07acbc42cd287b9246883e0955d
5
5
  SHA512:
6
- metadata.gz: d2681e97e1f16e71ecccb21fdd61075538396f02755d14241c157c2d9862ba3d507b0110cfe8dcb052bdd7a85198805b59c1ad42968aa68c1bb5352e132711fa
7
- data.tar.gz: e90e44494c18adcc7c6ccbb4fc9d3e6963cba5269daea7eb8f0c53619ba33eb086b09ce692fd7479171f44aa3c6396993bd5fe7cf948b76f8605526b4485ac95
6
+ metadata.gz: b78bb6b6ef52a2010d0725165a0e898d72dbf5fcf843b4ae756291f5648032e12ac1c6ded47a90418b258627c5ea1521e956dbd3e1387f428d623ec144a0c9cc
7
+ data.tar.gz: ce87f326e76dce205a987bbb1474a154406afd5fda42a9f3b92017cd8912f621a54ef36a16bd1f0861e6193341a0db24220341a8d1c5120cdfbbb3d2438473c5
data/README.md CHANGED
@@ -128,7 +128,7 @@ Lit::CloudTranslation.provider = Lit::CloudTranslation::Providers::Google
128
128
 
129
129
  ...and make sure you have this in your Gemfile:
130
130
  ```
131
- gem 'google-cloud-translate'
131
+ gem 'google-cloud-translate', '~> 1.2.4'
132
132
  ```
133
133
 
134
134
  To use translation via Google, you need to obtain a [service account key](https://cloud.google.com/iam/docs/creating-managing-service-account-keys) containing all the credentials required by the API.
@@ -145,6 +145,13 @@ These credentials can be given in three ways:
145
145
  ... # see Google docs link above for reference
146
146
  }
147
147
  end
148
+
149
+ # For example, for Rails 6, from encrypted credentials file (HashWithIndifferentAccess is used, because the keys in
150
+ the credentials.config could be strings or, as well, symbols):
151
+
152
+ Lit::CloudTranslation.configure do |config|
153
+ config.keyfile_hash = HashWithIndifferentAccess.new(Rails.application.credentials.config[:google_translate_api])
154
+ end
148
155
  ```
149
156
  * directly via `GOOGLE_TRANSLATE_API_<element>` environment variables, where e.g. the `GOOGLE_TRANSLATE_API_PROJECT_ID` variable corresponds to the `project_id` element of a JSON keyfile. Typically, only the following variables are mandatory:
150
157
  * `GOOGLE_TRANSLATE_API_PROJECT_ID`
@@ -273,7 +280,8 @@ Lit.store_request_info = true
273
280
  1. `gem install bundler && bundle install` - ensure Bundler and all required gems are installed
274
281
  2. `bundle exec appraisal install` - install gems from appraisal's gemfiles
275
282
  3. `cp test/dummy/config/database.yml.sample test/dummy/config/database.yml` - move a database.yml in place (remember to fill your DB credentials in it)
276
- 4. `RAILS_ENV=test appraisal rails-5.2 rake db:setup` - setup lit DB (see test/config/database.yml); do it only once, it does not matter which Rails version you use for `appraisal`
283
+ 4. `RAILS_ENV=test bundle exec appraisal rails-5.2 rake db:setup` - setup lit DB (see test/config/database.yml); do it
284
+ only once, it does not matter which Rails version you use for `appraisal`
277
285
  5. `bundle exec appraisal rake` - run the tests!
278
286
 
279
287
  ### License
@@ -0,0 +1,7 @@
1
+ /*!
2
+ * Bootstrap v3.0.3 (http://getbootstrap.com)
3
+ * Copyright 2013 Twitter, Inc.
4
+ * Licensed under http://www.apache.org/licenses/LICENSE-2.0
5
+ */
6
+
7
+ if("undefined"==typeof jQuery)throw new Error("Bootstrap requires jQuery");+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]}}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one(a.support.transition.end,function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b()})}(jQuery),+function(a){"use strict";var b='[data-dismiss="alert"]',c=function(c){a(c).on("click",b,this.close)};c.prototype.close=function(b){function c(){f.trigger("closed.bs.alert").remove()}var d=a(this),e=d.attr("data-target");e||(e=d.attr("href"),e=e&&e.replace(/.*(?=#[^\s]*$)/,""));var f=a(e);b&&b.preventDefault(),f.length||(f=d.hasClass("alert")?d:d.parent()),f.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(f.removeClass("in"),a.support.transition&&f.hasClass("fade")?f.one(a.support.transition.end,c).emulateTransitionEnd(150):c())};var d=a.fn.alert;a.fn.alert=function(b){return this.each(function(){var d=a(this),e=d.data("bs.alert");e||d.data("bs.alert",e=new c(this)),"string"==typeof b&&e[b].call(d)})},a.fn.alert.Constructor=c,a.fn.alert.noConflict=function(){return a.fn.alert=d,this},a(document).on("click.bs.alert.data-api",b,c.prototype.close)}(jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d)};b.DEFAULTS={loadingText:"loading..."},b.prototype.setState=function(a){var b="disabled",c=this.$element,d=c.is("input")?"val":"html",e=c.data();a+="Text",e.resetText||c.data("resetText",c[d]()),c[d](e[a]||this.options[a]),setTimeout(function(){"loadingText"==a?c.addClass(b).attr(b,b):c.removeClass(b).removeAttr(b)},0)},b.prototype.toggle=function(){var a=this.$element.closest('[data-toggle="buttons"]'),b=!0;if(a.length){var c=this.$element.find("input");"radio"===c.prop("type")&&(c.prop("checked")&&this.$element.hasClass("active")?b=!1:a.find(".active").removeClass("active")),b&&c.prop("checked",!this.$element.hasClass("active")).trigger("change")}b&&this.$element.toggleClass("active")};var c=a.fn.button;a.fn.button=function(c){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof c&&c;e||d.data("bs.button",e=new b(this,f)),"toggle"==c?e.toggle():c&&e.setState(c)})},a.fn.button.Constructor=b,a.fn.button.noConflict=function(){return a.fn.button=c,this},a(document).on("click.bs.button.data-api","[data-toggle^=button]",function(b){var c=a(b.target);c.hasClass("btn")||(c=c.closest(".btn")),c.button("toggle"),b.preventDefault()})}(jQuery),+function(a){"use strict";var b=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=this.sliding=this.interval=this.$active=this.$items=null,"hover"==this.options.pause&&this.$element.on("mouseenter",a.proxy(this.pause,this)).on("mouseleave",a.proxy(this.cycle,this))};b.DEFAULTS={interval:5e3,pause:"hover",wrap:!0},b.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},b.prototype.getActiveIndex=function(){return this.$active=this.$element.find(".item.active"),this.$items=this.$active.parent().children(),this.$items.index(this.$active)},b.prototype.to=function(b){var c=this,d=this.getActiveIndex();return b>this.$items.length-1||0>b?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){c.to(b)}):d==b?this.pause().cycle():this.slide(b>d?"next":"prev",a(this.$items[b]))},b.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition.end&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},b.prototype.next=function(){return this.sliding?void 0:this.slide("next")},b.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},b.prototype.slide=function(b,c){var d=this.$element.find(".item.active"),e=c||d[b](),f=this.interval,g="next"==b?"left":"right",h="next"==b?"first":"last",i=this;if(!e.length){if(!this.options.wrap)return;e=this.$element.find(".item")[h]()}this.sliding=!0,f&&this.pause();var j=a.Event("slide.bs.carousel",{relatedTarget:e[0],direction:g});if(!e.hasClass("active")){if(this.$indicators.length&&(this.$indicators.find(".active").removeClass("active"),this.$element.one("slid.bs.carousel",function(){var b=a(i.$indicators.children()[i.getActiveIndex()]);b&&b.addClass("active")})),a.support.transition&&this.$element.hasClass("slide")){if(this.$element.trigger(j),j.isDefaultPrevented())return;e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),d.one(a.support.transition.end,function(){e.removeClass([b,g].join(" ")).addClass("active"),d.removeClass(["active",g].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger("slid.bs.carousel")},0)}).emulateTransitionEnd(600)}else{if(this.$element.trigger(j),j.isDefaultPrevented())return;d.removeClass("active"),e.addClass("active"),this.sliding=!1,this.$element.trigger("slid.bs.carousel")}return f&&this.cycle(),this}};var c=a.fn.carousel;a.fn.carousel=function(c){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c),g="string"==typeof c?c:f.slide;e||d.data("bs.carousel",e=new b(this,f)),"number"==typeof c?e.to(c):g?e[g]():f.interval&&e.pause().cycle()})},a.fn.carousel.Constructor=b,a.fn.carousel.noConflict=function(){return a.fn.carousel=c,this},a(document).on("click.bs.carousel.data-api","[data-slide], [data-slide-to]",function(b){var c,d=a(this),e=a(d.attr("data-target")||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"")),f=a.extend({},e.data(),d.data()),g=d.attr("data-slide-to");g&&(f.interval=!1),e.carousel(f),(g=d.attr("data-slide-to"))&&e.data("bs.carousel").to(g),b.preventDefault()}),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var b=a(this);b.carousel(b.data())})})}(jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d),this.transitioning=null,this.options.parent&&(this.$parent=a(this.options.parent)),this.options.toggle&&this.toggle()};b.DEFAULTS={toggle:!0},b.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},b.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b=a.Event("show.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.$parent&&this.$parent.find("> .panel > .in");if(c&&c.length){var d=c.data("bs.collapse");if(d&&d.transitioning)return;c.collapse("hide"),d||c.data("bs.collapse",null)}var e=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[e](0),this.transitioning=1;var f=function(){this.$element.removeClass("collapsing").addClass("in")[e]("auto"),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return f.call(this);var g=a.camelCase(["scroll",e].join("-"));this.$element.one(a.support.transition.end,a.proxy(f,this)).emulateTransitionEnd(350)[e](this.$element[0][g])}}},b.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse").removeClass("in"),this.transitioning=1;var d=function(){this.transitioning=0,this.$element.trigger("hidden.bs.collapse").removeClass("collapsing").addClass("collapse")};return a.support.transition?(this.$element[c](0).one(a.support.transition.end,a.proxy(d,this)).emulateTransitionEnd(350),void 0):d.call(this)}}},b.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()};var c=a.fn.collapse;a.fn.collapse=function(c){return this.each(function(){var d=a(this),e=d.data("bs.collapse"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c);e||d.data("bs.collapse",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.collapse.Constructor=b,a.fn.collapse.noConflict=function(){return a.fn.collapse=c,this},a(document).on("click.bs.collapse.data-api","[data-toggle=collapse]",function(b){var c,d=a(this),e=d.attr("data-target")||b.preventDefault()||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,""),f=a(e),g=f.data("bs.collapse"),h=g?"toggle":d.data(),i=d.attr("data-parent"),j=i&&a(i);g&&g.transitioning||(j&&j.find('[data-toggle=collapse][data-parent="'+i+'"]').not(d).addClass("collapsed"),d[f.hasClass("in")?"addClass":"removeClass"]("collapsed")),f.collapse(h)})}(jQuery),+function(a){"use strict";function b(){a(d).remove(),a(e).each(function(b){var d=c(a(this));d.hasClass("open")&&(d.trigger(b=a.Event("hide.bs.dropdown")),b.isDefaultPrevented()||d.removeClass("open").trigger("hidden.bs.dropdown"))})}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}var d=".dropdown-backdrop",e="[data-toggle=dropdown]",f=function(b){a(b).on("click.bs.dropdown",this.toggle)};f.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){if("ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a('<div class="dropdown-backdrop"/>').insertAfter(a(this)).on("click",b),f.trigger(d=a.Event("show.bs.dropdown")),d.isDefaultPrevented())return;f.toggleClass("open").trigger("shown.bs.dropdown"),e.focus()}return!1}},f.prototype.keydown=function(b){if(/(38|40|27)/.test(b.keyCode)){var d=a(this);if(b.preventDefault(),b.stopPropagation(),!d.is(".disabled, :disabled")){var f=c(d),g=f.hasClass("open");if(!g||g&&27==b.keyCode)return 27==b.which&&f.find(e).focus(),d.click();var h=a("[role=menu] li:not(.divider):visible a",f);if(h.length){var i=h.index(h.filter(":focus"));38==b.keyCode&&i>0&&i--,40==b.keyCode&&i<h.length-1&&i++,~i||(i=0),h.eq(i).focus()}}}};var g=a.fn.dropdown;a.fn.dropdown=function(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new f(this)),"string"==typeof b&&d[b].call(c)})},a.fn.dropdown.Constructor=f,a.fn.dropdown.noConflict=function(){return a.fn.dropdown=g,this},a(document).on("click.bs.dropdown.data-api",b).on("click.bs.dropdown.data-api",".dropdown form",function(a){a.stopPropagation()}).on("click.bs.dropdown.data-api",e,f.prototype.toggle).on("keydown.bs.dropdown.data-api",e+", [role=menu]",f.prototype.keydown)}(jQuery),+function(a){"use strict";var b=function(b,c){this.options=c,this.$element=a(b),this.$backdrop=this.isShown=null,this.options.remote&&this.$element.load(this.options.remote)};b.DEFAULTS={backdrop:!0,keyboard:!0,show:!0},b.prototype.toggle=function(a){return this[this.isShown?"hide":"show"](a)},b.prototype.show=function(b){var c=this,d=a.Event("show.bs.modal",{relatedTarget:b});this.$element.trigger(d),this.isShown||d.isDefaultPrevented()||(this.isShown=!0,this.escape(),this.$element.on("click.dismiss.modal",'[data-dismiss="modal"]',a.proxy(this.hide,this)),this.backdrop(function(){var d=a.support.transition&&c.$element.hasClass("fade");c.$element.parent().length||c.$element.appendTo(document.body),c.$element.show(),d&&c.$element[0].offsetWidth,c.$element.addClass("in").attr("aria-hidden",!1),c.enforceFocus();var e=a.Event("shown.bs.modal",{relatedTarget:b});d?c.$element.find(".modal-dialog").one(a.support.transition.end,function(){c.$element.focus().trigger(e)}).emulateTransitionEnd(300):c.$element.focus().trigger(e)}))},b.prototype.hide=function(b){b&&b.preventDefault(),b=a.Event("hide.bs.modal"),this.$element.trigger(b),this.isShown&&!b.isDefaultPrevented()&&(this.isShown=!1,this.escape(),a(document).off("focusin.bs.modal"),this.$element.removeClass("in").attr("aria-hidden",!0).off("click.dismiss.modal"),a.support.transition&&this.$element.hasClass("fade")?this.$element.one(a.support.transition.end,a.proxy(this.hideModal,this)).emulateTransitionEnd(300):this.hideModal())},b.prototype.enforceFocus=function(){a(document).off("focusin.bs.modal").on("focusin.bs.modal",a.proxy(function(a){this.$element[0]===a.target||this.$element.has(a.target).length||this.$element.focus()},this))},b.prototype.escape=function(){this.isShown&&this.options.keyboard?this.$element.on("keyup.dismiss.bs.modal",a.proxy(function(a){27==a.which&&this.hide()},this)):this.isShown||this.$element.off("keyup.dismiss.bs.modal")},b.prototype.hideModal=function(){var a=this;this.$element.hide(),this.backdrop(function(){a.removeBackdrop(),a.$element.trigger("hidden.bs.modal")})},b.prototype.removeBackdrop=function(){this.$backdrop&&this.$backdrop.remove(),this.$backdrop=null},b.prototype.backdrop=function(b){var c=this.$element.hasClass("fade")?"fade":"";if(this.isShown&&this.options.backdrop){var d=a.support.transition&&c;if(this.$backdrop=a('<div class="modal-backdrop '+c+'" />').appendTo(document.body),this.$element.on("click.dismiss.modal",a.proxy(function(a){a.target===a.currentTarget&&("static"==this.options.backdrop?this.$element[0].focus.call(this.$element[0]):this.hide.call(this))},this)),d&&this.$backdrop[0].offsetWidth,this.$backdrop.addClass("in"),!b)return;d?this.$backdrop.one(a.support.transition.end,b).emulateTransitionEnd(150):b()}else!this.isShown&&this.$backdrop?(this.$backdrop.removeClass("in"),a.support.transition&&this.$element.hasClass("fade")?this.$backdrop.one(a.support.transition.end,b).emulateTransitionEnd(150):b()):b&&b()};var c=a.fn.modal;a.fn.modal=function(c,d){return this.each(function(){var e=a(this),f=e.data("bs.modal"),g=a.extend({},b.DEFAULTS,e.data(),"object"==typeof c&&c);f||e.data("bs.modal",f=new b(this,g)),"string"==typeof c?f[c](d):g.show&&f.show(d)})},a.fn.modal.Constructor=b,a.fn.modal.noConflict=function(){return a.fn.modal=c,this},a(document).on("click.bs.modal.data-api",'[data-toggle="modal"]',function(b){var c=a(this),d=c.attr("href"),e=a(c.attr("data-target")||d&&d.replace(/.*(?=#[^\s]+$)/,"")),f=e.data("modal")?"toggle":a.extend({remote:!/#/.test(d)&&d},e.data(),c.data());b.preventDefault(),e.modal(f,this).one("hide",function(){c.is(":visible")&&c.focus()})}),a(document).on("show.bs.modal",".modal",function(){a(document.body).addClass("modal-open")}).on("hidden.bs.modal",".modal",function(){a(document.body).removeClass("modal-open")})}(jQuery),+function(a){"use strict";var b=function(a,b){this.type=this.options=this.enabled=this.timeout=this.hoverState=this.$element=null,this.init("tooltip",a,b)};b.DEFAULTS={animation:!0,placement:"top",selector:!1,template:'<div class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',trigger:"hover focus",title:"",delay:0,html:!1,container:!1},b.prototype.init=function(b,c,d){this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d);for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focus",i="hover"==g?"mouseleave":"blur";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},b.prototype.getDefaults=function(){return b.DEFAULTS},b.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},b.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},b.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget)[this.type](this.getDelegateOptions()).data("bs."+this.type);return clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show),void 0):c.show()},b.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget)[this.type](this.getDelegateOptions()).data("bs."+this.type);return clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide),void 0):c.hide()},b.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){if(this.$element.trigger(b),b.isDefaultPrevented())return;var c=this.tip();this.setContent(),this.options.animation&&c.addClass("fade");var d="function"==typeof this.options.placement?this.options.placement.call(this,c[0],this.$element[0]):this.options.placement,e=/\s?auto?\s?/i,f=e.test(d);f&&(d=d.replace(e,"")||"top"),c.detach().css({top:0,left:0,display:"block"}).addClass(d),this.options.container?c.appendTo(this.options.container):c.insertAfter(this.$element);var g=this.getPosition(),h=c[0].offsetWidth,i=c[0].offsetHeight;if(f){var j=this.$element.parent(),k=d,l=document.documentElement.scrollTop||document.body.scrollTop,m="body"==this.options.container?window.innerWidth:j.outerWidth(),n="body"==this.options.container?window.innerHeight:j.outerHeight(),o="body"==this.options.container?0:j.offset().left;d="bottom"==d&&g.top+g.height+i-l>n?"top":"top"==d&&g.top-l-i<0?"bottom":"right"==d&&g.right+h>m?"left":"left"==d&&g.left-h<o?"right":d,c.removeClass(k).addClass(d)}var p=this.getCalculatedOffset(d,g,h,i);this.applyPlacement(p,d),this.$element.trigger("shown.bs."+this.type)}},b.prototype.applyPlacement=function(a,b){var c,d=this.tip(),e=d[0].offsetWidth,f=d[0].offsetHeight,g=parseInt(d.css("margin-top"),10),h=parseInt(d.css("margin-left"),10);isNaN(g)&&(g=0),isNaN(h)&&(h=0),a.top=a.top+g,a.left=a.left+h,d.offset(a).addClass("in");var i=d[0].offsetWidth,j=d[0].offsetHeight;if("top"==b&&j!=f&&(c=!0,a.top=a.top+f-j),/bottom|top/.test(b)){var k=0;a.left<0&&(k=-2*a.left,a.left=0,d.offset(a),i=d[0].offsetWidth,j=d[0].offsetHeight),this.replaceArrow(k-e+i,i,"left")}else this.replaceArrow(j-f,j,"top");c&&d.offset(a)},b.prototype.replaceArrow=function(a,b,c){this.arrow().css(c,a?50*(1-a/b)+"%":"")},b.prototype.setContent=function(){var a=this.tip(),b=this.getTitle();a.find(".tooltip-inner")[this.options.html?"html":"text"](b),a.removeClass("fade in top bottom left right")},b.prototype.hide=function(){function b(){"in"!=c.hoverState&&d.detach()}var c=this,d=this.tip(),e=a.Event("hide.bs."+this.type);return this.$element.trigger(e),e.isDefaultPrevented()?void 0:(d.removeClass("in"),a.support.transition&&this.$tip.hasClass("fade")?d.one(a.support.transition.end,b).emulateTransitionEnd(150):b(),this.$element.trigger("hidden.bs."+this.type),this)},b.prototype.fixTitle=function(){var a=this.$element;(a.attr("title")||"string"!=typeof a.attr("data-original-title"))&&a.attr("data-original-title",a.attr("title")||"").attr("title","")},b.prototype.hasContent=function(){return this.getTitle()},b.prototype.getPosition=function(){var b=this.$element[0];return a.extend({},"function"==typeof b.getBoundingClientRect?b.getBoundingClientRect():{width:b.offsetWidth,height:b.offsetHeight},this.$element.offset())},b.prototype.getCalculatedOffset=function(a,b,c,d){return"bottom"==a?{top:b.top+b.height,left:b.left+b.width/2-c/2}:"top"==a?{top:b.top-d,left:b.left+b.width/2-c/2}:"left"==a?{top:b.top+b.height/2-d/2,left:b.left-c}:{top:b.top+b.height/2-d/2,left:b.left+b.width}},b.prototype.getTitle=function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||("function"==typeof c.title?c.title.call(b[0]):c.title)},b.prototype.tip=function(){return this.$tip=this.$tip||a(this.options.template)},b.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},b.prototype.validate=function(){this.$element[0].parentNode||(this.hide(),this.$element=null,this.options=null)},b.prototype.enable=function(){this.enabled=!0},b.prototype.disable=function(){this.enabled=!1},b.prototype.toggleEnabled=function(){this.enabled=!this.enabled},b.prototype.toggle=function(b){var c=b?a(b.currentTarget)[this.type](this.getDelegateOptions()).data("bs."+this.type):this;c.tip().hasClass("in")?c.leave(c):c.enter(c)},b.prototype.destroy=function(){this.hide().$element.off("."+this.type).removeData("bs."+this.type)};var c=a.fn.tooltip;a.fn.tooltip=function(c){return this.each(function(){var d=a(this),e=d.data("bs.tooltip"),f="object"==typeof c&&c;e||d.data("bs.tooltip",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.tooltip.Constructor=b,a.fn.tooltip.noConflict=function(){return a.fn.tooltip=c,this}}(jQuery),+function(a){"use strict";var b=function(a,b){this.init("popover",a,b)};if(!a.fn.tooltip)throw new Error("Popover requires tooltip.js");b.DEFAULTS=a.extend({},a.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:'<div class="popover"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>'}),b.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),b.prototype.constructor=b,b.prototype.getDefaults=function(){return b.DEFAULTS},b.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content")[this.options.html?"html":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},b.prototype.hasContent=function(){return this.getTitle()||this.getContent()},b.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},b.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")},b.prototype.tip=function(){return this.$tip||(this.$tip=a(this.options.template)),this.$tip};var c=a.fn.popover;a.fn.popover=function(c){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof c&&c;e||d.data("bs.popover",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.popover.Constructor=b,a.fn.popover.noConflict=function(){return a.fn.popover=c,this}}(jQuery),+function(a){"use strict";function b(c,d){var e,f=a.proxy(this.process,this);this.$element=a(c).is("body")?a(window):a(c),this.$body=a("body"),this.$scrollElement=this.$element.on("scroll.bs.scroll-spy.data-api",f),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||(e=a(c).attr("href"))&&e.replace(/.*(?=#[^\s]+$)/,"")||"")+" .nav li > a",this.offsets=a([]),this.targets=a([]),this.activeTarget=null,this.refresh(),this.process()}b.DEFAULTS={offset:10},b.prototype.refresh=function(){var b=this.$element[0]==window?"offset":"position";this.offsets=a([]),this.targets=a([]);var c=this;this.$body.find(this.selector).map(function(){var d=a(this),e=d.data("target")||d.attr("href"),f=/^#\w/.test(e)&&a(e);return f&&f.length&&[[f[b]().top+(!a.isWindow(c.$scrollElement.get(0))&&c.$scrollElement.scrollTop()),e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){c.offsets.push(this[0]),c.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.$scrollElement[0].scrollHeight||this.$body[0].scrollHeight,d=c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(b>=d)return g!=(a=f.last()[0])&&this.activate(a);for(a=e.length;a--;)g!=f[a]&&b>=e[a]&&(!e[a+1]||b<=e[a+1])&&this.activate(f[a])},b.prototype.activate=function(b){this.activeTarget=b,a(this.selector).parents(".active").removeClass("active");var c=this.selector+'[data-target="'+b+'"],'+this.selector+'[href="'+b+'"]',d=a(c).parents("li").addClass("active");d.parent(".dropdown-menu").length&&(d=d.closest("li.dropdown").addClass("active")),d.trigger("activate.bs.scrollspy")};var c=a.fn.scrollspy;a.fn.scrollspy=function(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.scrollspy.Constructor=b,a.fn.scrollspy.noConflict=function(){return a.fn.scrollspy=c,this},a(window).on("load",function(){a('[data-spy="scroll"]').each(function(){var b=a(this);b.scrollspy(b.data())})})}(jQuery),+function(a){"use strict";var b=function(b){this.element=a(b)};b.prototype.show=function(){var b=this.element,c=b.closest("ul:not(.dropdown-menu)"),d=b.data("target");if(d||(d=b.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,"")),!b.parent("li").hasClass("active")){var e=c.find(".active:last a")[0],f=a.Event("show.bs.tab",{relatedTarget:e});if(b.trigger(f),!f.isDefaultPrevented()){var g=a(d);this.activate(b.parent("li"),c),this.activate(g,g.parent(),function(){b.trigger({type:"shown.bs.tab",relatedTarget:e})})}}},b.prototype.activate=function(b,c,d){function e(){f.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),b.addClass("active"),g?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu")&&b.closest("li.dropdown").addClass("active"),d&&d()}var f=c.find("> .active"),g=d&&a.support.transition&&f.hasClass("fade");g?f.one(a.support.transition.end,e).emulateTransitionEnd(150):e(),f.removeClass("in")};var c=a.fn.tab;a.fn.tab=function(c){return this.each(function(){var d=a(this),e=d.data("bs.tab");e||d.data("bs.tab",e=new b(this)),"string"==typeof c&&e[c]()})},a.fn.tab.Constructor=b,a.fn.tab.noConflict=function(){return a.fn.tab=c,this},a(document).on("click.bs.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(b){b.preventDefault(),a(this).tab("show")})}(jQuery),+function(a){"use strict";var b=function(c,d){this.options=a.extend({},b.DEFAULTS,d),this.$window=a(window).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(c),this.affixed=this.unpin=null,this.checkPosition()};b.RESET="affix affix-top affix-bottom",b.DEFAULTS={offset:0},b.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},b.prototype.checkPosition=function(){if(this.$element.is(":visible")){var c=a(document).height(),d=this.$window.scrollTop(),e=this.$element.offset(),f=this.options.offset,g=f.top,h=f.bottom;"object"!=typeof f&&(h=g=f),"function"==typeof g&&(g=f.top()),"function"==typeof h&&(h=f.bottom());var i=null!=this.unpin&&d+this.unpin<=e.top?!1:null!=h&&e.top+this.$element.height()>=c-h?"bottom":null!=g&&g>=d?"top":!1;this.affixed!==i&&(this.unpin&&this.$element.css("top",""),this.affixed=i,this.unpin="bottom"==i?e.top-d:null,this.$element.removeClass(b.RESET).addClass("affix"+(i?"-"+i:"")),"bottom"==i&&this.$element.offset({top:document.body.offsetHeight-h-this.$element.height()}))}};var c=a.fn.affix;a.fn.affix=function(c){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof c&&c;e||d.data("bs.affix",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.affix.Constructor=b,a.fn.affix.noConflict=function(){return a.fn.affix=c,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var b=a(this),c=b.data();c.offset=c.offset||{},c.offsetBottom&&(c.offset.bottom=c.offsetBottom),c.offsetTop&&(c.offset.top=c.offsetTop),b.affix(c)})})}(jQuery);
@@ -22,12 +22,17 @@ module Lit
22
22
  private
23
23
 
24
24
  def fetch_localizations
25
+ scope = Localization.includes(
26
+ :locale,
27
+ :localization_key
28
+ )
29
+
25
30
  if params[:after].present?
26
31
  after_date = Time.parse("#{params[:after]} #{Time.zone.name}")
27
32
  .in_time_zone
28
- Localization.after(after_date).to_a
33
+ scope.after(after_date).to_a
29
34
  else
30
- Localization.all
35
+ scope.all
31
36
  end
32
37
  end
33
38
  end
@@ -37,6 +37,10 @@ module Lit
37
37
  @scope.respond_to?(Kaminari.config.page_method_name)
38
38
  @scope = @scope.send(Kaminari.config.page_method_name, params[:page])
39
39
  end
40
+ if defined?(WillPaginate) &&
41
+ @scope.respond_to?(:paginate)
42
+ @scope = @scope.paginate(page: params[:page])
43
+ end
40
44
  get_localization_keys
41
45
  render action: :index
42
46
  end
@@ -88,6 +92,8 @@ module Lit
88
92
  end
89
93
  if defined?(Kaminari) and @scope.respond_to?(Kaminari.config.page_method_name)
90
94
  @localization_keys = @scope.send(Kaminari.config.page_method_name, params[:page])
95
+ elsif defined?(WillPaginate) and @scope.respond_to?(:paginate)
96
+ @localization_keys = @scope.paginate(page: params[:page])
91
97
  else
92
98
  @localization_keys = @scope
93
99
  end
@@ -20,7 +20,7 @@ module Lit
20
20
  # #after_update_operations. So it'll first set :is_changed to true
21
21
  # and then it will be properly read in the cache setting routine.
22
22
  @localization.transaction do
23
- after_update_operations if @localization.update_attributes(clear_params)
23
+ after_update_operations if @localization.update(clear_params)
24
24
  end
25
25
  respond_to do |f|
26
26
  f.js
@@ -40,7 +40,7 @@ module Lit
40
40
  end
41
41
 
42
42
  def update
43
- if @source.update_attributes(clear_params)
43
+ if @source.update(clear_params)
44
44
  redirect_to @source, notice: 'Source was successfully updated.'
45
45
  else
46
46
  render action: 'edit'
@@ -9,7 +9,7 @@ module Lit
9
9
  key = scope_key_by_partial(key)
10
10
  key = pluralized_key(key, count) if count
11
11
 
12
- content = super(key, options)
12
+ content = super(key, options.symbolize_keys)
13
13
  if !options[:skip_lit] && lit_authorized?
14
14
  content = get_translateable_span(key, content)
15
15
  end
@@ -9,6 +9,15 @@ module Lit
9
9
  escape_javascript val.to_s
10
10
  end
11
11
 
12
+ def locale_flag locale
13
+ locale = locale.to_s.upcase[0,2]
14
+ locale = case locale
15
+ when 'EN' then 'GB'
16
+ else locale
17
+ end
18
+ locale.tr('A-Z', "\u{1F1E6}-\u{1F1FF}")
19
+ end
20
+
12
21
  def allow_wysiwyg_editor?(key)
13
22
  Lit.all_translations_are_html_safe || key.to_s =~ /(\b|_|\.)html$/
14
23
  end
@@ -1,38 +1,54 @@
1
1
  class Lit::Base < ActiveRecord::Base
2
2
  self.abstract_class = true
3
3
 
4
- before_save :mark_for_retry_on_create, on: :create
5
- before_save :mark_for_retry_on_update, on: :update
4
+ before_save :mark_for_retry
6
5
 
7
6
  attr_accessor :retried_created, :retried_updated
8
7
 
9
- def mark_for_retry_on_create
8
+ def mark_for_retry
10
9
  @will_retry_create = true
11
10
  end
12
11
 
13
- def mark_for_retry_on_update
14
- @will_retry_update = true
12
+ after_rollback :retry_lit_model_save
13
+
14
+ def rolledback_after_insert?
15
+ persisted? && @was_saved_with_insert && @was_rolled_back
15
16
  end
16
17
 
17
- after_rollback :retry_lit_model_save
18
+ def rolledback_after_update?
19
+ persisted? && @was_saved_with_update && @was_rolled_back
20
+ end
18
21
 
19
22
  private
20
23
 
21
- def retry_lit_model_save
22
- retry_on_create if instance_variable_defined?(:@will_retry_create) && @will_retry_create
23
- retry_on_update if instance_variable_defined?(:@will_retry_update) && @will_retry_update
24
+ def create_or_update(*args, &block)
25
+ @was_saved_with_insert = true if new_record?
26
+ @was_saved_with_update = true if persisted?
27
+
28
+ super
24
29
  end
25
30
 
26
- def retry_on_create
27
- return if self.retried_created
28
- self.retried_created = true
29
- self.class.create! attributes.merge(retried_created: true)
31
+ def retry_lit_model_save
32
+ return if @was_rolled_back
33
+ @was_rolled_back = true
34
+ do_retry if instance_variable_defined?(:@will_retry_create) && @will_retry_create
30
35
  end
31
36
 
32
- def retry_on_update
33
- return if self.retried_updated
34
- self.retried_updated = true
35
- update! attributes.merge(retried_updated: true)
37
+ def do_retry
38
+ if !retried_created
39
+ self.retried_created = true
40
+ if rolledback_after_insert?
41
+ self.class.create! attributes.merge(retried_created: true)
42
+ end
43
+ elsif !retried_updated
44
+ # Why elsif and not just if? Because if a record object was first saved
45
+ # with INSERT and then with UPDATE (still being the same Ruby object),
46
+ # it makes no sense to retry both create and upadte. Let's only retry create.
47
+ self.retried_updated = true
48
+ if rolledback_after_update?
49
+ update_columns(attributes)
50
+ end
51
+ end
36
52
  end
37
53
 
38
54
  end
@@ -59,14 +59,14 @@ module Lit
59
59
  end
60
60
 
61
61
  def update_existing_localization_data
62
- localization.update_attributes!(
62
+ localization.update!(
63
63
  translated_value: translated_value,
64
64
  is_changed: true
65
65
  )
66
66
  end
67
67
 
68
68
  def update_existing_localization_key_data
69
- localization_key.update_attributes!(
69
+ localization_key.update!(
70
70
  is_deleted: localization_key_is_deleted
71
71
  )
72
72
  end
@@ -11,6 +11,7 @@ class RemoteInteractorService
11
11
  rescue => e
12
12
  return unless defined?(Rails)
13
13
  ::Rails.logger.error { "Lit remote error: #{e}" }
14
+ nil # Return nil to be consistent with checks for .nil? otherwise returns True
14
15
  end
15
16
 
16
17
  private
@@ -10,7 +10,6 @@
10
10
  <%= stylesheet_link_tag '//netdna.bootstrapcdn.com/bootswatch/3.0.3/yeti/bootstrap.min.css', media: 'all' %>
11
11
  <%= stylesheet_link_tag 'lit/application', media: 'all' %>
12
12
  <%= javascript_include_tag 'lit/application' %>
13
- <%= javascript_include_tag '//netdna.bootstrapcdn.com/bootstrap/3.0.3/js/bootstrap.min.js' %>
14
13
  <%= csrf_meta_tags %>
15
14
  </head>
16
15
  <body class="<%= params[:controller] %>">
@@ -4,6 +4,8 @@
4
4
 
5
5
  <% if defined?(Kaminari) %>
6
6
  <%= paginate @localization_keys, :theme=>"lit" %>
7
+ <% elsif defined?(WillPaginate) %>
8
+ <%= will_paginate @localization_keys %>
7
9
  <% end %>
8
10
 
9
11
  <%= render 'sidebar' %>
@@ -4,6 +4,8 @@
4
4
 
5
5
  <% if defined?(Kaminari) %>
6
6
  <%= paginate @localization_keys, :theme=>"lit" %>
7
+ <% elsif defined?(WillPaginate) %>
8
+ <%= will_paginate @localization_keys %>
7
9
  <% end %>
8
10
 
9
11
  <%= render 'sidebar' %>
@@ -4,6 +4,9 @@
4
4
 
5
5
  <% if defined?(Kaminari) %>
6
6
  <%= paginate @localization_keys, :theme=>"lit" %>
7
+ <% elsif defined?(WillPaginate) %>
8
+ <%= will_paginate @localization_keys %>
7
9
  <% end %>
8
10
 
11
+
9
12
  <%= render 'sidebar' %>
@@ -29,11 +29,14 @@ Lit.all_translations_are_html_safe = false
29
29
  # <span title="translation missing string"></span>
30
30
  Lit.humanize_key = false
31
31
 
32
- # If set to `true` will always parse yaml files upon startup and update cache
33
- # values with ones with yaml - but only, if those keys haven't been changed via
34
- # web ui before
32
+ # If set to `false` will always parse yaml files upon startup and update cached
33
+ # values with ones with yaml (but only, if those keys haven't been changed via
34
+ # web ui before)
35
35
  Lit.ignore_yaml_on_startup = true
36
36
 
37
+ # Array of ignored keys or key prefixes
38
+ Lit.ignored_keys = ['i18n.transliterate'].freeze
39
+
37
40
  # API enabled? API allows third party browsing your translations, as well as
38
41
  # synchronizing them between environments
39
42
  Lit.api_enabled = false
data/lib/lit.rb CHANGED
@@ -17,6 +17,8 @@ module Lit
17
17
  mattr_accessor :all_translations_are_html_safe
18
18
  mattr_accessor :set_last_updated_at_upon_creation
19
19
  mattr_accessor :store_request_info
20
+ mattr_accessor :store_request_keys
21
+ mattr_accessor :hits_counter_enabled
20
22
 
21
23
  class << self
22
24
  attr_accessor :loader
@@ -30,13 +32,16 @@ module Lit
30
32
  Lit.humanize_key_ignored_keys = [] if Lit.humanize_key_ignored_keys.nil?
31
33
  Lit.humanize_key_ignored = %w[i18n date datetime number time support ]
32
34
  Lit.humanize_key_ignored |= Lit.humanize_key_ignored_keys
33
- Lit.humanize_key_ignored = %r{(#{Lit.humanize_key_ignored.join('|')}).*}
34
- if Lit.ignored_keys.is_a?(String)
35
- keys = Lit.ignored_keys.split(',').map(&:strip)
36
- Lit.ignored_keys = keys
37
- end
35
+ Lit.humanize_key_ignored = Regexp.new("(#{Lit.humanize_key_ignored.join('|')}).*")
38
36
  Lit.ignore_yaml_on_startup = true if Lit.ignore_yaml_on_startup.nil?
37
+
38
+ Lit.ignored_keys = Lit.ignored_keys.split(',').map(&:strip) if Lit.ignored_keys.is_a?(String)
39
39
  Lit.ignored_keys = [] unless Lit.ignored_keys.is_a?(Array)
40
+ Lit.ignored_keys = Lit.ignored_keys.map(&:freeze).freeze
41
+
42
+ Lit.hits_counter_enabled = false if Lit.hits_counter_enabled.nil?
43
+ Lit.store_request_info = false if Lit.store_request_info.nil?
44
+ Lit.store_request_keys = false if Lit.store_request_keys.nil?
40
45
  # if loading all translations on start, migrations have to be already
41
46
  # performed, fails on first deploy
42
47
  # self.loader.cache.load_all_translations
@@ -47,7 +52,15 @@ module Lit
47
52
 
48
53
  def self.check_if_table_exists
49
54
  Lit::Locale.table_exists?
50
- rescue
55
+ rescue ActiveRecord::ActiveRecordError => e
56
+ log_txt =
57
+ "An #{e.class} error has been raised during Lit initialization. " \
58
+ "Lit assumes that database tables do not exist.\n\n" \
59
+ "Error: #{e.message}\n\n" \
60
+ "Backtrace:\n" \
61
+ "#{e.backtrace.join("\n")}"
62
+ Logger.new(STDOUT).error(log_txt) if ::Rails.env.test? # ensure this is logged to stdout in test
63
+ ::Rails.logger.error(log_txt)
51
64
  false
52
65
  end
53
66
 
@@ -2,11 +2,45 @@ module Lit
2
2
  class HashStorage < Hash
3
3
  def incr(key)
4
4
  self[key] ||= 0
5
- self[key] += 1
5
+ if self[key].is_a?(Integer)
6
+ self[key] += 1
7
+ else
8
+ subtree_keys(key).each { |k| self[k] += 1 }
9
+ end
6
10
  end
7
11
 
8
12
  def prefix
9
13
  nil
10
14
  end
15
+
16
+ def [](key)
17
+ super || subtree_of_key(key)
18
+ end
19
+
20
+ private
21
+
22
+ def subtree_of_key(key)
23
+ keys_of_subtree = subtree_keys(key)
24
+ return nil if keys_of_subtree.empty?
25
+
26
+ cache_localizations = form_cache_localizations(keys_of_subtree)
27
+
28
+ full_subtree = Lit::LocalizationKeysToHashService.call(cache_localizations)
29
+ requested_part = full_subtree.dig(*key.split('.'))
30
+ return nil if requested_part.blank?
31
+ return requested_part if requested_part.is_a?(String)
32
+
33
+ requested_part.deep_transform_keys(&:to_sym)
34
+ end
35
+
36
+ def subtree_keys(key)
37
+ keys.select { |k| k.match?(/\A#{key}*/) }
38
+ end
39
+
40
+ def form_cache_localizations(keys_of_subtree)
41
+ self_copy = self.select { |k, _| k.in?(keys_of_subtree) }
42
+ values_of_subtree = keys_of_subtree.map { |k| self_copy[k] }
43
+ Hash[keys_of_subtree.zip(values_of_subtree)]
44
+ end
11
45
  end
12
46
  end
@@ -1,4 +1,6 @@
1
1
  require 'redis'
2
+ require 'lit/services/localization_keys_to_hash_service'
3
+
2
4
  module Lit
3
5
  extend self
4
6
  def redis
@@ -16,13 +18,24 @@ module Lit
16
18
  Lit.redis
17
19
  end
18
20
 
21
+ # This handles a change in the redis-rb gem that changes exists => exists?
22
+ def exists?(key)
23
+ # Use recommended binary-returning method create [with this redis-rb commit](https://github.com/redis/redis-rb/commit/bf42fc9e0db4a1719d9b1ecc65aeb20425d44427).
24
+ return Lit.redis.exists?(key) if Lit.redis.respond_to?(:exists?)
25
+ # Fall back with older gem
26
+ Lit.redis.exists(key)
27
+ end
28
+
19
29
  def [](key)
20
- if Lit.redis.exists(_prefixed_key_for_array(key))
30
+ if self.exists?(_prefixed_key_for_array(key))
21
31
  Lit.redis.lrange(_prefixed_key(key), 0, -1)
22
- elsif Lit.redis.exists(_prefixed_key_for_nil(key))
32
+ elsif self.exists?(_prefixed_key_for_nil(key))
23
33
  nil
24
34
  else
25
- Lit.redis.get(_prefixed_key(key))
35
+ val = Lit.redis.get(_prefixed_key(key))
36
+ return val if val.present?
37
+
38
+ subtree_of_key(key)
26
39
  end
27
40
  end
28
41
 
@@ -56,7 +69,7 @@ module Lit
56
69
  end
57
70
 
58
71
  def has_key?(key)
59
- Lit.redis.exists(_prefixed_key(key))
72
+ self.exists?(_prefixed_key(key))
60
73
  end
61
74
  alias key? has_key?
62
75
 
@@ -77,11 +90,13 @@ module Lit
77
90
  private
78
91
 
79
92
  def _prefix
80
- prefix = 'lit:'
93
+ return @prefix_cached if @prefix_cached.present?
94
+
95
+ @prefix_cached = 'lit:'
81
96
  if Lit.storage_options.is_a?(Hash) && Lit.storage_options.key?(:prefix)
82
- prefix += "#{Lit.storage_options[:prefix]}:"
97
+ @prefix_cached += "#{Lit.storage_options[:prefix]}:"
83
98
  end
84
- prefix
99
+ @prefix_cached
85
100
  end
86
101
 
87
102
  def _prefixed_key(key = '')
@@ -95,5 +110,24 @@ module Lit
95
110
  def _prefixed_key_for_nil(key = '')
96
111
  _prefix + 'nil_flags:' + key.to_s
97
112
  end
113
+
114
+ def subtree_of_key(key)
115
+ keys_of_subtree = Lit.redis.keys("#{_prefixed_key(key)}*")
116
+ return nil if keys_of_subtree.empty?
117
+
118
+ values_of_subtree = Lit.redis.mget(keys_of_subtree)
119
+ cache_localizations = form_cache_localizations(keys_of_subtree, values_of_subtree)
120
+
121
+ full_subtree = Lit::LocalizationKeysToHashService.call(cache_localizations)
122
+ requested_part = full_subtree.dig(*key.split('.'))
123
+ return nil if requested_part.blank?
124
+ return requested_part if requested_part.is_a?(String)
125
+
126
+ requested_part.deep_transform_keys(&:to_sym)
127
+ end
128
+
129
+ def form_cache_localizations(keys, values)
130
+ Hash[keys.map { |k| k.sub(_prefix, '') }.zip(values)]
131
+ end
98
132
  end
99
133
  end
@@ -21,19 +21,32 @@ module Lit
21
21
  def initialize
22
22
  @hits_counter = Lit.get_key_value_engine
23
23
  @request_info_store = Lit.get_key_value_engine
24
- @hits_counter_working = true
24
+ @hits_counter_working = Lit.hits_counter_enabled
25
25
  @keys = nil
26
+
27
+ # current instance cache
26
28
  @localization_object_cache = {}
27
29
  @localization_key_object_cache = {}
30
+ clear_localization_cache
28
31
  end
29
32
 
30
33
  def [](key)
31
- key_without_locale = split_key(key).last
32
- update_hits_count(key)
33
- store_request_info(key_without_locale)
34
- localization = localizations[key]
35
- update_request_keys(key_without_locale, localization)
36
- localization
34
+ value = nil
35
+ unless localization_cache.key?(key)
36
+ value = localizations[key]
37
+ localization_cache[key] = value
38
+ else
39
+ value = localization_cache[key]
40
+ end
41
+ update_hits_count(key) if @hits_counter_working
42
+
43
+ if Lit.store_request_info ||
44
+ Lit.store_request_keys
45
+ key_without_locale = split_key(key).last
46
+ store_request_info(key_without_locale)
47
+ update_request_keys(key_without_locale, value)
48
+ end
49
+ value
37
50
  end
38
51
 
39
52
  def []=(key, value)
@@ -45,7 +58,8 @@ module Lit
45
58
  end
46
59
 
47
60
  def has_key?(key)
48
- localizations.has_key?(key)
61
+ # check for instance cache first
62
+ localization_cache.has_key?(key) || localizations.has_key?(key)
49
63
  end
50
64
 
51
65
  def sync
@@ -68,11 +82,13 @@ module Lit
68
82
  localization = find_localization(locale, key_without_locale, value: value, force_array: force_array, update_value: true)
69
83
  return localization.translation if startup_process && localization.is_changed?
70
84
  localizations[key] = localization.translation if localization
85
+ localization_cache[key] = localizations[key]
71
86
  end
72
87
 
73
88
  def update_cache(key, value)
74
89
  key = key.to_s
75
90
  localizations[key] = value
91
+ localization_cache[key] = value
76
92
  end
77
93
 
78
94
  def delete_locale(key)
@@ -83,6 +99,7 @@ module Lit
83
99
  delete_localization(locale, key_without_locale)
84
100
  @localization_key_object_cache = {}
85
101
  @localization_object_cache = {}
102
+ clear_localization_cache
86
103
  end
87
104
 
88
105
  def load_all_translations
@@ -119,6 +136,8 @@ module Lit
119
136
  @locale_cache = {}
120
137
  @localization_key_object_cache = {}
121
138
  @localization_object_cache = {}
139
+ @localization_cache = {}
140
+ clear_localization_cache
122
141
  end
123
142
 
124
143
  def reset
@@ -132,7 +151,7 @@ module Lit
132
151
  def find_locale(locale_key)
133
152
  locale_key = locale_key.to_s
134
153
  @locale_cache ||= {}
135
- unless @locale_cache.key?(locale_key)
154
+ unless @locale_cache.key?(locale_key) && @locale_cache[locale_key].id
136
155
  locale = Lit::Locale.where(locale: locale_key).first_or_create!
137
156
  @locale_cache[locale_key] = locale
138
157
  end
@@ -155,6 +174,14 @@ module Lit
155
174
  @hits_counter_working = true
156
175
  end
157
176
 
177
+ def localization_cache
178
+ Thread.current[:lit_thread_cache] ||= {}
179
+ end
180
+
181
+ def clear_localization_cache
182
+ Thread.current[:lit_thread_cache] = {}
183
+ end
184
+
158
185
  private
159
186
 
160
187
  def localizations
@@ -309,6 +336,7 @@ module Lit
309
336
 
310
337
  def update_hits_count(key)
311
338
  return unless @hits_counter_working
339
+
312
340
  key_without_locale = split_key(key).last
313
341
  @hits_counter.incr('hits_counter.' + key)
314
342
  @hits_counter.incr('global_hits_counter.' + key_without_locale)
@@ -317,6 +345,7 @@ module Lit
317
345
  def store_request_info(key_without_locale)
318
346
  return unless Lit.store_request_info
319
347
  return unless Thread.current[:lit_current_request_path].present?
348
+
320
349
  info = get_request_info(key_without_locale)
321
350
  parts = info.split(' ').push(Thread.current[:lit_current_request_path]).uniq
322
351
  parts.shift if parts.count > 10
@@ -324,7 +353,9 @@ module Lit
324
353
  end
325
354
 
326
355
  def update_request_keys(key_without_locale, localization)
356
+ return unless Lit.store_request_keys
327
357
  return if Thread.current[:lit_request_keys].nil?
358
+
328
359
  Thread.current[:lit_request_keys] ||= {}
329
360
  Thread.current[:lit_request_keys][key_without_locale] = localization
330
361
  end
@@ -9,12 +9,6 @@ module Lit
9
9
  initializer 'lit.assets.precompile' do |app|
10
10
  app.config.assets.precompile += %w[lit/application.css lit/application.js]
11
11
  app.config.assets.precompile += %w[lit/lit_frontend.css lit/lit_frontend.js]
12
- # add language flags to list of precompiled assets
13
- if app.config.i18n.available_locales
14
- app.config.i18n.available_locales.each do |l|
15
- app.config.assets.precompile << "lit/famfamfam_flags/#{l.to_s[0,2]}.png"
16
- end
17
- end
18
12
  end
19
13
 
20
14
  initializer 'lit.reloader' do |app|
@@ -30,5 +24,11 @@ module Lit
30
24
  end
31
25
  end
32
26
  end
27
+
28
+ initializer :append_before_action do
29
+ ActionController::Base.send :before_action do
30
+ Thread.current[:lit_thread_cache] = {}
31
+ end
32
+ end
33
33
  end
34
34
  end
@@ -1,4 +1,5 @@
1
1
  require 'csv'
2
+ require 'lit/services/localization_keys_to_hash_service'
2
3
 
3
4
  module Lit
4
5
  class Export
@@ -17,7 +18,7 @@ module Lit
17
18
 
18
19
  case format
19
20
  when :yaml
20
- exported_keys = nested_string_keys_to_hash(db_localizations)
21
+ exported_keys = Lit::LocalizationKeysToHashService.call(db_localizations)
21
22
  exported_keys.to_yaml
22
23
  when :csv
23
24
  relevant_locales = locale_keys.presence || I18n.available_locales.map(&:to_s)
@@ -55,23 +56,6 @@ module Lit
55
56
  end
56
57
  end
57
58
 
58
- private_class_method def self.nested_string_keys_to_hash(db_localizations)
59
- # http://subtech.g.hatena.ne.jp/cho45/20061122
60
- deep_proc = proc do |_k, s, o|
61
- if s.is_a?(Hash) && o.is_a?(Hash)
62
- next s.merge(o, &deep_proc)
63
- end
64
- next o
65
- end
66
- nested_keys = {}
67
- db_localizations.sort.each do |k, v|
68
- key_parts = k.to_s.split('.')
69
- converted = key_parts.reverse.reduce(v) { |a, n| { n => a } }
70
- nested_keys.merge!(converted, &deep_proc)
71
- end
72
- nested_keys
73
- end
74
-
75
59
  # This is like Array#transpose but ignores size differences between inner arrays.
76
60
  private_class_method def self.transpose(matrix)
77
61
  maxlen = matrix.max { |x| x.length }.length
@@ -69,11 +69,20 @@ module Lit
69
69
 
70
70
  parts = I18n.normalize_keys(locale, key, scope, options[:separator])
71
71
  key_with_locale = parts.join('.')
72
+
73
+ # we might want to return content later, but we first need to check if it's in cache.
74
+ # it's important to rememver, that accessing non-existen key modifies cache by creating one
75
+ had_key = @cache.has_key?(key_with_locale)
76
+
72
77
  # check in cache or in simple backend
73
78
  content = @cache[key_with_locale] || super
79
+
80
+ # return if content is in cache - it CAN be `nil`
81
+ return content if had_key && !options[:default]
82
+
74
83
  return content if parts.size <= 1
75
84
 
76
- if content.nil? && should_cache?(key_with_locale, options)
85
+ if content.nil? && should_cache?(key_with_locale, options, had_key)
77
86
  new_content = @cache.init_key_with_value(key_with_locale, content)
78
87
  content = new_content if content.nil? # Content can change when Lit.humanize is true for example
79
88
  # so there is no content in cache - it might not be if ie. we're doing
@@ -107,7 +116,7 @@ module Lit
107
116
  # it anyway if we return nil, but then it will wrap it also in
108
117
  # translation_missing span.
109
118
  # Humanizing key should be last resort
110
- if content.nil? && Lit.humanize_key && key.match(Lit.humanize_key_ignored).nil?
119
+ if content.nil? && Lit.humanize_key && Lit.humanize_key_ignored.match(key).nil?
111
120
  content = key.to_s.split('.').last.humanize
112
121
  if content.present?
113
122
  @cache[key_with_locale] = content
@@ -130,7 +139,7 @@ module Lit
130
139
  # end
131
140
  elsif data.respond_to?(:to_str) || data.is_a?(Array)
132
141
  key = ([locale] + scope).join('.')
133
- return if startup_process && @cache.keys.member?(key) && Lit.ignore_yaml_on_startup
142
+ return if startup_process && Lit.ignore_yaml_on_startup && (Thread.current[:lit_cache_keys] || @cache.keys).member?(key)
134
143
  @cache.update_locale(key, data, data.is_a?(Array), startup_process)
135
144
  elsif data.nil?
136
145
  return if startup_process
@@ -140,11 +149,13 @@ module Lit
140
149
  end
141
150
 
142
151
  def load_translations_to_cache
152
+ Thread.current[:lit_cache_keys] = @cache.keys
143
153
  ActiveRecord::Base.transaction do
144
154
  (@translations || {}).each do |locale, data|
145
155
  store_item(locale, data, [], true) if valid_locale?(locale)
146
156
  end
147
157
  end
158
+ Thread.current[:lit_cache_keys] = nil
148
159
  end
149
160
 
150
161
  def init_translations
@@ -156,7 +167,7 @@ module Lit
156
167
  # load translations from database to cache
157
168
  @cache.load_all_translations
158
169
  # load translations from @translations to cache
159
- load_translations_to_cache
170
+ load_translations_to_cache unless Lit.ignore_yaml_on_startup
160
171
  @initialized = true
161
172
  end
162
173
 
@@ -180,8 +191,9 @@ module Lit
180
191
  Lit.ignored_keys.any?{ |k| key_without_locale.start_with?(k) }
181
192
  end
182
193
 
183
- def should_cache?(key_with_locale, options)
184
- if @cache.has_key?(key_with_locale)
194
+ # checks if should cache. `had_key` is passed, as once cache has been accesed, it's already modified and key exists
195
+ def should_cache?(key_with_locale, options, had_key)
196
+ if had_key
185
197
  return false unless options[:default] && !options[:default].is_a?(Array)
186
198
  end
187
199
 
@@ -136,17 +136,20 @@ module Lit
136
136
  # is the array
137
137
  val = value.is_a?(Array) ? [value] : value
138
138
  I18n.t(key, default: val)
139
- unless @raw
140
- # this indicates that this translation already exists
141
- existing_translation =
142
- Lit::Localization.joins(:locale, :localization_key)
143
- .find_by('localization_key = ? and locale = ?',
144
- key, locale)
145
- if existing_translation
146
- existing_translation.update(translated_value: value, is_changed: true)
147
- lkey = existing_translation.localization_key
148
- lkey.update(is_deleted: false) if lkey.is_deleted
149
- end
139
+
140
+ # this indicates that this translation already exists
141
+ existing_translation =
142
+ Lit::Localization.joins(:locale, :localization_key)
143
+ .find_by('localization_key = ? and locale = ?', key, locale)
144
+
145
+ return unless existing_translation
146
+
147
+ if @raw
148
+ existing_translation.update(default_value: value)
149
+ else
150
+ existing_translation.update(translated_value: value, is_changed: true)
151
+ lkey = existing_translation.localization_key
152
+ lkey.update(is_deleted: false) if lkey.is_deleted
150
153
  end
151
154
  end
152
155
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lit
4
+ # Converts flat hash with localization_keys as keys and translations as values to nested hash
5
+ # by nesting on '.' localization key dots
6
+ class LocalizationKeysToHashService
7
+ # http://subtech.g.hatena.ne.jp/cho45/20061122
8
+ def self.call(db_localizations)
9
+ deep_proc = proc do |_k, s, o|
10
+ next s.merge(o, &deep_proc) if s.is_a?(Hash) && o.is_a?(Hash)
11
+
12
+ next o
13
+ end
14
+ nested_keys = {}
15
+ db_localizations.sort.each do |k, v|
16
+ key_parts = k.to_s.split('.')
17
+ converted = key_parts.reverse.reduce(v) { |a, n| { n => a } }
18
+ nested_keys.merge!(converted, &deep_proc)
19
+ end
20
+ nested_keys
21
+ end
22
+ end
23
+ end
@@ -1,3 +1,3 @@
1
1
  module Lit
2
- VERSION = '1.0.2'.freeze
2
+ VERSION = '1.1.0'.freeze
3
3
  end
metadata CHANGED
@@ -1,17 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lit
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Maciej Litwiniuk
8
8
  - Piotr Boniecki
9
9
  - Michał Buszkiewicz
10
10
  - Szymon Soppa
11
- autorequire:
11
+ autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2019-06-27 00:00:00.000000000 Z
14
+ date: 2020-11-03 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: rails
@@ -103,14 +103,14 @@ dependencies:
103
103
  requirements:
104
104
  - - "~>"
105
105
  - !ruby/object:Gem::Version
106
- version: 4.5.0
106
+ version: 4.7.1
107
107
  type: :development
108
108
  prerelease: false
109
109
  version_requirements: !ruby/object:Gem::Requirement
110
110
  requirements:
111
111
  - - "~>"
112
112
  - !ruby/object:Gem::Version
113
- version: 4.5.0
113
+ version: 4.7.1
114
114
  - !ruby/object:Gem::Dependency
115
115
  name: google-cloud-translate
116
116
  requirement: !ruby/object:Gem::Requirement
@@ -159,14 +159,14 @@ dependencies:
159
159
  requirements:
160
160
  - - "~>"
161
161
  - !ruby/object:Gem::Version
162
- version: 3.6.0
162
+ version: 3.9.0
163
163
  type: :development
164
164
  prerelease: false
165
165
  version_requirements: !ruby/object:Gem::Requirement
166
166
  requirements:
167
167
  - - "~>"
168
168
  - !ruby/object:Gem::Version
169
- version: 3.6.0
169
+ version: 3.9.0
170
170
  - !ruby/object:Gem::Dependency
171
171
  name: vcr
172
172
  requirement: !ruby/object:Gem::Requirement
@@ -210,6 +210,7 @@ files:
210
210
  - app/assets/images/lit/jquery-te.png
211
211
  - app/assets/javascripts/lit/application.js
212
212
  - app/assets/javascripts/lit/backend/bootstrap.js.coffee
213
+ - app/assets/javascripts/lit/backend/bootstrap.min.js
213
214
  - app/assets/javascripts/lit/backend/dashboard.js
214
215
  - app/assets/javascripts/lit/backend/jquery-te-1.4.0.min.js
215
216
  - app/assets/javascripts/lit/backend/localizations.js.coffee
@@ -320,13 +321,14 @@ files:
320
321
  - lib/lit/loader.rb
321
322
  - lib/lit/rails.rb
322
323
  - lib/lit/railtie.rb
324
+ - lib/lit/services/localization_keys_to_hash_service.rb
323
325
  - lib/lit/version.rb
324
326
  - lib/tasks/lit_tasks.rake
325
327
  homepage: https://github.com/prograils/lit
326
328
  licenses:
327
329
  - MIT
328
330
  metadata: {}
329
- post_install_message:
331
+ post_install_message:
330
332
  rdoc_options: []
331
333
  require_paths:
332
334
  - lib
@@ -341,8 +343,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
341
343
  - !ruby/object:Gem::Version
342
344
  version: '0'
343
345
  requirements: []
344
- rubygems_version: 3.0.3
345
- signing_key:
346
+ rubygems_version: 3.1.2
347
+ signing_key:
346
348
  specification_version: 4
347
349
  summary: Database powered i18n backend with web gui
348
350
  test_files: []