Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
5441cce
feat: add the value objects the provider contract is built from
Halvanhelv Sep 9, 2026
6631ef7
feat: give providers a base class and require it in the registry
Halvanhelv Sep 9, 2026
6d0d117
fix: narrow the require-bridge rescue to InvalidProviderError
Halvanhelv Sep 9, 2026
1b89880
feat: own the HTTP transport instead of borrowing a vendor's
Halvanhelv Sep 9, 2026
6cf94fa
fix: decode JSON responses ourselves instead of via faraday's middleware
Halvanhelv Sep 9, 2026
5a94e13
refactor!: talk to DeepL directly instead of through deepl-rb
Halvanhelv Sep 9, 2026
e02b682
refactor!: talk to Google directly instead of through its SDK
Halvanhelv Sep 9, 2026
56fea23
fix: capture the request env before Faraday's test adapter mutates it
Halvanhelv Sep 9, 2026
eabdc6c
fix: describe what the option-collision test now guards
Halvanhelv Sep 9, 2026
d58c7a9
feat: add an Azure Translator provider
Halvanhelv Sep 9, 2026
0244b7d
feat: add a ModernMT provider
Halvanhelv Sep 9, 2026
aa27d96
feat: add a LibreTranslate provider
Halvanhelv Sep 9, 2026
7886a48
refactor: migrate DeepL and Google tests onto the shared stub helper
Halvanhelv Sep 9, 2026
90b0120
feat: add an Amazon Translate provider
Halvanhelv Sep 9, 2026
af7c7fd
feat!: read limits and abilities from provider capabilities
Halvanhelv Sep 9, 2026
bff01a7
Compress multi-line comments to a single line
Halvanhelv Sep 9, 2026
4a3ebeb
Drop frozen_string_literal magic comments, require Ruby 3.4
Halvanhelv Sep 9, 2026
fe3d928
fix: reject a non-String translation in Response.build
Halvanhelv Sep 9, 2026
9d717af
fix: stop Cache#store consuming the array it is handed
Halvanhelv Sep 9, 2026
41cd8b9
fix: restore the environment-variable credential fallbacks
Halvanhelv Sep 9, 2026
984fbfc
fix: refuse a non-Provider however it was supplied
Halvanhelv Sep 9, 2026
8c75718
fix: normalise language codes in every provider, once
Halvanhelv Sep 9, 2026
1ba0ead
fix: one convention for billed characters across the three that repor…
Halvanhelv Sep 9, 2026
b0ec2a1
fix: one option-merge order for all six providers
Halvanhelv Sep 9, 2026
a8f282f
test: pin every part of the cache key against main
Halvanhelv Sep 9, 2026
2fac011
ci: test only the Ruby versions the gemspec supports
Halvanhelv Sep 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
strategy:
fail-fast: false
matrix:
ruby: ["3.2", "3.3", "3.4", "4.0"]
ruby: ["3.4", "4.0"]

steps:
- uses: actions/checkout@v4
Expand Down
7 changes: 6 additions & 1 deletion .rubocop.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
AllCops:
TargetRubyVersion: 3.2
TargetRubyVersion: 3.4
NewCops: enable

Style/Documentation:
Expand All @@ -21,6 +21,11 @@ Layout/LineLength:
Gemspec/DevelopmentDependencies:
EnforcedStyle: gemspec

# Ruby 3.4 chills string literals in files without this comment, and the
# supported floor is now 3.4.
Style/FrozenStringLiteralComment:
Enabled: false

