lolita-i18n 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +4 -0
- data/.rspec +1 -0
- data/Gemfile +24 -0
- data/Gemfile.lock +165 -0
- data/LICENSE.txt +20 -0
- data/README.md +27 -0
- data/Rakefile +46 -0
- data/VERSION +1 -0
- data/app/assets/images/lolita/i18n/google_translate_icon.png +0 -0
- data/app/assets/javascripts/lolita/i18n/application.js +2 -0
- data/app/assets/javascripts/lolita/i18n/i18n.js.coffee +135 -0
- data/app/assets/javascripts/lolita/i18n/spin.min.js +2 -0
- data/app/assets/stylesheets/lolita/i18n/application.scss +102 -0
- data/app/controllers/lolita/i18n_controller.rb +39 -0
- data/app/helpers/lolita/i18n_helper.rb +25 -0
- data/app/views/lolita/i18n/index.html.haml +34 -0
- data/config/locales/en.yml +5 -0
- data/config/locales/lv.yml +4 -0
- data/config/routes.rb +11 -0
- data/lib/lolita-i18n.rb +81 -0
- data/lib/lolita-i18n/backend.rb +69 -0
- data/lib/lolita-i18n/google_translate.rb +76 -0
- data/lib/lolita-i18n/module.rb +1 -0
- data/lib/lolita-i18n/rails.rb +6 -0
- data/lolita-i18n.gemspec +110 -0
- data/spec/controllers/lolita/i18n_controller_spec.rb +39 -0
- data/spec/lolita-i18n/backend_spec.rb +21 -0
- data/spec/lolita-i18n/google_translate_spec.rb +31 -0
- data/spec/rails_app/app/controllers/application_controller.rb +3 -0
- data/spec/rails_app/config/application.rb +18 -0
- data/spec/rails_app/config/enviroment.rb +5 -0
- data/spec/rails_app/config/initializers/lolita_i18n.rb +15 -0
- data/spec/rails_app/config/locales/en.yml +9 -0
- data/spec/rails_app/config/locales/lv.yml +2 -0
- data/spec/rails_app/config/routes.rb +3 -0
- data/spec/spec_helper.rb +33 -0
- metadata +197 -0
@@ -0,0 +1,102 @@
|
|
1
|
+
/*
|
2
|
+
*= require_self
|
3
|
+
*/
|
4
|
+
.clear { clear: both; height: 0; }
|
5
|
+
|
6
|
+
div.i18n-box div.list {
|
7
|
+
background-color: white;
|
8
|
+
overflow: none;
|
9
|
+
}
|
10
|
+
.i18n-language-switch{
|
11
|
+
overflow: hidden;
|
12
|
+
list-style: none;
|
13
|
+
}
|
14
|
+
.i18n-language-switch li{
|
15
|
+
float:left;
|
16
|
+
margin-left:10px;
|
17
|
+
cursor:pointer;
|
18
|
+
}
|
19
|
+
.i18n-language-switch li a {
|
20
|
+
color: #C3C3C3;
|
21
|
+
}
|
22
|
+
.i18n-language-switch li.active a{
|
23
|
+
color: #444;
|
24
|
+
font-weight: bold;
|
25
|
+
cursor:default;
|
26
|
+
}
|
27
|
+
span.hint {
|
28
|
+
font-size: 11px;
|
29
|
+
color: #666;
|
30
|
+
display: block;
|
31
|
+
}
|
32
|
+
#i18n_edit textarea {
|
33
|
+
height: 100px;
|
34
|
+
}
|
35
|
+
div.list div.actions {
|
36
|
+
padding: 10px;
|
37
|
+
.filters {
|
38
|
+
float: left;
|
39
|
+
position: relative;
|
40
|
+
line-height: 35px;
|
41
|
+
|
42
|
+
input {
|
43
|
+
position: absolute;
|
44
|
+
top: 11px;
|
45
|
+
}
|
46
|
+
label {
|
47
|
+
padding-top: 10px;
|
48
|
+
padding-left: 20px;
|
49
|
+
}
|
50
|
+
}
|
51
|
+
button.translate {
|
52
|
+
position: relative;
|
53
|
+
padding-left: 30px;
|
54
|
+
height: 34px;
|
55
|
+
background-image: url(image-path('lolita/i18n/google_translate_icon.png'));
|
56
|
+
background-position: 5px 5px;
|
57
|
+
background-repeat: no-repeat;
|
58
|
+
box-shadow: inset 0 0 1px 1px #eaeaea;
|
59
|
+
text-shadow: 0 1px 0 white;
|
60
|
+
border: 1px solid #bbb;
|
61
|
+
float: right;
|
62
|
+
}
|
63
|
+
button.loading {
|
64
|
+
background-image: none;
|
65
|
+
color: #999;
|
66
|
+
}
|
67
|
+
}
|
68
|
+
div.list table {
|
69
|
+
table-layout: fixed;
|
70
|
+
clear: both;
|
71
|
+
td span.unapproved {
|
72
|
+
color: red;
|
73
|
+
}
|
74
|
+
tr td:first-child p {
|
75
|
+
min-height: 40px;
|
76
|
+
}
|
77
|
+
td textarea {
|
78
|
+
border: 1px solid #cdcdcd;
|
79
|
+
-moz-border-radius: 3px;
|
80
|
+
-webkit-border-radius: 3px;
|
81
|
+
border-radius: 3px;
|
82
|
+
-moz-box-sizing: border-box;
|
83
|
+
-webkit-box-sizing: border-box;
|
84
|
+
box-sizing: border-box;
|
85
|
+
padding: 3px 10px;
|
86
|
+
width: 100%;
|
87
|
+
min-height: 50px;
|
88
|
+
color: #494949;
|
89
|
+
}
|
90
|
+
td, th {
|
91
|
+
white-space: normal;
|
92
|
+
padding: 5px 20px;
|
93
|
+
line-height: 20px;
|
94
|
+
}
|
95
|
+
thead th {
|
96
|
+
line-height: 30px;
|
97
|
+
select {
|
98
|
+
float: right;
|
99
|
+
margin-top: 5px;
|
100
|
+
}
|
101
|
+
}
|
102
|
+
}
|
@@ -0,0 +1,39 @@
|
|
1
|
+
class Lolita::I18nController < ApplicationController
|
2
|
+
include Lolita::Controllers::UserHelpers
|
3
|
+
before_filter :authenticate_lolita_user!, :set_current_locale
|
4
|
+
|
5
|
+
layout "lolita/application"
|
6
|
+
|
7
|
+
def index
|
8
|
+
@translation_keys=Lolita::I18n.flatten_keys
|
9
|
+
end
|
10
|
+
|
11
|
+
def update
|
12
|
+
respond_to do |format|
|
13
|
+
format.json do
|
14
|
+
render :nothing => true, :json => {error: !Lolita::I18n::Backend.set(params[:id],params[:translation])}
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def translate_untranslated
|
20
|
+
respond_to do |format|
|
21
|
+
format.json do
|
22
|
+
google_translate = Lolita::I18n::GoogleTranslate.new @active_locale
|
23
|
+
google_translate.run
|
24
|
+
render :nothing => true, :status => 200, :json => {errors: google_translate.errors, :translated => google_translate.untranslated}
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def next_locale
|
32
|
+
::I18n::available_locales.collect{|locale| locale if locale != ::I18n.default_locale}.compact.first
|
33
|
+
end
|
34
|
+
|
35
|
+
def set_current_locale
|
36
|
+
@active_locale = (params[:active_locale] || next_locale).to_sym
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Lolita
|
2
|
+
module I18nHelper
|
3
|
+
def locale_options
|
4
|
+
::I18n::available_locales.collect{|locale|
|
5
|
+
[::I18n.t(locale, :default => locale), locale] unless [::I18n.default_locale,@active_locale].include?(locale)
|
6
|
+
}.compact.insert(0,[::I18n.t("lolita-i18n.choose-other-language", :default => "Choose other language"),""])
|
7
|
+
end
|
8
|
+
|
9
|
+
def show_translation key
|
10
|
+
value = ::I18n.t(key, :locale => @active_locale, :default => '')
|
11
|
+
unless value.blank?
|
12
|
+
value
|
13
|
+
else
|
14
|
+
value = Lolita::I18n::GoogleTranslate.get_translation(@active_locale,key)
|
15
|
+
unless value.blank?
|
16
|
+
content_tag(:span, value, :class => "unapproved")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def is_untranslated key
|
22
|
+
Lolita::I18n::GoogleTranslate.get_translation(@active_locale,key)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
- content_for :style do
|
2
|
+
= stylesheet_link_tag "lolita/i18n/application", :media => "screen,projection"
|
3
|
+
- content_for :script do
|
4
|
+
= javascript_include_tag "lolita/i18n/application"
|
5
|
+
.box.i18n-bo
|
6
|
+
.boxtitle
|
7
|
+
%h1.black= ::I18n.t('lolita-i18n.title', :default => "Static content translation")
|
8
|
+
.arrow
|
9
|
+
.list
|
10
|
+
%div.actions
|
11
|
+
.filters
|
12
|
+
= check_box_tag "show_untranslated", nil, params[:show_untranslated]
|
13
|
+
= label_tag "show_untranslated", "Show translations that need approval"
|
14
|
+
|
15
|
+
%button.translate{:'data-url' => translate_untranslated_lolita_i18n_index_path, :'data-locale' => @active_locale} Translate empty translations with Google
|
16
|
+
.clear
|
17
|
+
%table
|
18
|
+
%thead
|
19
|
+
%tr
|
20
|
+
%th{:style => "width:450px"}
|
21
|
+
= label_tag :active_locale, I18n.t(@active_locale, :default => @active_locale)
|
22
|
+
- if locale_options.size > 1
|
23
|
+
= select_tag :active_locale, options_for_select(locale_options), :id => "active_locale"
|
24
|
+
%th{:style => "width:90%"}= ::I18n.t(::I18n.default_locale)
|
25
|
+
%tbody
|
26
|
+
- @translation_keys.each do |key|
|
27
|
+
- if !params[:show_untranslated] or is_untranslated(key)
|
28
|
+
%tr
|
29
|
+
%td{:style => "width:450px", :'data-key' => key, :'data-locale' => @active_locale}
|
30
|
+
%p= show_translation key
|
31
|
+
%td{:style => "width:90%", :'data-key' => key, :'data-locale' => ::I18n.default_locale}
|
32
|
+
%p= ::I18n.t(key, :locale => ::I18n.default_locale, :default => "")
|
33
|
+
%span.hint
|
34
|
+
= key
|
data/config/routes.rb
ADDED
data/lib/lolita-i18n.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
$:<<File.dirname(__FILE__) unless $:.include?(File.dirname(__FILE__))
|
2
|
+
require 'redis'
|
3
|
+
require 'yajl'
|
4
|
+
|
5
|
+
module Lolita
|
6
|
+
# === Uses Redis DB as backend
|
7
|
+
# All translations ar stored with full key like "en.home.index.title" -> Hello world.
|
8
|
+
# Translations whitch are translated with Google translate have prefix "g" -> "g.en.home.index.title".
|
9
|
+
# These translations should be accepted/edited and approved then they will become as normal for general use.
|
10
|
+
#
|
11
|
+
# === Other stored data
|
12
|
+
# => :unapproved_keys_<locale> - a SET containing all unapproved keys from GoogleTranslate
|
13
|
+
#
|
14
|
+
module I18n
|
15
|
+
autoload :Backend, 'lolita-i18n/backend'
|
16
|
+
autoload :GoogleTranslate, 'lolita-i18n/google_translate'
|
17
|
+
|
18
|
+
# Loads given key/value engine as backend
|
19
|
+
# place this method in rails initializer lolita.rb
|
20
|
+
# === Example
|
21
|
+
#
|
22
|
+
# I18n.backend = Lolita::I18n.load Redis.new
|
23
|
+
#
|
24
|
+
def self.load store
|
25
|
+
@@store=store
|
26
|
+
@@backend=::I18n::Backend::KeyValue.new(@@store)
|
27
|
+
@@yaml_backend=::I18n.backend
|
28
|
+
::I18n::Backend::Simple.send(:include, ::I18n::Backend::Flatten)
|
29
|
+
::I18n::Backend::Simple.send(:include, ::I18n::Backend::Memoize)
|
30
|
+
::I18n::Backend::Chain.new(Lolita::I18n.backend, @@yaml_backend)
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.store
|
34
|
+
@@store
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.backend
|
38
|
+
@@backend
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.yaml_backend
|
42
|
+
@@yaml_backend
|
43
|
+
end
|
44
|
+
|
45
|
+
# returns Array of flattened keys as "home.index.title"
|
46
|
+
def self.flatten_keys
|
47
|
+
@@yaml_backend.flatten_translations(nil, @@yaml_backend.send(:translations)[::I18n.default_locale] || {}, ::I18n::Backend::Flatten::SEPARATOR_ESCAPE_CHAR, false).keys
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
module LolitaI18nConfiguration
|
54
|
+
def i18n
|
55
|
+
Lolita::I18n
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
Lolita.scope.extend(LolitaI18nConfiguration)
|
60
|
+
|
61
|
+
require 'lolita-i18n/module'
|
62
|
+
if Lolita.rails3?
|
63
|
+
require 'lolita-i18n/rails'
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
Lolita.after_routes_loaded do
|
68
|
+
if tree=Lolita::Navigation::Tree[:"left_side_navigation"]
|
69
|
+
unless tree.branches.detect{|b| b.title=="System"}
|
70
|
+
branch=tree.append(nil,:title=>"System")
|
71
|
+
#mapping=Lolita::Mapping.new(:i18n_index,:singular=>:i18n,:class_name=>Object,:controller=>"lolita/i18n")
|
72
|
+
branch.append(Object,:title=>"I18n",:url=>Proc.new{|view,branch|
|
73
|
+
view.send(:lolita_i18n_index_path)
|
74
|
+
}, :active=>Proc.new{|view,parent_branch,branch|
|
75
|
+
params=view.send(:params)
|
76
|
+
params[:controller].to_s.match(/lolita\/i18n/)
|
77
|
+
})
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Lolita
|
2
|
+
module I18n
|
3
|
+
# Allow to operate with I18n keys and values.
|
4
|
+
class Backend
|
5
|
+
|
6
|
+
class << self
|
7
|
+
# Get translation and decode it. Set translate from and to translations.
|
8
|
+
# Return Hash with
|
9
|
+
# * <tt>:key</tt> - translation key
|
10
|
+
# * <tt>:value</tt> - translation
|
11
|
+
# * <tt>:translate_to</tt> - translation locale, like <i>en</i>
|
12
|
+
# * <tt>:translate_from</tt> - translation key for default language
|
13
|
+
def get(key)
|
14
|
+
result={:key=>key}
|
15
|
+
result[:value]=decoded_value(key)
|
16
|
+
result[:original_value]=decoded_value(translate_from(key))
|
17
|
+
result[:translate_to]=translate_to(key)
|
18
|
+
result[:translate_from]=translate_from(key)
|
19
|
+
result
|
20
|
+
end
|
21
|
+
|
22
|
+
# Store translation, decode and store.
|
23
|
+
# Accept:
|
24
|
+
# * <tt>key</tt> - translation key
|
25
|
+
# * <tt>translation</tt> - String with translation
|
26
|
+
def set(key,translation)
|
27
|
+
locale=translate_to(key)
|
28
|
+
translation_key=translation_key(key)
|
29
|
+
value=Yajl::Parser.parse(translation.to_json)
|
30
|
+
if Lolita::I18n.backend.store_translations(locale,{translation_key=>value},:escape=>false)
|
31
|
+
Lolita::I18n::GoogleTranslate.del_translation locale, translation_key
|
32
|
+
true
|
33
|
+
else
|
34
|
+
false
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def locale(key)
|
39
|
+
translate_to(key) || ::I18n.default_locale
|
40
|
+
end
|
41
|
+
|
42
|
+
def translation_key(key)
|
43
|
+
(key.to_s.split('.')[1..-1]).join(".")
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def keys
|
49
|
+
@keys||=Lolita::I18n.flattened_translations.keys.sort
|
50
|
+
end
|
51
|
+
|
52
|
+
def decoded_value(key)
|
53
|
+
value=Lolita::I18n.backend.store[key]
|
54
|
+
value ? (Yajl::Parser.parse(value) rescue "") : ::I18n.t(key.split('.')[1..-1].join('.'), :default => "", :locale => translate_to(key))
|
55
|
+
end
|
56
|
+
|
57
|
+
def translate_to(key)
|
58
|
+
key.to_s.split(".").first
|
59
|
+
end
|
60
|
+
|
61
|
+
def translate_from(key,locale=nil)
|
62
|
+
(key.to_s.split('.')[1..-1]).insert(0,locale || ::I18n.default_locale).join(".")
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'easy_translate'
|
2
|
+
|
3
|
+
module Lolita
|
4
|
+
module I18n
|
5
|
+
# Translates untranslated strings using google translate API
|
6
|
+
class GoogleTranslate
|
7
|
+
attr_reader :untranslated, :errors
|
8
|
+
def initialize locale
|
9
|
+
@locale = locale
|
10
|
+
@untranslated = 0
|
11
|
+
@errors = []
|
12
|
+
end
|
13
|
+
|
14
|
+
def run
|
15
|
+
unless untranslated_translations.empty?
|
16
|
+
@untranslated = untranslated_translations.count
|
17
|
+
untranslated_translations.each_slice(10).to_a.each do |batch|
|
18
|
+
begin
|
19
|
+
EasyTranslate.translate(batch.map(&:last), :to => @locale).each_with_index do |result,index|
|
20
|
+
unless result.blank?
|
21
|
+
key = batch[index].first
|
22
|
+
add_translation key, result
|
23
|
+
add_to_unapproved key
|
24
|
+
end
|
25
|
+
end
|
26
|
+
rescue EasyTranslate::EasyTranslateException => e
|
27
|
+
@errors << e.to_s
|
28
|
+
rescue Exception => e
|
29
|
+
puts "#{e.to_s}\n\n#{$@.join("\n")}"
|
30
|
+
@errors << e.to_s
|
31
|
+
break
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def untranslated_translations
|
38
|
+
unless @untranslated_translations
|
39
|
+
@untranslated_translations = []
|
40
|
+
Lolita::I18n.flatten_keys.each do |key|
|
41
|
+
unless unapproved_keys.include?(key)
|
42
|
+
if !::I18n.t(key, :locale => ::I18n.default_locale, :default => '').blank? && ::I18n.t(key, :locale => @locale, :default => '').blank? && Lolita::I18n::GoogleTranslate.get_translation(@locale,key).blank?
|
43
|
+
value = ::I18n.t(key, :locale => ::I18n.default_locale, :default => '').to_s
|
44
|
+
@untranslated_translations << [key,value] unless value.blank?
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
@untranslated_translations
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.get_translation locale, key
|
53
|
+
Lolita::I18n.store.get :"g.#{locale}.#{key}"
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.del_translation locale, key
|
57
|
+
Lolita::I18n.store.del :"g.#{locale}.#{key}"
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def unapproved_keys
|
63
|
+
@@unapproved_keys ||= Lolita::I18n.store.smembers(:"unapproved_keys_#{@locale}").map(&:to_sym)
|
64
|
+
end
|
65
|
+
|
66
|
+
def add_translation key, value
|
67
|
+
Lolita::I18n.store.set :"g.#{@locale}.#{key}", value
|
68
|
+
end
|
69
|
+
|
70
|
+
def add_to_unapproved key
|
71
|
+
Lolita::I18n.store.sadd(:"unapproved_keys_#{@locale}",key)
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|