translation_diff 1.0.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.
Files changed (85) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +84 -0
  3. data/.github/workflows/release.yml +28 -0
  4. data/.gitignore +11 -0
  5. data/.rubocop.yml +39 -0
  6. data/.ruby-version +1 -0
  7. data/CHANGELOG.md +723 -0
  8. data/Gemfile +61 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +158 -0
  11. data/Rakefile +41 -0
  12. data/data/languages/azure.json +285 -0
  13. data/data/languages/deepl.json +220 -0
  14. data/data/languages/google.json +399 -0
  15. data/data/languages/modernmt.json +413 -0
  16. data/docs/caching.md +185 -0
  17. data/docs/configuration.md +205 -0
  18. data/docs/contracts.md +187 -0
  19. data/docs/development.md +34 -0
  20. data/docs/errors.md +92 -0
  21. data/docs/how-it-works.md +145 -0
  22. data/docs/instrumentation.md +96 -0
  23. data/docs/languages.md +94 -0
  24. data/docs/providers.md +379 -0
  25. data/docs/sql-cache.md +366 -0
  26. data/lib/generators/translation_diff/install_generator.rb +15 -0
  27. data/lib/generators/translation_diff/templates/create_translation_diff_tables.rb.erb +24 -0
  28. data/lib/translation_diff/active_record/support.rb +34 -0
  29. data/lib/translation_diff/active_record.rb +3 -0
  30. data/lib/translation_diff/batch.rb +98 -0
  31. data/lib/translation_diff/call_preparation.rb +47 -0
  32. data/lib/translation_diff/capabilities.rb +9 -0
  33. data/lib/translation_diff/configuration/cache_guard_options.rb +39 -0
  34. data/lib/translation_diff/configuration/cache_ttl_option.rb +30 -0
  35. data/lib/translation_diff/configuration/option_table.rb +38 -0
  36. data/lib/translation_diff/configuration/provider_option_owners.rb +40 -0
  37. data/lib/translation_diff/configuration.rb +135 -0
  38. data/lib/translation_diff/context.rb +23 -0
  39. data/lib/translation_diff/dispatcher.rb +57 -0
  40. data/lib/translation_diff/document.rb +23 -0
  41. data/lib/translation_diff/errors.rb +44 -0
  42. data/lib/translation_diff/fragment.rb +33 -0
  43. data/lib/translation_diff/http_provider.rb +128 -0
  44. data/lib/translation_diff/instrumentation.rb +25 -0
  45. data/lib/translation_diff/languages/refresh.rb +70 -0
  46. data/lib/translation_diff/languages/set.rb +53 -0
  47. data/lib/translation_diff/languages.rb +30 -0
  48. data/lib/translation_diff/leaves.rb +22 -0
  49. data/lib/translation_diff/markup.rb +85 -0
  50. data/lib/translation_diff/passage.rb +149 -0
  51. data/lib/translation_diff/preview.rb +3 -0
  52. data/lib/translation_diff/previewer.rb +78 -0
  53. data/lib/translation_diff/provider.rb +91 -0
  54. data/lib/translation_diff/providers/amazon.rb +126 -0
  55. data/lib/translation_diff/providers/azure.rb +78 -0
  56. data/lib/translation_diff/providers/deepl.rb +88 -0
  57. data/lib/translation_diff/providers/google.rb +64 -0
  58. data/lib/translation_diff/providers/libretranslate.rb +65 -0
  59. data/lib/translation_diff/providers/modernmt.rb +74 -0
  60. data/lib/translation_diff/providers/null.rb +20 -0
  61. data/lib/translation_diff/providers.rb +69 -0
  62. data/lib/translation_diff/railtie.rb +12 -0
  63. data/lib/translation_diff/rate_limiters/active_record.rb +92 -0
  64. data/lib/translation_diff/rate_limiters/redis.rb +59 -0
  65. data/lib/translation_diff/rate_limiters.rb +9 -0
  66. data/lib/translation_diff/redaction.rb +45 -0
  67. data/lib/translation_diff/registry.rb +31 -0
  68. data/lib/translation_diff/segment.rb +32 -0
  69. data/lib/translation_diff/segmenters/pragmatic.rb +102 -0
  70. data/lib/translation_diff/segmenters/simple.rb +122 -0
  71. data/lib/translation_diff/segmenters.rb +4 -0
  72. data/lib/translation_diff/sentence_cache.rb +76 -0
  73. data/lib/translation_diff/stores/active_record.rb +106 -0
  74. data/lib/translation_diff/stores/memory.rb +34 -0
  75. data/lib/translation_diff/stores/redis.rb +49 -0
  76. data/lib/translation_diff/stores.rb +9 -0
  77. data/lib/translation_diff/tasks/translation_diff.rake +21 -0
  78. data/lib/translation_diff/translation/request.rb +8 -0
  79. data/lib/translation_diff/translation/response.rb +35 -0
  80. data/lib/translation_diff/translation/usage.rb +8 -0
  81. data/lib/translation_diff/translator.rb +103 -0
  82. data/lib/translation_diff/version.rb +3 -0
  83. data/lib/translation_diff.rb +93 -0
  84. data/translation_diff.gemspec +56 -0
  85. metadata +243 -0