# Request takes its collaborators as keyword arguments -- values, from:, to:,
# provider:, config: and the provider's own options. The confusion this cop
# guards against is positional: six named arguments at a call site read
Expand Down
91 changes: 82 additions & 9 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,46 @@ described below. Everything here is relative to `deepl_diff` 2.2.0.
configured above 600 seconds is enforced over 600 seconds instead -- up to
six times more eager than the configuration reads. Keep `rate_interval`
within that range, or expect a tighter effective window than configured.
- Providers must inherit `TranslationDiff::Provider`. A duck-typed object is
no longer accepted: the base class supplies the transport, the
configuration check and the capability defaults, and a provider without
them is a provider that fails in the ways this library has already been
bitten by twice.
- `provider.translate(texts, from:, to:, **options)` is now
`provider.translate(request)`, taking a `Translation::Request` and
returning a `Translation::Response`. The response carries the detected
source language and, where the provider reports it, the characters billed.
- `max_request_size` and `max_batch_size` move from provider methods to
`Capabilities`.
- `TranslationDiff::Providers::Naming` is gone; the registry stamps
`cache_key` and `Provider` implements it.
- Every provider normalises language codes to the casing its own vendor
documents and accepts either casing from the caller: DeepL upper-cases, the
other five lower-case. A code carrying a script or region subtag
(`"zh-Hans"`, `"pt-BR"`) is passed through untouched. `Provider#language`
is the shared rule and `self.language_case` selects the casing, so a
provider of your own gets it by inheriting. Previously only Google and
DeepL normalised at all -- Amazon Translate rejected `"EN"`/`"RU"` and
LibreTranslate answered 400, on every call, for anyone who followed the
README's "switch provider by changing `config.provider`" with DeepL-style
codes -- and DeepL upper-cased subtags too, corrupting `"zh-Hans"`.
- `deepl-rb` and `google-cloud-translate-v2` are no longer used at all.
`faraday` and `faraday-retry` become runtime dependencies; `aws-sigv4` is
required lazily by the Amazon provider only.
- `config.deepl_host` is renamed `config.deepl_api_base`, matching the
`<provider>_api_base` name every other provider uses. There is no alias: a
configuration still setting `deepl_host` raises `NoMethodError` on
`TranslationDiff.configure`. Rename it.
- A provider returning the wrong number of translations now raises
`TranslationDiff::ResponseError`, not `TranslationDiff::Request::Error`.
`Request::Error` still exists, and still means "`from:` is missing and the
provider cannot detect"; a `rescue TranslationDiff::Request::Error` written
to catch a short response no longer catches one. Both are
`TranslationDiff::Error`, so a rescue of the base class is unaffected.
- A provider returning a well-formed response that carries no translation for
one input -- Azure answers 200 for a batch where a single string failed --
also raises `TranslationDiff::ResponseError`, naming the position. It
previously reached `Spacing.restore` and died there as `NoMethodError`.

### Removed

Expand All @@ -86,15 +126,29 @@ described below. Everything here is relative to `deepl_diff` 2.2.0.
### Added

- A Google provider: `config.provider = :google` translates through Cloud
Translation v2 (Basic), on the `google-cloud-translate-v2` gem, required
lazily so an application using DeepL never needs it installed. It declares
`google_api_key` and `google_project_id`; an API key alone is enough, and
with none configured the gem reads `TRANSLATE_KEY`/`GOOGLE_CLOUD_KEY` or
falls back to application default credentials. The provider asks for
`format: :html`, which the tokenizer's output requires -- a `notranslate`
span is handed over with its tags -- and downcases bare language codes so
a configuration written for DeepL (`"EN"`) keeps working, leaving
subtagged codes such as `"zh-Hans"` alone.
Translation v2 (Basic) over HTTP directly, with no Google gem installed. It
declares `google_api_key`, `google_project_id` and `google_api_base`; an API
key alone is enough. With no key configured it reads `TRANSLATE_KEY` and
then `GOOGLE_CLOUD_KEY`, and `google_project_id` falls back to
`TRANSLATE_PROJECT` -- the variables `google-cloud-translate-v2` used to
read on your behalf. Application default credentials are **not** supported:
that path lived in the gem that is gone, and an application relying on ADC
must now configure an API key. The provider asks for `format: :html`, which
the tokenizer's output requires -- a `notranslate` span is handed over with
its tags -- and downcases bare language codes so a configuration written for
DeepL (`"EN"`) keeps working, leaving subtagged codes such as `"zh-Hans"`
alone.
- Environment-variable credential fallbacks, read by this library now that the
vendor SDKs that read them are gone: `DEEPL_AUTH_KEY` for `deepl_api_key`,
`TRANSLATE_KEY` then `GOOGLE_CLOUD_KEY` for `google_api_key`, and
`TRANSLATE_PROJECT` for `google_project_id`. Each is read on use rather than
at load, so setting one after requiring the gem still works, and an
explicitly configured value always wins. The Amazon provider deliberately
has no environment fallback: `aws-sigv4` is handed explicit credentials and
this library does not implement the AWS credential chain.
- A provider declares a default for one of its options by writing
`key => default` in `configuration_options` instead of a bare symbol; a
callable default is evaluated on every read.
- `TranslationDiff::Configuration`, a declarative settings object built
through the `option(key, default)` macro. Options fall back to their
default until assigned, treat a blank string as unset, and support a
Expand Down Expand Up @@ -181,6 +235,22 @@ described below. Everything here is relative to `deepl_diff` 2.2.0.
told apart by an argument's value. A provider with no `detect` makes
`from:` required and raises a clear error when it is missing, instead of
`NoMethodError`.
- Four new providers: `TranslationDiff::Providers::Azure` (`:azure`),
`TranslationDiff::Providers::ModernMT` (`:modernmt`),
`TranslationDiff::Providers::LibreTranslate` (`:libretranslate`), and
`TranslationDiff::Providers::Amazon` (`:amazon`), Amazon Translate, signed
with `aws-sigv4` rather than headed. Every provider's limits, HTML
support, `notranslate` handling, detection and billing reporting are
declared through `Capabilities` and measured against the vendor rather
than assumed -- see the provider table in the README.
- An error hierarchy for everything a provider's transport can do wrong:
`TranslationDiff::ConfigurationError`, `TranslationDiff::ProviderError`
(and its `AuthenticationError`, `QuotaExceededError`,
`InvalidRequestError`, `ServiceError` and `RateLimitError` subclasses),
`TranslationDiff::TransportError`, `TranslationDiff::ResponseError` and
`TranslationDiff::InvalidProviderError`, all under `TranslationDiff::Error`.
- `config.open_timeout`, `config.timeout` and `config.max_retries`, read by
every HTTP provider's connection and retry policy.

