Skip to content

Commit ac450ff

Browse files
Merge branch 'cloud_translation'
2 parents 3b8f6d5 + 5b25c8d commit ac450ff

File tree

32 files changed

+1771
-1
lines changed

32 files changed

+1771
-1
lines changed

README.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,79 @@ Keys marked as deleted (i.e. still existing but deleted from the Lit UI) are *no
107107

108108
Deleted keys whose translations are encountered during import are restored automatically.
109109

110+
### Cloud translation services
111+
112+
Lit can use external translation services such as [Google Cloud Translation API](https://cloud.google.com/translate/) and [Yandex.Translate API](https://translate.yandex.com/developers) to tentatively translate localizations to a given language.
113+
Currently, Google and Yandex translation providers are supported, but extending it to any other translation provider of your choice is as easy as subclassing `Lit::CloudTranslation::Providers::Base`; see classes in `lib/lit/cloud_translation/providers` for reference.
114+
115+
#### Usage
116+
117+
Configure your translation provider using one of routines described below. When a translation provider is configured, each localization in Lit web UI will have a "Translate using _Provider Name_" button next to it, which by default translates to the localization's language from the localization currently saved for the app's `I18n.default_locale`.
118+
Next to the button, there is a dropdown that allows translating from the key's localization in a language different than the default one.
119+
120+
#### Google Cloud Translation API
121+
122+
Insert this into your Lit initializer:
123+
```
124+
require 'lit/cloud_translation/providers/google'
125+
126+
Lit::CloudTranslation.provider = Lit::CloudTranslation::Providers::Google
127+
```
128+
129+
...and make sure you have this in your Gemfile:
130+
```
131+
gem 'google-cloud-translate'
132+
```
133+
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.
135+
136+
These credentials can be given in three ways:
137+
* via a `.json` keyfile, the path to which should be stored in the `GOOGLE_TRANSLATE_API_KEYFILE` environment variable,
138+
* programmatically, in the initializer - be sure to use secrets in all the sensitive fields so you don't expose private credentials in the code:
139+
```
140+
Lit::CloudTranslation.configure do |config|
141+
config.keyfile_hash = {
142+
'type' => 'service_account',
143+
'project_id' => 'foo',
144+
'private_key_id' => 'keyid',
145+
... # see Google docs link above for reference
146+
}
147+
end
148+
```
149+
* 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+
* `GOOGLE_TRANSLATE_API_PROJECT_ID`
151+
* `GOOGLE_TRANSLATE_API_PRIVATE_KEY` (make sure that it contains correct line breaks and markers of the private key's begin and end)
152+
* `GOOGLE_TRANSLATE_API_CLIENT_EMAIL`
153+
154+
#### Yandex.Translate API
155+
156+
Insert this into your Lit initializer:
157+
```
158+
require 'lit/cloud_translation/providers/yandex'
159+
160+
Lit::CloudTranslation.provider = Lit::CloudTranslation::Providers::Yandex
161+
```
162+
163+
To use Yandex translation, an [API key must be obtained](https://translate.yandex.com/developers/keys). Then, you can pass it to your application via the `YANDEX_TRANSLATE_API_KEY` environment variable.
164+
165+
The API key can also be set programmatically in your Lit initializer (again, be sure to use secrets if you choose to do so):
166+
```
167+
Lit::CloudTranslation.configure do |config|
168+
config.api_key = 'the_api_key'
169+
end
170+
```
171+
172+
### 0.3 -> 1.0 upgrade guide
173+
174+
Also applies to upgrading from `0.4.pre.alpha` versions.
175+
176+
1. Specify `gem 'lit', '~> 1.0'` in your Gemfile and run `bundle update lit`.
177+
2. Run Lit migrations - `rails db:migrate`.
178+
* __Caution:__ One of the new migrations adds a unique index in `lit_localizations` on `(localization_key_id, locale_id)`, which may cause constraint violations in some cases. If you encounter such errors during running this migration - in this case you'll need to enter Rails console and remove duplicates manually. The following query might be helpful to determine duplicate locale/localization key ID pairs:
179+
```
180+
Lit::Localization.group(:locale_id, :localization_key_id).having('count(*) > 1').count
181+
```
182+
110183
### 0.2 -> 0.3 upgrade guide
111184

112185
1. Specify exact lit version in your Gemfile: `gem 'lit', '~> 0.3.0'`
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# frozen_string_literal: true
2+
3+
module Lit
4+
class CloudTranslationsController < ::Lit::ApplicationController
5+
def show
6+
params.delete(:from) if params[:from] == 'auto'
7+
@target_localization = Localization.find(params[:localization_id])
8+
@localization_key = @target_localization.localization_key
9+
if params[:from]
10+
@localization = @localization_key.localizations.joins(:locale)
11+
.find_by!(lit_locales: { locale: params[:from] })
12+
end
13+
opts =
14+
{
15+
# if :from was auto, translate from the target localization's
16+
# current text itself
17+
text: (@localization || @target_localization).value,
18+
from: params[:from],
19+
to: @target_localization.locale.locale
20+
}.compact
21+
@translated_text = Lit::CloudTranslation.translate(opts)
22+
rescue Lit::CloudTranslation::TranslationError => e
23+
@error_message = "Translation failed. #{e.message}"
24+
end
25+
end
26+
end

app/controllers/lit/localizations_controller.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ def find_localization_key
4848
end
4949

5050
def find_localization
51-
@localization = @localization_key.localizations.find(params[:id])
51+
@localization =
52+
@localization_key.localizations.includes(:locale).find(params[:id])
5253
end
5354

5455
def after_update_operations

app/helpers/lit/localizations_helper.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,9 @@ def ejs(val)
1212
def allow_wysiwyg_editor?(key)
1313
Lit.all_translations_are_html_safe || key.to_s =~ /(\b|_|\.)html$/
1414
end
15+
16+
def available_locales_with_default_first
17+
I18n.available_locales.sort_by { |l| l == I18n.default_locale ? 0 : 1 }
18+
end
1519
end
1620
end
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<% if @translated_text.is_a?(String) %>
2+
$('.localization_row[data-id="<%= @target_localization.id %>"] textarea').val("<%= j @translated_text %>");
3+
<% elsif @translated_text.is_a?(Array) %>
4+
$inputs = $('.localization_row[data-id="<%= @target_localization.id %>"] input[type=text]');
5+
var translated_elements = JSON.parse("<%= j @translated_text.to_json.html_safe %>");
6+
for (i in $inputs.toArray()) {
7+
$input = $($inputs[i]);
8+
$input.val(translated_elements[i]);
9+
}
10+
<% elsif @error_message.present? %>
11+
alert("<%= j @error_message %>")
12+
<% else %>
13+
alert("An unknown error has occurred while translating.")
14+
<% end %>

app/views/lit/localizations/_form.html.erb

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,32 @@
1919

2020
<button class="btn btn-primary btn-sm" type="submit"><%= I18n.t('lit.common.update', default: "Update") %></button>
2121
<button class="btn btn-default btn-sm cancel" type="reset"><%= I18n.t('lit.common.cancel', default: 'Cancel') %></button>
22+
23+
<% if Lit::CloudTranslation.provider %>
24+
<div class="btn-group pull-right">
25+
<%= link_to "Translate using #{Lit::CloudTranslation.provider.name.demodulize}", cloud_translation_path(from: I18n.default_locale, localization_id: @localization.id), remote: true, class: 'btn btn-sm btn-default' %>
26+
<button class="btn btn-sm dropdown-toggle" data-toggle="dropdown">
27+
<span class="caret"></span>
28+
<span class="sr-only">Source languages</span>
29+
</button>
30+
<ul class="dropdown-menu">
31+
<li>
32+
<a>Translate from:</a>
33+
</li>
34+
<li role="separator" class="divider"></li>
35+
<% (available_locales_with_default_first + [:auto]).each_with_index do |source_locale, i| %>
36+
<li>
37+
<%= link_to cloud_translation_path(from: source_locale, localization_id: @localization.id), remote: true do %>
38+
<% if i == 0 %>
39+
<strong><%= source_locale.to_s %></strong>
40+
<% else %>
41+
<%= source_locale.to_s %>
42+
<% end %>
43+
<% end %>
44+
</li>
45+
<% end %>
46+
</ul>
47+
</div>
48+
<% end %>
2249
<% end %>
2350

config/routes.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,5 +50,7 @@
5050
end
5151
end
5252

53+
resource :cloud_translation, only: :show
54+
5355
root to: 'dashboard#index'
5456
end

gemfiles/rails_4.2.gemfile.lock

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ GEM
5353
arel (6.0.4)
5454
bcrypt (3.1.12)
5555
builder (3.2.3)
56+
byebug (10.0.2)
5657
capybara (3.12.0)
5758
addressable
5859
mini_mime (>= 0.1.3)
@@ -82,35 +83,66 @@ GEM
8283
warden (~> 1.2.3)
8384
erubis (2.7.0)
8485
execjs (2.7.0)
86+
faraday (0.15.4)
87+
multipart-post (>= 1.2, < 3)
8588
ffi (1.9.25)
8689
globalid (0.4.1)
8790
activesupport (>= 4.2.0)
91+
google-cloud-core (1.2.7)
92+
google-cloud-env (~> 1.0)
93+
google-cloud-env (1.0.5)
94+
faraday (~> 0.11)
95+
google-cloud-translate (1.2.4)
96+
faraday (~> 0.13)
97+
google-cloud-core (~> 1.2)
98+
googleauth (~> 0.6.2)
99+
googleauth (0.6.7)
100+
faraday (~> 0.12)
101+
jwt (>= 1.4, < 3.0)
102+
memoist (~> 0.16)
103+
multi_json (~> 1.11)
104+
os (>= 0.9, < 2.0)
105+
signet (~> 0.7)
88106
hashdiff (0.3.7)
89107
i18n (0.9.5)
90108
concurrent-ruby (~> 1.0)
91109
jquery-rails (4.3.3)
92110
rails-dom-testing (>= 1, < 3)
93111
railties (>= 4.2.0)
94112
thor (>= 0.14, < 2.0)
113+
jwt (2.1.0)
95114
loofah (2.2.3)
96115
crass (~> 1.0.2)
97116
nokogiri (>= 1.5.9)
98117
mail (2.7.1)
99118
mini_mime (>= 0.1.1)
119+
memoist (0.16.0)
100120
metaclass (0.0.4)
101121
method_source (0.9.2)
102122
mini_mime (1.0.1)
103123
mini_portile2 (2.3.0)
124+
minispec-metadata (2.0.0)
125+
minitest
104126
minitest (5.11.3)
127+
minitest-vcr (1.4.0)
128+
minispec-metadata (~> 2.0)
129+
minitest (>= 4.7.5)
130+
vcr (>= 2.9)
105131
mocha (1.7.0)
106132
metaclass (~> 0.0.1)
133+
multi_json (1.13.1)
134+
multipart-post (2.0.0)
107135
nokogiri (1.8.5)
108136
mini_portile2 (~> 2.3.0)
109137
orm_adapter (0.5.0)
138+
os (1.0.0)
110139
pg (0.18.4)
111140
pry (0.12.2)
112141
coderay (~> 1.1.0)
113142
method_source (~> 0.9.0)
143+
pry-byebug (3.6.0)
144+
byebug (~> 10.0)
145+
pry (~> 0.10)
114146
pry-rails (0.3.7)
115147
pry (>= 0.10.4)
116148
public_suffix (3.0.3)
@@ -162,6 +194,11 @@ GEM
162194
sprockets (>= 2.8, < 4.0)
163195
sprockets-rails (>= 2.0, < 4.0)
164196
tilt (>= 1.1, < 3)
197+
signet (0.11.0)
198+
addressable (~> 2.3)
199+
faraday (~> 0.9)
200+
jwt (>= 1.5, < 3.0)
201+
multi_json (~> 1.10)
165202
sprockets (3.7.2)
166203
concurrent-ruby (~> 1.0)
167204
rack (> 1, < 3)
@@ -177,6 +214,7 @@ GEM
177214
tilt (2.0.9)
178215
tzinfo (1.2.5)
179216
thread_safe (~> 0.1)
217+
vcr (4.0.0)
180218
warden (1.2.7)
181219
rack (>= 1.0)
182220
webmock (3.4.2)
@@ -195,16 +233,21 @@ DEPENDENCIES
195233
coffee-rails
196234
database_cleaner
197235
devise
236+
google-cloud-translate (~> 1.2.4)
198237
jquery-rails
199238
lit!
239+
minitest (~> 5.11.3)
240+
minitest-vcr (~> 1.4.0)
200241
mocha
201242
pg (~> 0.18.4)
243+
pry-byebug (~> 3.6.0)
202244
pry-rails
203245
rails (~> 4.2.0)
204246
redis
205247
sass-rails
206248
test_after_commit
207249
test_declarative
250+
vcr (~> 4.0.0)
208251
webmock
209252

210253
BUNDLED WITH

0 commit comments

Comments
 (0)