data/Gemfile ADDED
@@ -0,0 +1,61 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ # Not runtime dependencies of the gem (see the gemspec) -- Configuration#
6
+ # redis_pool requires them lazily, and Stores::Redis/RateLimiters::Redis
7
+ # duck-type against whatever a caller's connection pool yields. They are
8
+ # only here so the test suite, which builds real pools against these
9
+ # classes, has them available.
10
+ gem "connection_pool", "~> 2.4", require: false
11
+ gem "redis", "~> 5.0", require: false
12
+ gem "redis-namespace", "~> 1.11", require: false
13
+
14
+ # Not a runtime dependency of the gem (see the gemspec) -- RateLimiters::Redis
15
+ # requires it lazily on the first check, so an application that configures no
16
+ # rate limit never needs it installed. It is only here so the test suite,
17
+ # which exercises the limiter against the real Ratelimit class rather than a
18
+ # stand-in, has it available.
19
+ gem "ratelimit", "~> 1.1", require: false
20
+
21
+ # Not a runtime dependency of the gem (see the gemspec) -- lib/ only ever
22
+ # needs CGI.escape, which cgi/escape (in Ruby's default load path) still
23
+ # provides. This is here because the Google provider's test decodes the
24
+ # query string it sent, and Ruby 4.0 removed CGI.parse from the default
25
+ # load path; "install cgi gem" is Ruby's own suggested fix.
26
+ gem "cgi", "~> 0.5", require: false
27
+
28
+ # Not a runtime dependency of the gem (see the gemspec) -- the Amazon
29
+ # provider requires it lazily when it signs its first request, so an
30
+ # application using another provider never needs it installed. It is here so
31
+ # the test suite, which signs against the real library rather than a
32
+ # stand-in, has it available.
33
+ gem "aws-sigv4", "~> 1.12", require: false
34
+
35
+ # Not runtime dependencies of the gem (see the gemspec) -- TranslationDiff::Stores::ActiveRecord
36
+ # and TranslationDiff::RateLimiters::ActiveRecord require active_record lazily on first use, so an
37
+ # application caching in Redis never needs it installed. They are here so the
38
+ # suite can exercise the stores against a real database rather than a stand-in.
39
+ gem "activerecord", "~> 8.1", require: false
40
+ gem "sqlite3", "~> 2.9", require: false
41
+
42
+ # Not a runtime dependency of the gem (see the gemspec) -- only the CI job that
43
+ # sets TRANSLATION_DIFF_DATABASE_URL ever opens a Postgres connection, where
44
+ # the concurrency the SQL store and rate limiter rest on can actually be
45
+ # tested. SQLite has one writer, so most of the suite never needs this gem.
46
+ gem "pg", "~> 1.5", require: false
47
+
48
+ # Not a runtime dependency of the gem (see the gemspec) -- only the CI job that
49
+ # sets TRANSLATION_DIFF_DATABASE_URL to a MySQL database ever opens one, where
50
+ # the missing supports_insert_conflict_target? behaviour actually bites. Trilogy
51
+ # over mysql2: it is a pure Ruby/C socket client with no libmysqlclient headers
52
+ # to install, so it builds on a bare CI runner and on this machine alike.
53
+ gem "trilogy", "~> 2.9", require: false
54
+
55
+ # Not a runtime dependency of the gem (see the gemspec) -- the generator under
56
+ # lib/generators/ is loaded only when Rails loads generators, so a non-Rails
57
+ # application never needs it installed. It is here so
58
+ # test/translation_diff/install_generator_test.rb can load it and check the
59
+ # generated migration against the schema the rest of the suite runs against,
60
+ # instead of skipping itself for want of Rails::Generators::Base.
61
+ gem "railties", "~> 8.1", require: false
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Islam Gagiev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,158 @@
1
+ # TranslationDiff
2
+
3
+ A translation cache that helps translate only changes between revisions of long texts.
4
+
5
+ ## Why TranslationDiff?
6
+
7
+ Assume your project contains a significant amount of products descriptions which:
8
+ - Require retranslation each time user edits them.
9
+ - Have a lot of equal parts (like return policy).
10
+ - Change frequently.
11
+
12
+ If your user changes a single word within the long description, you will be charged for the retranslation of the whole text.
13
+
14
+ Much better approach is to try to translate every repeated structural element (sentence) in your texts array just once to save money. This gem helps to make it done.
15
+
16
+ ## Show me the code
17
+
18
+ ```ruby
19
+ # The simplest translation
20
+ TranslationDiff.configure do |config|
21
+ config.deepl_api_key = ENV["DEEPL_API_KEY"]
22
+ config.redis_url = ENV["REDIS_URL"]
23
+ end
24
+
25
+ TranslationDiff.translate("Привет.", from: "ru", to: "en")
26
+ ```
27
+
28
+ ```ruby
29
+ # Nested structures come back the same shape
30
+ TranslationDiff.translate("test", from: "en", to: "es")
31
+ TranslationDiff.translate(%w[test language], from: "en", to: "es")
32
+ TranslationDiff.translate(
33
+ { title: "test", values: { type: "frequent" } }, from: "en", to: "es"
34
+ )
35
+ ```
36
+
37
+ ```ruby
38
+ # HTML markup is preserved
39
+ TranslationDiff.translate("<b>Black</b>", from: "en", to: "es")
40
+ ```
41
+
42
+ ```ruby
43
+ # class="notranslate" marks a span to protect (provider support varies -- see the caveats below)
44
+ TranslationDiff.translate(
45
+ '<span class="notranslate">Bold Mountain</span> is a good place.', from: "en", to: "ru"
46
+ )
47
+ ```
48
+
49
+ ```ruby
50
+ # Switch provider for one call, without touching the global configuration
51
+ TranslationDiff.translate(blog_post, from: "en", to: "de", provider: :google)
52
+ ```
53
+
54
+ ```ruby
55
+ # Any keyword other than from:, to:, provider:, config: and assume_supported: is forwarded to
56
+ # the provider. assume_supported: is this library's own decision, not a vendor's, so it's
57
+ # reserved rather than forwarded -- it must never reach a payload.
58
+ TranslationDiff.translate(contract, from: "en", to: "de", formality: :more)
59
+ ```
60
+
61
+ ```ruby
62
+ # An isolated context for its own configuration -- global config.deepl_api_key stays untouched
63
+ formal = TranslationDiff.context do |config|
64
+ config.provider = :deepl
65
+ config.deepl_api_key = ENV["DEEPL_AUTH_KEY"]
66
+ end
67
+ formal.translate(contract, from: "en", to: "de", formality: :more)
68
+ ```
69
+
70
+ ```ruby
71
+ # See what a call would send and find cached, without calling the provider or writing anything
72
+ preview = TranslationDiff.preview(contract, from: "en", to: "de")
73
+ preview.sendable_sentences # => sentences not yet cached
74
+ preview.cached_sentences # => sentences already cached
75
+ ```
76
+
77
+ ```ruby
78
+ # Redis-backed cache, shared across processes
79
+ TranslationDiff.configure { |config| config.redis_url = ENV["REDIS_URL"] }
80
+ ```
81
+
82
+ ```ruby
83
+ # A rate limit, in characters per interval
84
+ TranslationDiff.configure { |config| config.rate_limit = 100_000 }
85
+ ```
86
+
87
+ ```ruby
88
+ # Instrumentation, through anything satisfying the ActiveSupport::Notifications interface
89
+ TranslationDiff.configure { |config| config.instrumenter = ActiveSupport::Notifications }
90
+ ```
91
+
92
+ ## Providers
93
+
94
+ Every provider declares what it can do through
95
+ `TranslationDiff::Capabilities` -- there is nowhere else these numbers live,
96
+ so this table is generated from the same source the library reads at
97
+ runtime:
98
+
99
+ | Provider | `config.provider` | Auth option(s) | Batch size | Request size (escaped chars) | HTML support | `notranslate` | Detects language | Reports billing |
100
+ | --- | --- | --- | --- | --- | --- | --- | --- | --- |
101
+ | Null | `:null` | none | 1,000,000 | 1,000,000 | no | no | no | no |
102
+ | DeepL | `:deepl` (default) | `deepl_api_key` | 50 | 1,700 | yes (`tag_handling`) | yes | yes | yes |
103
+ | Google | `:google` | `google_api_key` | 128 | 5,000 | yes (`format`) | yes | yes | no |
104
+ | Azure | `:azure` | `azure_api_key` | 1,000 | 50,000 | yes (`textType`) | yes | yes | yes |
105
+ | ModernMT | `:modernmt` | `modernmt_api_key` | 128 | 5,000 | yes (`format`) | no | yes | yes |
106
+ | LibreTranslate | `:libretranslate` | `libretranslate_api_base` | 50 | 5,000 | yes (`format`) | no | yes | no |
107
+ | Amazon | `:amazon` | `amazon_access_key_id`, `amazon_secret_access_key`, `amazon_region` | 1 | 10,000 | no | no | yes | no |
108
+
109
+ **Amazon:** translates one text per call (no batch form of `TranslateText`) and has no HTML mode, so `notranslate` is not honoured.
110
+
111
+ **LibreTranslate:** preserves markup but does not honour `notranslate` either -- measured against a real instance, not assumed.
112
+
113
+ See [Providers](docs/providers.md) for configuring each one, the full capabilities explanation, and writing your own.
114
+
115
+ ## Features
116
+
117
+ - **Content-hash caching:** a small edit to a long document is billed for the edit, not the whole document
118
+ - **Six built-in providers** -- DeepL, Google Cloud Translation, Azure AI Translator, ModernMT, LibreTranslate, Amazon Translate -- or bring your own by subclassing a small base class
119
+ - **HTML aware:** markup is preserved, and `class="notranslate"` can protect a span (provider support varies -- see the caveats below)
120
+ - **Any shape:** strings, arrays, and deep hashes go in and come back translated in the same shape
121
+ - **Three cache stores:** `Stores::Memory` out of the box, `Stores::Redis` once you configure `redis_url`, `Stores::ActiveRecord` to cache in your own database instead -- see [SQL cache](docs/sql-cache.md)
122
+ - **Isolated contexts:** `TranslationDiff.context` for multi-tenant apps and per-request provider overrides, without touching the global configuration
123
+ - **Pluggable sentence segmenter:** `pragmatic_segmenter` by default, with a zero-dependency `Simple` alternative
124
+ - **HTTP retries, timeouts, and backoff** on every REST-backed provider, via `faraday` and `faraday-retry`
125
+ - **One error hierarchy** under `TranslationDiff::Error`, carrying the provider name and HTTP status
126
+ - **Optional rate limiting and instrumentation** -- credentials and translated content never appear in a log line this gem writes (the SQL cache store is the one exception worth knowing before you adopt it -- see [SQL cache](docs/sql-cache.md#what-ends-up-in-your-log))
127
+
128
+ ## Installation
129
+
130
+ Ruby 3.4 or newer is required.
131
+
132
+ Add this line to your application's Gemfile:
133
+
134
+ ```ruby
135
+ gem 'translation_diff'
136
+ ```
137
+
138
+ And then execute:
139
+
140
+ $ bundle
141
+
142
+ Or install it yourself as:
143
+
144
+ $ gem install translation_diff
145
+
146
+ This gem loads `ox`, `pragmatic_segmenter`, `faraday`, and `faraday-retry` at require time. `aws-sigv4`, `activerecord`, `redis`, `connection_pool`, `redis-namespace`, and `ratelimit` are yours to add, only if you use the feature that needs them -- see [Dependencies](docs/configuration.md#dependencies).
147
+
148
+ ## Documentation
149
+
150
+ [Configuration](docs/configuration.md) · [Providers](docs/providers.md) · [Languages](docs/languages.md) · [Caching](docs/caching.md) · [SQL cache](docs/sql-cache.md) · [Contracts](docs/contracts.md) · [Instrumentation](docs/instrumentation.md) · [Errors](docs/errors.md) · [How it works](docs/how-it-works.md) · [Upgrading & development](docs/development.md)
151
+
152
+ ## Contributing
153
+
154
+ Bug reports and pull requests are welcome on GitHub at https://github.com/Halvanhelv/translation_diff.
155
+
156
+ ## License
157
+
158
+ Released under the [MIT License](LICENSE.txt).
data/Rakefile ADDED
@@ -0,0 +1,41 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ # The dependencies are noisy under -w; their warnings drown out our own.
9
+ t.warning = false
10
+ end
11
+
12
+ task default: :test
13
+
14
+ namespace :languages do
15
+ desc "Re-fetch every provider's language lists from its vendor"
16
+ task :refresh do
17
+ require "translation_diff"
18
+
19
+ not_shipped = TranslationDiff::Languages::NOT_SHIPPED
20
+ unless not_shipped.empty?
21
+ puts "not shipping data for #{not_shipped.join(', ')}: a vendor credential or a private instance " \
22
+ "would make the file unshareable"
23
+ end
24
+
25
+ skip = [:null, *not_shipped]
26
+ providers = TranslationDiff::Providers.names.reject { |name| skip.include?(name) }.map do |name|
27
+ TranslationDiff::Providers.build(name, TranslationDiff.config)
28
+ rescue TranslationDiff::ConfigurationError => e
29
+ warn "skipping #{name}: #{e.message}"
30
+ nil
31
+ end.compact
32
+
33
+ report = TranslationDiff::Languages::Refresh.call(providers: providers)
34
+ puts "updated: #{report[:updated].join(', ')}" unless report[:updated].empty?
35
+ puts "skipped: #{report[:skipped].join(', ')}" unless report[:skipped].empty?
36
+ report[:failed].each { |name, message| warn "failed: #{name}: #{message}" }
37
+ end
38
+ end
39
+
40
+ # The task itself ships in lib/, so a host application's own `rake` can load it too -- this just reuses it here.
41
+ load File.expand_path("lib/translation_diff/tasks/translation_diff.rake", __dir__)
@@ -0,0 +1,285 @@
1
+ {
2
+ "provider": "azure",
3
+ "captured_at": "2026-09-10",
4
+ "endpoint": "https://api.cognitive.microsofttranslator.com/languages?api-version=3.0&scope=translation",
5
+ "source": [
6
+ "af",
7
+ "am",
8
+ "ar",
9
+ "as",
10
+ "az",
11
+ "ba",
12
+ "be",
13
+ "bg",
14
+ "bho",
15
+ "bn",
16
+ "bo",
17
+ "brx",
18
+ "bs",
19
+ "ca",
20
+ "cs",
21
+ "cy",
22
+ "da",
23
+ "de",
24
+ "doi",
25
+ "dsb",
26
+ "dv",
27
+ "el",
28
+ "en",
29
+ "es",
30
+ "es-mx",
31
+ "et",
32
+ "eu",
33
+ "fa",
34
+ "fi",
35
+ "fil",
36
+ "fj",
37
+ "fo",
38
+ "fr",
39
+ "fr-ca",
40
+ "ga",
41
+ "gl",
42
+ "gom",
43
+ "gu",
44
+ "ha",
45
+ "he",
46
+ "hi",
47
+ "hne",
48
+ "hr",
49
+ "hsb",
50
+ "ht",
51
+ "hu",
52
+ "hy",
53
+ "id",
54
+ "ig",
55
+ "ikt",
56
+ "is",
57
+ "it",
58
+ "iu",
59
+ "iu-latn",
60
+ "ja",
61
+ "ka",
62
+ "kk",
63
+ "km",
64
+ "kmr",
65
+ "kn",
66
+ "ko",
67
+ "ks",
68
+ "ku",
69
+ "ky",
70
+ "lb",
71
+ "ln",
72
+ "lo",
73
+ "lt",
74
+ "lug",
75
+ "lv",
76
+ "lzh",
77
+ "mai",
78
+ "mg",
79
+ "mi",
80
+ "mk",
81
+ "ml",
82
+ "mn-cyrl",
83
+ "mn-mong",
84
+ "mni",
85
+ "mr",
86
+ "ms",
87
+ "mt",
88
+ "mww",
89
+ "my",
90
+ "nb",
91
+ "ne",
92
+ "nl",
93
+ "nso",
94
+ "nya",
95
+ "or",
96
+ "otq",
97
+ "pa",
98
+ "pl",
99
+ "prs",
100
+ "ps",
101
+ "pt",
102
+ "pt-pt",
103
+ "ro",
104
+ "ru",
105
+ "run",
106
+ "rw",
107
+ "sd",
108
+ "si",
109
+ "sk",
110
+ "sl",
111
+ "sm",
112
+ "sn",
113
+ "so",
114
+ "sq",
115
+ "sr-cyrl",
116
+ "sr-latn",
117
+ "st",
118
+ "sv",
119
+ "sw",
120
+ "ta",
121
+ "te",
122
+ "th",
123
+ "ti",
124
+ "tk",
125
+ "tlh-latn",
126
+ "tlh-piqd",
127
+ "tn",
128
+ "to",
129
+ "tr",
130
+ "tt",
131
+ "ty",
132
+ "ug",
133
+ "uk",
134
+ "ur",
135
+ "uz",
136
+ "vi",
137
+ "xh",
138
+ "yo",
139
+ "yua",
140
+ "yue",
141
+ "zh-hans",
142
+ "zh-hant",
143
+ "zu"
144
+ ],
145
+ "target": [
146
+ "af",
147
+ "am",
148
+ "ar",
149
+ "as",
150
+ "az",
151
+ "ba",
152
+ "be",
153
+ "bg",
154
+ "bho",
155
+ "bn",
156
+ "bo",
157
+ "brx",
158
+ "bs",
159
+ "ca",
160
+ "cs",
161
+ "cy",
162
+ "da",
163
+ "de",
164
+ "doi",
165
+ "dsb",
166
+ "dv",
167
+ "el",
168
+ "en",
169
+ "es",
170
+ "es-mx",
171
+ "et",
172
+ "eu",
173
+ "fa",
174
+ "fi",
175
+ "fil",
176
+ "fj",
177
+ "fo",
178
+ "fr",
179
+ "fr-ca",
180
+ "ga",
181
+ "gl",
182
+ "gom",
183
+ "gu",
184
+ "ha",
185
+ "he",
186
+ "hi",
187
+ "hne",
188
+ "hr",
189
+ "hsb",
190
+ "ht",
191
+ "hu",
192
+ "hy",
193
+ "id",
194
+ "ig",
195
+ "ikt",
196
+ "is",
197
+ "it",
198
+ "iu",
199
+ "iu-latn",
200
+ "ja",
201
+ "ka",
202
+ "kk",
203
+ "km",
204
+ "kmr",
205
+ "kn",
206
+ "ko",
207
+ "ks",
208
+ "ku",
209
+ "ky",
210
+ "lb",
211
+ "ln",
212
+ "lo",
213
+ "lt",
214
+ "lug",
215
+ "lv",
216
+ "lzh",
217
+ "mai",
218
+ "mg",
219
+ "mi",
220
+ "mk",
221
+ "ml",
222
+ "mn-cyrl",
223
+ "mn-mong",
224
+ "mni",
225
+ "mr",
226
+ "ms",
227
+ "mt",
228
+ "mww",
229
+ "my",
230
+ "nb",
231
+ "ne",
232
+ "nl",
233
+ "nso",
234
+ "nya",
235
+ "or",
236
+ "otq",
237
+ "pa",
238
+ "pl",
239
+ "prs",
240
+ "ps",
241
+ "pt",
242
+ "pt-pt",
243
+ "ro",
244
+ "ru",
245
+ "run",
246
+ "rw",
247
+ "sd",
248
+ "si",
249
+ "sk",
250
+ "sl",
251
+ "sm",
252
+ "sn",
253
+ "so",
254
+ "sq",
255
+ "sr-cyrl",
256
+ "sr-latn",
257
+ "st",
258
+ "sv",
259
+ "sw",
260
+ "ta",
261
+ "te",
262
+ "th",
263
+ "ti",
264
+ "tk",
265
+ "tlh-latn",
266
+ "tlh-piqd",
267
+ "tn",
268
+ "to",
269
+ "tr",
270
+ "tt",
271
+ "ty",
272
+ "ug",
273
+ "uk",
274
+ "ur",
275
+ "uz",
276
+ "vi",
277
+ "xh",
278
+ "yo",
279
+ "yua",
280
+ "yue",
281
+ "zh-hans",
282
+ "zh-hant",
283
+ "zu"
284
+ ]
285
+ }