### Changed

Expand Down Expand Up @@ -218,6 +288,9 @@ described below. Everything here is relative to `deepl_diff` 2.2.0.
first check, and raises `TranslationDiff::Error` naming the gem to add
when it is missing. Previously the bare constant surfaced a raw
`NameError` instead of the message the Redis path already raises.
- DeepL's batch limit was declared as 300 sentences per request; DeepL
documents 50. The request-size limit (1,700 escaped characters) was
already correct and is unchanged.

## [2.2.0] - 2026-09-07

Expand Down
27 changes: 13 additions & 14 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -1,15 +1,7 @@
# frozen_string_literal: true

source "https://rubygems.org"

gemspec

# Not a runtime dependency of the gem (see the gemspec) -- the DeepL
# provider requires it lazily at build time. It is only here so the test
# suite, which exercises that provider against the real deepl-rb objects,
# has it available.
gem "deepl-rb", "~> 3.9", require: false

# Not runtime dependencies of the gem (see the gemspec) -- Configuration#
# redis_pool requires them lazily, and RedisCacheStore/RedisRateLimiter
# duck-type against whatever a caller's connection pool yields. They are
Expand All @@ -26,9 +18,16 @@ gem "redis-namespace", "~> 1.11", require: false
# stand-in, has it available.
gem "ratelimit", "~> 1.1", require: false

# Not a runtime dependency of the gem (see the gemspec) -- the Google
# provider requires it lazily at build time, so an application using DeepL
# never needs it installed. It is only here so the test suite, which
# exercises that provider's build path against the real
# Google::Cloud::Translate::V2 objects, has it available.
gem "google-cloud-translate-v2", "~> 1.2", require: false
# Not a runtime dependency of the gem (see the gemspec) -- lib/ only ever
# needs CGI.escape, which cgi/escape (in Ruby's default load path) still
# provides. This is here because the Google provider's test decodes the
# query string it sent, and Ruby 4.0 removed CGI.parse from the default
# load path; "install cgi gem" is Ruby's own suggested fix.
gem "cgi", "~> 0.5", require: false

# Not a runtime dependency of the gem (see the gemspec) -- the Amazon
# provider requires it lazily when it signs its first request, so an
# application using another provider never needs it installed. It is here so
# the test suite, which signs against the real library rather than a
# stand-in, has it available.
gem "aws-sigv4", "~> 1.12", require: false
Loading
Loading