active_translation 0.7.1
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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +365 -0
- data/Rakefile +8 -0
- data/app/assets/stylesheets/active_translation/application.css +15 -0
- data/app/controllers/active_translation/application_controller.rb +4 -0
- data/app/helpers/active_translation/application_helper.rb +4 -0
- data/app/jobs/active_translation/application_job.rb +4 -0
- data/app/lib/active_translation/google_translate.rb +52 -0
- data/app/mailers/active_translation/application_mailer.rb +6 -0
- data/app/models/active_translation/application_record.rb +5 -0
- data/app/models/active_translation/translation.rb +13 -0
- data/app/views/layouts/active_translation/application.html.erb +17 -0
- data/config/routes.rb +2 -0
- data/lib/active_translation/configuration.rb +29 -0
- data/lib/active_translation/engine.rb +11 -0
- data/lib/active_translation/translatable.rb +230 -0
- data/lib/active_translation/translation_job.rb +36 -0
- data/lib/active_translation/version.rb +3 -0
- data/lib/active_translation.rb +21 -0
- data/lib/generators/active_translation/install_generator.rb +26 -0
- data/lib/generators/active_translation/templates/README +19 -0
- data/lib/generators/active_translation/templates/active_translation.rb +14 -0
- data/lib/generators/active_translation/templates/create_translations.rb +14 -0
- data/lib/tasks/active_translation_tasks.rake +4 -0
- metadata +123 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 1234e553819683f3b7d5f77a491a8a39e1c1aef7f5fdef19190e2f046c7debfd
|
|
4
|
+
data.tar.gz: 5823014a65f7a39d1f3cf625f30ab1021d8cc6def7b655aa2f8b893419f56a5a
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: b5c18a5c9df15dddbd68088de1e7ed409d354bd9097430f1294559606abb6b527311515bf59fef1b60fb912496cc5d952dc0e0cbe38c5fd453c7dbfd156183f9
|
|
7
|
+
data.tar.gz: 37384c54c3d287811635d616cafac796200ebc86c856ee5d827a9c2fd3c9db137abf7dfb7724250c3870a4718b590ebd58945b11a68f3b1ff4f138819756c1e8
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright Sean Hogge
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
# WARNING KLAXON WARNING
|
|
2
|
+
|
|
3
|
+
🚨 ActiveTranslation is pre-1.0 🚨
|
|
4
|
+
|
|
5
|
+
This means there may be unhandled edge cases and undiscovered bugs. Please don't use it in production without rigorous QA.
|
|
6
|
+
|
|
7
|
+
If you find a bug or problem, please report it. If you have an idea for a new feature, please suggest it.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# ActiveTranslation
|
|
11
|
+
|
|
12
|
+
ActiveTranslation is a Rails plugin that lets you easily translate ActiveRecord models. With a single line added to that model, you can declare which columns, which locales, and what constraints to allow or prevent translation.
|
|
13
|
+
|
|
14
|
+
ActiveTranslation was built at and is sponsored by [Talentronic](https://talentronic.com)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
## How does this differ from internationalization (`I18n`)?
|
|
18
|
+
|
|
19
|
+
I18n is a great solution for elements of your app that are static, nearly static, aren't database-driven, or otherwise can be controlled or constrained in some fashion.
|
|
20
|
+
|
|
21
|
+
It can't help you if you have something that's user-supplied. It can't help you if you need translations available without a deploy, on demand.
|
|
22
|
+
|
|
23
|
+
Even in instances where it can help you, there are times when having hundreds of lines of YAML can become tedious or difficult to maintain.
|
|
24
|
+
|
|
25
|
+
Consider something like product categories: you might allow product managers to create them as needed. If you want them translated, you now need to communicate this to someone who can update the translation files. It's not difficult or tedious at first.
|
|
26
|
+
|
|
27
|
+
Instead, what if you just add a single line to your `Category` model. Now product managers can create new categories with whimsical abandon, and your international customers don't have to wait for engineering or a third party to copy/paste into a `.yml` file.
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
Add the gem to your gemfile:
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
gem "active_translation", git: "https://github.com/seanhogge/active_translation"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
And then bundle:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
bundle
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Run the installer to add a migration and initializer:
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
rails generate active_translation:install
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Migrate your primary database:
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
rails db:migrate
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
You will need to restart your rails server and your ActiveJob adapter process (if separate) if it was running when you installed and migrated.
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
## Configuration
|
|
60
|
+
|
|
61
|
+
The first step after installation is to configure your Google credentials. ActiveTranslation uses the Google Translate API in the background for translation. This is a bit more than just an API key.
|
|
62
|
+
|
|
63
|
+
The general idea is:
|
|
64
|
+
|
|
65
|
+
1. Create a project at https://console.cloud.google.com
|
|
66
|
+
1. In “APIs & Services” > “Library” look for “Cloud Translation API”
|
|
67
|
+
1. Create a Service Account and download the JSON key file
|
|
68
|
+
1. Ensure billing is enabled, and all the other prerequisites that Google requires
|
|
69
|
+
1. Extract the necessary data from that JSON file and plug those values into `config/initializers/active_translation.rb` by setting the appropriate environment variables
|
|
70
|
+
|
|
71
|
+
Feel free to change the names of the environment variables, or to alter that initializer to assign those keys however you like. At Talentronic, we have an `APIConnection` model we use for stuff like that so we grab the credentials from there and assign them.
|
|
72
|
+
|
|
73
|
+
You could also use something like `dotenv-rails` and create a `.env` file in your various environments.
|
|
74
|
+
|
|
75
|
+
Or you could use the OS to define them, such as in an `/etc/environment` file.
|
|
76
|
+
|
|
77
|
+
If you're using Kamal, you probably already have a way to manage secrets and add them to env variables - that works, too.
|
|
78
|
+
|
|
79
|
+
Obvious reminder: whatever method you use, just make sure it's not committed to any repository, even once. If you do, make sure you get new credentials and expire/delete the credentials that got committed.
|
|
80
|
+
|
|
81
|
+
That's the hard part!
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
## Usage
|
|
85
|
+
|
|
86
|
+
To any ActiveRecord model, add `translates` with a list of columns that should be translated, a list of locales and any constraints.
|
|
87
|
+
|
|
88
|
+
Simplest form:
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
translates :content, into: %i[es fr de]
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Into
|
|
95
|
+
|
|
96
|
+
The `into` argument can be an array of locales, a symbol that matches a method that returns an array of locales, or a Proc that returns an array of locales.
|
|
97
|
+
|
|
98
|
+
So you could do:
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
translates :content, into: :method_that_returns_locales
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
or
|
|
105
|
+
|
|
106
|
+
```ruby
|
|
107
|
+
translates :content, into: -> { I18n.available_locales - [ I18n.default_locale ] }
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
> `it` is a recent Ruby syntactical grain of sugar. It's the same as `_1` which lets you skip the `{ |arg| arg == :stuff }` repetition
|
|
111
|
+
|
|
112
|
+
#### Into All
|
|
113
|
+
|
|
114
|
+
Because translating a model into all the locales your app may define is so common, you can pass `:all` to the `into` argument to achieve the same result as passing `-> { I18n.available_locales - [ I18n.default_locale ] }`.
|
|
115
|
+
|
|
116
|
+
This means you cannot pass in your own method called "all" as a symbol, of course.
|
|
117
|
+
|
|
118
|
+
### If Constraints
|
|
119
|
+
|
|
120
|
+
An `if` constraint will prevent translating if it returns `false`.
|
|
121
|
+
|
|
122
|
+
If you have a boolean column like `published`, you might do:
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
translates :content, into: %i[es fr de], if: :published?
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Or you can define your own method that returns a boolean:
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
translates :content, into: %i[es fr de], if: :record_should_be_translated?
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Or you can use a Proc:
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
translates :content, into: %i[es fr de], if: -> { content.length > 10 }
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Unless Constraints
|
|
141
|
+
|
|
142
|
+
These work exactly the same as the `if` constraint, but the logic is flipped. If the constraint returns `true` then no translating will take place.
|
|
143
|
+
|
|
144
|
+
### Constraint Compliance
|
|
145
|
+
|
|
146
|
+
If your record is updated such that either an `if` or `unless` constraint is toggled, this will trigger the addition _or removal_ of translation data. The idea here is that the constraint controls whether a translation should _exist_, not whether a translation should be performed.
|
|
147
|
+
|
|
148
|
+
This means if you use a constraint that frequently changes value, you will be paying for half of all change events.
|
|
149
|
+
|
|
150
|
+
This is intentional. Translations are regenerated any time one of the translated attributes changes. But what about something like a `Post` that shouldn't be translated until it's published? There's no sense in translating it dozens of times as it's edited, but clicking the “publish” button doesn't update the translatable attributes.
|
|
151
|
+
|
|
152
|
+
So ActiveTranslation watches for the constraint to change so that when the `Post` is published, the translation is performed with no extra effort.
|
|
153
|
+
|
|
154
|
+
Likewise, if the constraint changes the other way, translations are removed since ActiveTranslation will no longer be keeping those translations up-to-date. Better to have no translation than a completely wrong one.
|
|
155
|
+
|
|
156
|
+
### Manual Attributes
|
|
157
|
+
|
|
158
|
+
Sometimes you want to translate an attribute, but it's not something Google Translate or an LLM can handle on their own. For instance, at Talentronic, we have names of businesses that operate in airports. These names have trademarked names that might look like common words, but aren't. These names also have the airport included which can confuse the LLM or API when it's mixed in with the business name.
|
|
159
|
+
|
|
160
|
+
So we need manual translation attributes:
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
translates :content, manual: :name, into: %i[es fr]
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Manual attributes have a special setter in the form of `#{locale}_#{attribute_name}`. So in this example, we get `fr_name=` and `es_name=`.
|
|
167
|
+
|
|
168
|
+
These attributes never trigger retranslation, and are never checked against the original text - it's entirely up to you to maintain them. However, it does get stored alongside all the other translations, keeping your database tidy and your translation code consistent.
|
|
169
|
+
|
|
170
|
+
### The Show
|
|
171
|
+
|
|
172
|
+
Once you have added the `translates` directive with your columns, locales, and constraints and your models have been translated to at least one locale, it's time to actually use them.
|
|
173
|
+
|
|
174
|
+
If you set:
|
|
175
|
+
|
|
176
|
+
```ruby
|
|
177
|
+
translates :content, manual: :name, into: %i[es fr]
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
on a `Post` model, then you can simply call `.content` and it will use the current locale from Rails `I18n`.
|
|
181
|
+
|
|
182
|
+
If you need the value for a locale other than the current, you can specify the locale explicitly:
|
|
183
|
+
|
|
184
|
+
```ruby
|
|
185
|
+
@post.content(locale: :fr)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
If the post has an `fr_translation`, then that will be shown. If no `fr_translation` exists, it will show the post's untranslated `content`.
|
|
189
|
+
|
|
190
|
+
In this way, you'll never have missing values, but you will have the default language version instead of the translated version.
|
|
191
|
+
|
|
192
|
+
The same goes for manual translations:
|
|
193
|
+
|
|
194
|
+
```ruby
|
|
195
|
+
@post.name
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
returns the translated name for the current locale if it exists, or the untranslated `name` if it does.
|
|
199
|
+
|
|
200
|
+
```ruby
|
|
201
|
+
@post.name(locale: :es)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Regardless of the current locale, it will return the `:es` translated value for the `name` attribute, or the untranslated `name` if the `es_translation` doesn't exist.
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
### Extras
|
|
208
|
+
|
|
209
|
+
There are a few niceties provided to make ActiveTranslation as flexible as possible.
|
|
210
|
+
|
|
211
|
+
Ideal world: you won't need them.
|
|
212
|
+
Real world: you might need them.
|
|
213
|
+
|
|
214
|
+
#### Translate on Demand
|
|
215
|
+
|
|
216
|
+
There may be times when things get hosed. You might need or want to translate the automatic columns manually. You can do this in three ways:
|
|
217
|
+
|
|
218
|
+
##### translate_if_needed
|
|
219
|
+
|
|
220
|
+
By calling `translate_if_needed`, you can run the same checks that would occur on update. This is similar to calling `touch`, but it doesn't update the `updated_at` timestamp
|
|
221
|
+
|
|
222
|
+
This will schedule the translation as a background job
|
|
223
|
+
|
|
224
|
+
##### translate!
|
|
225
|
+
|
|
226
|
+
By calling `translate!`, you skip all checks for whether a translation is outdated or missing and generate a new translation even if it's already extant and accurate.
|
|
227
|
+
|
|
228
|
+
This will schedule the translation as a background job.
|
|
229
|
+
|
|
230
|
+
##### translate_now!(locales)
|
|
231
|
+
|
|
232
|
+
By calling `translate_now!` and optionally passing 1 or more locales, you skip all checks for whether a translation is outdated or missing and generate a new translation for the passed locales even if they're already extant and accurate.
|
|
233
|
+
|
|
234
|
+
The default value for locales is `translatable_locales`, so if you don't pass anything, it will translate into all locales defined for that model.
|
|
235
|
+
|
|
236
|
+
This method does **not** schedule the job, and instead immediately performs the translation, blocking until the translations for all locales are complete.
|
|
237
|
+
|
|
238
|
+
#### Introspection
|
|
239
|
+
|
|
240
|
+
The goal of ActiveTranslation is to make translations as automatic and invisible as possible. However, there may be times when you need to know the state of translations on a model instance.
|
|
241
|
+
|
|
242
|
+
##### translation_checksum
|
|
243
|
+
|
|
244
|
+
By calling `translation_checksum`, you can return the checksum used on a record to determine whether translations are outdated.
|
|
245
|
+
|
|
246
|
+
##### translations_outdated?
|
|
247
|
+
|
|
248
|
+
By calling `translations_outdated?`, you can get `true` if any translation has a checksum that no longer matches the source (otherwise you get `false`).
|
|
249
|
+
|
|
250
|
+
This has limited value, but is provided in case you need to handle situations in which models change without triggering callbacks.
|
|
251
|
+
|
|
252
|
+
> NOTE: `translations_outdated?` will _always_ return `false` if the conditions you passed (`if` & `unless`) are not met
|
|
253
|
+
|
|
254
|
+
##### outdated_translations
|
|
255
|
+
|
|
256
|
+
By calling `outdated_translations`, you can get an array of all `translations` that are outdated.
|
|
257
|
+
|
|
258
|
+
This has limited value, but is provided in case you need to handle situations in which models change without triggering callbacks.
|
|
259
|
+
|
|
260
|
+
##### translations_missing?
|
|
261
|
+
|
|
262
|
+
By calling `translations_missing?`, you can get `true` if any translations are missing. This is a complex question, and is `false` unless:
|
|
263
|
+
|
|
264
|
+
- any automatic translation attributes are not blank
|
|
265
|
+
- any automatic translation attributes are missing an entry for any locale (in addition to not being blank)
|
|
266
|
+
|
|
267
|
+
So if you have `translates :title, manual: :name, into: :all` and your app supports `:fr` and `:es`, you will get `true` if:
|
|
268
|
+
|
|
269
|
+
- the `title` has been translated into `:es`, but not `:fr`
|
|
270
|
+
- no translations exist at all
|
|
271
|
+
- the `name` has been translated into both `:es` and `:fr` but `title` hasn't been translated
|
|
272
|
+
- the `name` has been translated into both `:es` and `:fr` but `title` has been translated into only one locale
|
|
273
|
+
|
|
274
|
+
and you will get `false` if:
|
|
275
|
+
|
|
276
|
+
- translations conditions are not met, regardless of the presence or absence of any translations
|
|
277
|
+
- the `title` column is blank (`nil` or empty string)
|
|
278
|
+
- the `title` column has been fully translated but the `name` column has not been (manual attributes are ignored)
|
|
279
|
+
- the `title` column has been fully translated, but the `title` column has changed since the translation in a way that doesn't trigger callbacks
|
|
280
|
+
|
|
281
|
+
This has limited value, but is provided in case you need to handle situations in which models change without triggering callbacks.
|
|
282
|
+
|
|
283
|
+
##### fully_translated?(auto_or_manual_or_all)
|
|
284
|
+
|
|
285
|
+
By calling `fully_translated?` with no arguments, you can get `true` if all attributes are translated. This ignores manual attributes by default.
|
|
286
|
+
|
|
287
|
+
There are some special symbols you can pass to change the scope of "fully." If you pass `:all` or `:include_manual`, then you will get `true` only if all automatic _and_ manual attributes have a translation.
|
|
288
|
+
|
|
289
|
+
If you pass `:manual` or `:manual_only`, then you will get `true` only if all manual attributes have a translation, disregarding automatic attributes.
|
|
290
|
+
|
|
291
|
+
Passing `:auto` or `:auto_only` is the same as passing no argument.
|
|
292
|
+
|
|
293
|
+
Passing an invalid argument raises an error.
|
|
294
|
+
|
|
295
|
+
> NOTE: `fully_translated?` will _always_ return `true` if the conditions you passed (`if` & `unless`) are not met
|
|
296
|
+
|
|
297
|
+
##### translatable_locales
|
|
298
|
+
|
|
299
|
+
By calling `translatable_locales`, you will get an array of locales for which the object will be translated. This has no bearing on whether any translations exist, or any conditions for translations to be performed.
|
|
300
|
+
|
|
301
|
+
> NOTES: This is only defined on instances of a model, not the model itself, since the `into` argument allows more than just an Array literal.
|
|
302
|
+
|
|
303
|
+
##### translation_config
|
|
304
|
+
|
|
305
|
+
You can call `translation_config` on a model or instance to see what you've set up for translations. You'll see something like:
|
|
306
|
+
|
|
307
|
+
```ruby
|
|
308
|
+
> Page.translation_config
|
|
309
|
+
=> {attributes: [:title, :heading, :subhead, :content], manual_attributes: [], locales: :all, unless: nil, if: :published?}
|
|
310
|
+
|
|
311
|
+
> Category.translation_config
|
|
312
|
+
=> {attributes: [:name, :short_name],
|
|
313
|
+
manual_attributes: [],
|
|
314
|
+
locales: #<Proc:0x000000012231a2b8 /path/to/projects/active_translation/app/models/category.rb:67 (lambda)>,
|
|
315
|
+
unless: nil,
|
|
316
|
+
if: nil}
|
|
317
|
+
|
|
318
|
+
> Widget.translation_config
|
|
319
|
+
=> {attributes: [:title, :headline, :ad_html],
|
|
320
|
+
manual_attributes: [],
|
|
321
|
+
locales: [:es, :fr],
|
|
322
|
+
unless: #<Proc:0x00000001228fea58 /path/to/projects/active_translation/app/models/widget.rb:42 (lambda)>,
|
|
323
|
+
if: nil}
|
|
324
|
+
|
|
325
|
+
> Widget.last.translation_config
|
|
326
|
+
=> {attributes: [:title, :headline, :ad_html],
|
|
327
|
+
manual_attributes: [],
|
|
328
|
+
locales: [:es, :fr],
|
|
329
|
+
unless: #<Proc:0x00000001228fea58 /path/to/projects/active_translation/app/models/widget.rb:42 (lambda)>,
|
|
330
|
+
if: nil}
|
|
331
|
+
|
|
332
|
+
> Account.translation_config
|
|
333
|
+
=> {attributes: [:profile_html], manual_attributes: ["name"], locales: :method_that_returns_locales, unless: nil, if: nil}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
#### Disclaimer
|
|
337
|
+
|
|
338
|
+
ActiveTranslation doesn't check the accuracy of translations in any way. It assumes that the response from Google is always perfect. If you are translating sensitive content where accuracy is critical in a legal or existential sense, you must handle translation auditing separately.
|
|
339
|
+
|
|
340
|
+
So if you use the for an EULA, make it a manual attribute or don't use ActiveTranslation for it at all.
|
|
341
|
+
|
|
342
|
+
ActiveTranslation doesn't redact any content. It assumes you would never send PII or financial data for translation. So... please don't.
|
|
343
|
+
|
|
344
|
+
## Testing
|
|
345
|
+
|
|
346
|
+
Ideally, you do not need to write any tests for translations or how they behave, since ActiveTranslation tests itself.
|
|
347
|
+
|
|
348
|
+
However, perhaps you bolt on additional functionality to tests, or translations are critical to your application, or you simply want to ensure you're expecting the correct results from using the gem. In that case, ActiveTranslation provides some simple testing features.
|
|
349
|
+
|
|
350
|
+
All translations skip the call to Google in the test environment, and return a simple modification of that content. For example, translating into the `:fr` locale in a test environment would behave as so:
|
|
351
|
+
|
|
352
|
+
- Input: "auto translated content from ActiveRecord object"
|
|
353
|
+
- Output: "[fr] auto translated content from ActiveRecord object"
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
## Contributing
|
|
357
|
+
|
|
358
|
+
Fork the repo, make your changes, make a pull request.
|
|
359
|
+
|
|
360
|
+
Or simply report issues on the [GitHub repository](https://github.com/seanhogge/active_translation)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
## License
|
|
364
|
+
|
|
365
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
|
3
|
+
* listed below.
|
|
4
|
+
*
|
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
|
6
|
+
* or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
|
|
7
|
+
*
|
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
|
9
|
+
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
|
|
10
|
+
* files in this directory. Styles in this file should be added after the last require_* statement.
|
|
11
|
+
* It is generally better to create a new file per style scope.
|
|
12
|
+
*
|
|
13
|
+
*= require_tree .
|
|
14
|
+
*= require_self
|
|
15
|
+
*/
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# USAGE: GoogleTranslate.translate(target_language_code: "es-MX", text: "Hello world!")
|
|
2
|
+
# USAGE: GoogleTranslate.translate(target_language_code: "es-MX", text: Job.posted_status.last.title, obj: Job.posted_status.last)
|
|
3
|
+
# @see https://cloud.google.com/translate/docs/basic/translating-text
|
|
4
|
+
|
|
5
|
+
module ActiveTranslation
|
|
6
|
+
class GoogleTranslate
|
|
7
|
+
class << self
|
|
8
|
+
def translate(target_language_code:, text:, source: "en-US", obj: nil)
|
|
9
|
+
conn = Faraday.new(url: "https://translation.googleapis.com/") do |faraday|
|
|
10
|
+
faraday.request :json
|
|
11
|
+
faraday.response :json
|
|
12
|
+
faraday.request :authorization, "Bearer", -> { token }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
response =
|
|
16
|
+
conn.post(
|
|
17
|
+
"language/translate/v2",
|
|
18
|
+
{
|
|
19
|
+
q: text,
|
|
20
|
+
target: target_language_code,
|
|
21
|
+
source: source,
|
|
22
|
+
}
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
return nil unless response.success?
|
|
26
|
+
|
|
27
|
+
parse_response(response)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def parse_response(response)
|
|
33
|
+
response.body.dig("data", "translations", 0, "translatedText")
|
|
34
|
+
# CGI.unescapeHTML(translation) if translation.is_a?(String)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def token
|
|
38
|
+
return "fake_access_token" if Rails.env.test?
|
|
39
|
+
|
|
40
|
+
Rails.cache.fetch("google_access_token", expires_in: 55.minutes) do
|
|
41
|
+
google_oauth_credentials = ActiveTranslation.configuration.to_json
|
|
42
|
+
authorizer = Google::Auth::ServiceAccountCredentials.make_creds(
|
|
43
|
+
json_key_io: StringIO.new(google_oauth_credentials),
|
|
44
|
+
scope: [ "https://www.googleapis.com/auth/cloud-platform" ]
|
|
45
|
+
)
|
|
46
|
+
authorizer.fetch_access_token!
|
|
47
|
+
authorizer.access_token
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module ActiveTranslation
|
|
2
|
+
class Translation < ApplicationRecord
|
|
3
|
+
belongs_to :translatable, polymorphic: true
|
|
4
|
+
|
|
5
|
+
validates :locale, presence: true, uniqueness: { scope: [ :translatable_type, :translatable_id ] }
|
|
6
|
+
|
|
7
|
+
serialize :translated_attributes, coder: JSON
|
|
8
|
+
|
|
9
|
+
def outdated?
|
|
10
|
+
source_checksum != translatable.translation_checksum
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>Active translations</title>
|
|
5
|
+
<%= csrf_meta_tags %>
|
|
6
|
+
<%= csp_meta_tag %>
|
|
7
|
+
|
|
8
|
+
<%= yield :head %>
|
|
9
|
+
|
|
10
|
+
<%= stylesheet_link_tag "active_translation/application", media: "all" %>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
|
|
14
|
+
<%= yield %>
|
|
15
|
+
|
|
16
|
+
</body>
|
|
17
|
+
</html>
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module ActiveTranslation
|
|
2
|
+
class Configuration
|
|
3
|
+
attr_accessor :type,
|
|
4
|
+
:project_id,
|
|
5
|
+
:private_key_id,
|
|
6
|
+
:private_key,
|
|
7
|
+
:client_email,
|
|
8
|
+
:client_id,
|
|
9
|
+
:auth_uri,
|
|
10
|
+
:token_uri,
|
|
11
|
+
:auth_provider_x509_cert_url,
|
|
12
|
+
:client_x509_cert_url,
|
|
13
|
+
:universe_domain
|
|
14
|
+
|
|
15
|
+
def initialize
|
|
16
|
+
@type = nil
|
|
17
|
+
@project_id = nil
|
|
18
|
+
@private_key_id = nil
|
|
19
|
+
@private_key = nil
|
|
20
|
+
@client_email = nil
|
|
21
|
+
@client_id = nil
|
|
22
|
+
@auth_uri = "https://accounts.google.com/o/oauth2/auth"
|
|
23
|
+
@token_uri = "https://oauth2.googleapis.com/token"
|
|
24
|
+
@auth_provider_x509_cert_url = "https://www.googleapis.com/oauth2/v1/certs"
|
|
25
|
+
@client_x509_cert_url = nil
|
|
26
|
+
@universe_domain = "googleapis.com"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
module ActiveTranslation
|
|
2
|
+
module Translatable
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
class_methods do
|
|
6
|
+
def translates(*attributes, manual: [], into:, unless: nil, if: nil)
|
|
7
|
+
@translation_config ||= {}
|
|
8
|
+
@translation_config[:attributes] = Array(attributes).map(&:to_s)
|
|
9
|
+
@translation_config[:manual_attributes] = Array(manual).map(&:to_s)
|
|
10
|
+
@translation_config[:locales] = into
|
|
11
|
+
@translation_config[:unless] = binding.local_variable_get(:unless)
|
|
12
|
+
@translation_config[:if] = binding.local_variable_get(:if)
|
|
13
|
+
|
|
14
|
+
has_many :translations, class_name: "ActiveTranslation::Translation", as: :translatable, dependent: :destroy
|
|
15
|
+
|
|
16
|
+
delegate :translation_config, to: :class
|
|
17
|
+
delegate :translatable_attribute_names, to: :class
|
|
18
|
+
|
|
19
|
+
after_commit :translate_if_needed, on: [ :create, :update ]
|
|
20
|
+
|
|
21
|
+
# Respond to calls for manual attribute retrieval (model.fr_attribute)
|
|
22
|
+
# Respond to calls for manual attribute assignment (model.fr_attribute = "Bonjour")
|
|
23
|
+
# Respond to calls such as fr_translation or de_translation
|
|
24
|
+
# Respond to calls such as model.name(locale: :fr)
|
|
25
|
+
define_method(:method_missing) do |method_name, *args, &block|
|
|
26
|
+
super() unless method_name.to_s.split("_").size == 2
|
|
27
|
+
|
|
28
|
+
locale = method_name.to_s.split("_").first
|
|
29
|
+
attribute = method_name.to_s.split("_").last
|
|
30
|
+
|
|
31
|
+
if translation_config[:manual_attributes].include? attribute
|
|
32
|
+
translation = translations.find_by(locale: locale)
|
|
33
|
+
return read_attribute(attribute) unless translation
|
|
34
|
+
|
|
35
|
+
translation.translated_attributes[attribute].presence || read_attribute(attribute)
|
|
36
|
+
elsif attribute.last == "=" && translation_config[:manual_attributes].include?(attribute.delete("="))
|
|
37
|
+
attribute.delete!("=")
|
|
38
|
+
translation = translations.find_or_initialize_by(locale: locale.to_s)
|
|
39
|
+
attrs = translation.translated_attributes ? translation.translated_attributes : {}
|
|
40
|
+
attrs[attribute] = args.first
|
|
41
|
+
translation.translated_attributes = attrs
|
|
42
|
+
translation.save!
|
|
43
|
+
elsif attribute == "translation" || translation_config[:attributes].include?(attribute)
|
|
44
|
+
translations.find_by(locale: locale)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Override attribute methods so that they accept a locale argument (defaulting to current I18n locale)
|
|
49
|
+
attributes.each do |attr|
|
|
50
|
+
define_method(attr) do |locale: I18n.locale|
|
|
51
|
+
if locale && translation = translations.find_by(locale: locale.to_s)
|
|
52
|
+
translation.translated_attributes&.dig attr.to_s || super()
|
|
53
|
+
else
|
|
54
|
+
super()
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Override manual attribute methods so that they accept a locale argument (defaulting to current I18n locale)
|
|
60
|
+
Array(manual).each do |attr|
|
|
61
|
+
define_method("#{attr}") do |locale: I18n.locale|
|
|
62
|
+
if locale && translation = translations.find_by(locale: locale.to_s)
|
|
63
|
+
translation.translated_attributes[attr.to_s].presence || read_attribute(attr)
|
|
64
|
+
else
|
|
65
|
+
read_attribute(attr)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def translatable_attribute_names
|
|
72
|
+
translation_config[:attributes]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def translation_config
|
|
76
|
+
@translation_config
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def fully_translated?(attribute_types = :auto)
|
|
81
|
+
case attribute_types
|
|
82
|
+
when :auto, :auto_only
|
|
83
|
+
!translations_missing?
|
|
84
|
+
when :manual, :manual_only
|
|
85
|
+
!manual_translations_missing?
|
|
86
|
+
when :all, :include_manual
|
|
87
|
+
!translations_missing? && !manual_translations_missing?
|
|
88
|
+
else
|
|
89
|
+
raise ArgumentError, "acceptable arguments are [:auto, :auto_only, :manual, :manual_only, :all, :include_manual]"
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def manual_translations_missing?
|
|
94
|
+
return false unless conditions_met?
|
|
95
|
+
|
|
96
|
+
translatable_locales.each do |locale|
|
|
97
|
+
translation_config[:manual_attributes].each do |attribute|
|
|
98
|
+
next if read_attribute(attribute).blank?
|
|
99
|
+
|
|
100
|
+
return true unless translation = translations.find_by(locale: locale)
|
|
101
|
+
return true unless translation.translated_attributes.keys.include?(attribute)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
false
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def outdated_translations
|
|
109
|
+
translations.select { _1.outdated? }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def translatable_locales
|
|
113
|
+
case translation_config[:locales]
|
|
114
|
+
when Symbol
|
|
115
|
+
if translation_config[:locales] == :all
|
|
116
|
+
I18n.available_locales - [ I18n.default_locale ]
|
|
117
|
+
else
|
|
118
|
+
send(translation_config[:locales])
|
|
119
|
+
end
|
|
120
|
+
when Proc
|
|
121
|
+
instance_exec(&translation_config[:locales])
|
|
122
|
+
when Array
|
|
123
|
+
translation_config[:locales]
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def translate_if_needed
|
|
128
|
+
translations.delete_all and return unless conditions_met?
|
|
129
|
+
|
|
130
|
+
return unless translatable_attributes_changed? || condition_checks_changed? || translations_outdated? || translations_missing?
|
|
131
|
+
|
|
132
|
+
translatable_locales.each do |locale|
|
|
133
|
+
translation = translations.find_or_create_by(locale: locale.to_s)
|
|
134
|
+
|
|
135
|
+
if translation.new_record? || translation.outdated?
|
|
136
|
+
TranslationJob.perform_later(self, locale.to_s, translation_checksum)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def translate!
|
|
142
|
+
translatable_locales.each do |locale|
|
|
143
|
+
TranslationJob.perform_later(self, locale.to_s, translation_checksum)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def translate_now!(locales = translatable_locales)
|
|
148
|
+
Array(locales).each do |locale|
|
|
149
|
+
TranslationJob.perform_now(self, locale.to_s, translation_checksum)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def translation_checksum
|
|
154
|
+
values = translatable_attribute_names.map { |attr| read_attribute(attr).to_s }
|
|
155
|
+
Digest::MD5.hexdigest(values.join)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# translations are "missing" if they are not manual, the translatable attribute isn't blank
|
|
159
|
+
# and there's no translation for that attribute for all locales
|
|
160
|
+
def translations_missing?
|
|
161
|
+
return false unless conditions_met?
|
|
162
|
+
|
|
163
|
+
translatable_locales.each do |locale|
|
|
164
|
+
translatable_attribute_names.each do |attribute|
|
|
165
|
+
next if read_attribute(attribute).blank?
|
|
166
|
+
|
|
167
|
+
return true unless translation = translations.find_by(locale: locale)
|
|
168
|
+
return true unless translation.translated_attributes.keys.include?(attribute)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
false
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def translations_outdated?
|
|
176
|
+
return false unless conditions_met?
|
|
177
|
+
return true if translations.map(&:outdated?).any?
|
|
178
|
+
|
|
179
|
+
false
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
private
|
|
183
|
+
|
|
184
|
+
def condition_checks_changed?
|
|
185
|
+
saved_changes.any? && conditions_exist? && conditions_met?
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def conditions_exist?
|
|
189
|
+
return true if translation_config[:if] || translation_config[:unless]
|
|
190
|
+
|
|
191
|
+
false
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# returns true if all conditions are met, or if there are no conditions
|
|
195
|
+
def conditions_met?
|
|
196
|
+
if_condition_met? && unless_condition_met?
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def evaluate_condition(condition)
|
|
200
|
+
case condition
|
|
201
|
+
when Symbol
|
|
202
|
+
send(condition)
|
|
203
|
+
when Proc
|
|
204
|
+
instance_exec(&condition)
|
|
205
|
+
when nil
|
|
206
|
+
true
|
|
207
|
+
else
|
|
208
|
+
false
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# returns true if condition is met or there is no condition
|
|
213
|
+
def if_condition_met?
|
|
214
|
+
return true unless translation_config[:if]
|
|
215
|
+
|
|
216
|
+
evaluate_condition(translation_config[:if])
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def translatable_attributes_changed?
|
|
220
|
+
saved_changes.any? && saved_changes.keys.intersect?(translatable_attribute_names)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# returns true if condition is met or there is no condition
|
|
224
|
+
def unless_condition_met?
|
|
225
|
+
return true unless translation_config[:unless]
|
|
226
|
+
|
|
227
|
+
!evaluate_condition(translation_config[:unless])
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module ActiveTranslation
|
|
2
|
+
class TranslationJob < ActiveJob::Base
|
|
3
|
+
queue_as :default
|
|
4
|
+
|
|
5
|
+
def perform(object, locale, checksum)
|
|
6
|
+
translated_data = {}
|
|
7
|
+
|
|
8
|
+
object.translatable_attribute_names.each do |attribute|
|
|
9
|
+
source_text = object.read_attribute(attribute)
|
|
10
|
+
translated_data[attribute.to_s] = translate_text(source_text, locale)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
translation = object.translations
|
|
14
|
+
.find_or_initialize_by(
|
|
15
|
+
locale: locale,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
existing_data = translation.translated_attributes.present? ? translation.translated_attributes : {}
|
|
19
|
+
|
|
20
|
+
merged_attributes = existing_data.merge(translated_data)
|
|
21
|
+
|
|
22
|
+
translation.update!(
|
|
23
|
+
translated_attributes: merged_attributes,
|
|
24
|
+
source_checksum: checksum
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def translate_text(text, target_locale)
|
|
31
|
+
return "[#{target_locale}] #{text}" if Rails.env.test?
|
|
32
|
+
|
|
33
|
+
ActiveTranslation::GoogleTranslate.translate(target_language_code: target_locale, text: text)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
require "active_translation/version"
|
|
2
|
+
require "active_translation/engine"
|
|
3
|
+
require "active_translation/configuration"
|
|
4
|
+
require "active_translation/translatable"
|
|
5
|
+
require "active_translation/translation_job"
|
|
6
|
+
require "faraday"
|
|
7
|
+
require "googleauth"
|
|
8
|
+
|
|
9
|
+
module ActiveTranslation
|
|
10
|
+
class << self
|
|
11
|
+
attr_writer :configuration
|
|
12
|
+
|
|
13
|
+
def configuration
|
|
14
|
+
@configuration ||= Configuration.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def configure
|
|
18
|
+
yield(configuration)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module ActiveTranslation
|
|
2
|
+
module Generators
|
|
3
|
+
class InstallGenerator < Rails::Generators::Base
|
|
4
|
+
include Rails::Generators::Migration
|
|
5
|
+
|
|
6
|
+
source_root File.expand_path("templates", __dir__)
|
|
7
|
+
|
|
8
|
+
def self.next_migration_number(dirname)
|
|
9
|
+
next_migration_number = current_migration_number(dirname) + 1
|
|
10
|
+
ActiveRecord::Migration.next_migration_number(next_migration_number)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def copy_migration
|
|
14
|
+
migration_template "create_translations.rb", "db/migrate/create_translations.rb"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def copy_initializer
|
|
18
|
+
template "active_translation.rb", "config/initializers/active_translation.rb"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def show_readme
|
|
22
|
+
readme "README" if behavior == :invoke
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
===============================================================================
|
|
2
|
+
|
|
3
|
+
Active Translations has been installed!
|
|
4
|
+
|
|
5
|
+
Next steps:
|
|
6
|
+
|
|
7
|
+
1. Run migrations:
|
|
8
|
+
rails db:migrate
|
|
9
|
+
|
|
10
|
+
2. Configure your Google Translation API key:
|
|
11
|
+
# config/initializers/active_translation.rb
|
|
12
|
+
ActiveTranslation.configure do |config|
|
|
13
|
+
config.google_api_key = ENV['GOOGLE_TRANSLATION_API_KEY']
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
3. Add to your models:
|
|
17
|
+
translates :name, :description, into: [:es, :fr]
|
|
18
|
+
|
|
19
|
+
===============================================================================
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
ActiveTranslation.configure do |config|
|
|
2
|
+
config.project_id = ENV.fetch("GOOGLE_TRANSLATION_PROJECT_ID", :missing_google_translation_project_id)
|
|
3
|
+
config.private_key_id = ENV.fetch("GOOGLE_TRANSLATION_PRIVATE_KEY_ID", :missing_google_translation_private_key_id)
|
|
4
|
+
config.private_key = ENV.fetch("GOOGLE_TRANSLATION_PRIVATE_KEY", :missing_google_translation_private_key)
|
|
5
|
+
config.client_email = ENV.fetch("GOOGLE_TRANSLATION_CLIENT_EMAIL", :missing_google_translation_client_email)
|
|
6
|
+
config.client_id = ENV.fetch("GOOGLE_TRANSLATION_CLIENT_ID", :missing_google_translation_client_id)
|
|
7
|
+
config.client_x509_cert_url = ENV.fetch("GOOGLE_TRANSLATION_CLIENT_CERT_URL", :missing_google_translation_client_cert_url)
|
|
8
|
+
|
|
9
|
+
config.type = ENV.fetch("GOOGLE_TRANSLATION_TYPE", "service_account")
|
|
10
|
+
config.auth_uri = ENV.fetch("GOOGLE_TRANSLATION_AUTH_URI", "https://accounts.google.com/o/oauth2/auth")
|
|
11
|
+
config.token_uri = ENV.fetch("GOOGLE_TRANSLATION_TOKEN_URI", "https://oauth2.googleapis.com/token")
|
|
12
|
+
config.auth_provider_x509_cert_url = ENV.fetch("GOOGLE_TRANSLATION_AUTH_PROVIDER_CERT_URL", "https://www.googleapis.com/oauth2/v1/certs")
|
|
13
|
+
config.universe_domain = ENV.fetch("GOOGLE_TRANSLATION_UNIVERSE_DOMAIN", "googleapis.com")
|
|
14
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
class CreateTranslations < ActiveRecord::Migration[7.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :active_translation_translations do |t|
|
|
4
|
+
t.references :translatable, polymorphic: true, null: false
|
|
5
|
+
t.string :locale, null: false
|
|
6
|
+
t.text :translated_attributes
|
|
7
|
+
t.string :source_checksum
|
|
8
|
+
t.timestamps
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
add_index :active_translation_translations, [ :translatable_type, :translatable_id, :locale ],
|
|
12
|
+
unique: true, name: "index_translations_on_translatable_and_locale"
|
|
13
|
+
end
|
|
14
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: active_translation
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.7.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Sean Hogge
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rails
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '7'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '7'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: activerecord
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '7'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '7'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: faraday
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '2.0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '2.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: googleauth
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '1.0'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '1.0'
|
|
68
|
+
description: Easily translate specific attributes of any ActiveRecord model
|
|
69
|
+
email:
|
|
70
|
+
- sean@seanhogge.com
|
|
71
|
+
executables: []
|
|
72
|
+
extensions: []
|
|
73
|
+
extra_rdoc_files: []
|
|
74
|
+
files:
|
|
75
|
+
- MIT-LICENSE
|
|
76
|
+
- README.md
|
|
77
|
+
- Rakefile
|
|
78
|
+
- app/assets/stylesheets/active_translation/application.css
|
|
79
|
+
- app/controllers/active_translation/application_controller.rb
|
|
80
|
+
- app/helpers/active_translation/application_helper.rb
|
|
81
|
+
- app/jobs/active_translation/application_job.rb
|
|
82
|
+
- app/lib/active_translation/google_translate.rb
|
|
83
|
+
- app/mailers/active_translation/application_mailer.rb
|
|
84
|
+
- app/models/active_translation/application_record.rb
|
|
85
|
+
- app/models/active_translation/translation.rb
|
|
86
|
+
- app/views/layouts/active_translation/application.html.erb
|
|
87
|
+
- config/routes.rb
|
|
88
|
+
- lib/active_translation.rb
|
|
89
|
+
- lib/active_translation/configuration.rb
|
|
90
|
+
- lib/active_translation/engine.rb
|
|
91
|
+
- lib/active_translation/translatable.rb
|
|
92
|
+
- lib/active_translation/translation_job.rb
|
|
93
|
+
- lib/active_translation/version.rb
|
|
94
|
+
- lib/generators/active_translation/install_generator.rb
|
|
95
|
+
- lib/generators/active_translation/templates/README
|
|
96
|
+
- lib/generators/active_translation/templates/active_translation.rb
|
|
97
|
+
- lib/generators/active_translation/templates/create_translations.rb
|
|
98
|
+
- lib/tasks/active_translation_tasks.rake
|
|
99
|
+
homepage: https://github.com/seanhogge/active_translation
|
|
100
|
+
licenses:
|
|
101
|
+
- MIT
|
|
102
|
+
metadata:
|
|
103
|
+
homepage_uri: https://github.com/seanhogge/active_translation
|
|
104
|
+
source_code_uri: https://github.com/seanhogge/active_translation
|
|
105
|
+
changelog_uri: https://github.com/seanhogge/active_translation
|
|
106
|
+
rdoc_options: []
|
|
107
|
+
require_paths:
|
|
108
|
+
- lib
|
|
109
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
110
|
+
requirements:
|
|
111
|
+
- - ">="
|
|
112
|
+
- !ruby/object:Gem::Version
|
|
113
|
+
version: '3.3'
|
|
114
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
115
|
+
requirements:
|
|
116
|
+
- - ">="
|
|
117
|
+
- !ruby/object:Gem::Version
|
|
118
|
+
version: '0'
|
|
119
|
+
requirements: []
|
|
120
|
+
rubygems_version: 3.6.8
|
|
121
|
+
specification_version: 4
|
|
122
|
+
summary: Easily translate specific attributes of any ActiveRecord model
|
|
123
|
+
test_files: []
|