Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
* [#2960](https://github.com/ruby-grape/grape/pull/2960): Lint every response of an API once under `lint!`, including the router's 404 and mounted Rack apps, instead of inside each endpoint's stack - [@ericproulx](https://github.com/ericproulx).
* [#2966](https://github.com/ruby-grape/grape/pull/2966): Require Ruby 3.3.1, the first 3.3 release that parses anonymous argument forwarding from inside a block - [@ericproulx](https://github.com/ericproulx).
* [#3008](https://github.com/ruby-grape/grape/pull/3008): Add the grape-on-rack app as a live-server memory profiling harness - [@ericproulx](https://github.com/ericproulx).
* [#3019](https://github.com/ruby-grape/grape/pull/3019): Add the `eager_load_i18n` setting to load I18n translations when an API is compiled - [@ericproulx](https://github.com/ericproulx).
* [#3025](https://github.com/ruby-grape/grape/pull/3025): Add `:md` and `:markdown` formatters that render an object's `to_markdown`, for APIs that declare a `text/markdown` content type - [@ericproulx](https://github.com/ericproulx).
* [#3029](https://github.com/ruby-grape/grape/pull/3029): Add a built-in `:jsonl` (JSON Lines) format that streams an Enumerator body - [@ericproulx](https://github.com/ericproulx).
* Your contribution here.
Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ Twitter::API.compile!

This can be added to your `config.ru` (if using rackup), `application.rb` (if using rails), or any file that loads your server.

Outside Rails, compiling can also load I18n's translations. See [I18n](#i18n).

### Rack

The above sample creates a Rack application that can be run from a rackup `config.ru` file with `rackup`:
Expand Down Expand Up @@ -582,6 +584,7 @@ Currently the configurable settings are:
* `param_builder`: Sets the [Parameter Builder](#parameters), defaults to `:hash_with_indifferent_access`.
* `lint`: Checks every response against Rack's specification, defaults to `false`. See [Linting](#linting).
* `warn_on_helper_overrides`: Warns when a helper method masks a `Grape::Endpoint` instance method, defaults to `false`.
* `eager_load_i18n`: Loads I18n's translations when an API is compiled rather than on the first lookup, usually the first error response, defaults to `false`. Rails already does this when `config.eager_load` is on. See [I18n](#i18n).
* `raise_rendering_errors`: Lets an error response that cannot be rendered propagate out of the middleware stack instead of being answered with a failsafe `500`, defaults to `false`. See [When the error response itself cannot be rendered](#when-the-error-response-itself-cannot-be-rendered).

To change a setting value make sure that at some point during load time the following code runs
Expand Down Expand Up @@ -1999,6 +2002,18 @@ Grape supports I18n for parameter-related error messages, but will fallback to E

In case your app enforces available locales only and :en is not included in your available locales, Grape cannot fall back to English and will return the translation key for the error message. To avoid this behaviour, either provide a translation for your default locale or add :en to your available locales.

I18n reads its translations on the first lookup, which is usually the first error response a process answers. In Rails, the application loads them at boot whenever `config.eager_load` is on, as it is in production, so there is nothing to set. Elsewhere, turn on `eager_load_i18n` to have compiling an API load them, and compile at boot:

```ruby
Grape.configure do |config|
config.eager_load_i18n = true
end

Twitter::API.compile!
```

Call `compile!` after your own locale files are on `I18n.load_path`: a file appended later (`I18n.load_path << path`) is read only after `I18n.reload!`, while assigning the path (`I18n.load_path += [path]`) reloads the translations at once.

Custom validators that inherit from `Grape::Validations::Validators::Base` have access to a `translate` helper (see `Grape::Util::Translation`) and should use it instead of calling `I18n` directly. It applies the same `:en` fallback as built-in validators, defaults `scope` to `'grape.errors.messages'`, and handles interpolation without needing `format`:

```ruby
Expand Down
4 changes: 4 additions & 0 deletions benchmark/grape_on_rack/config.ru
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ Dir[File.expand_path('api/*.rb', __dir__)].each { |file| require file }
require_relative 'app/api'
require_relative 'app/acme_app'

Grape.configure do |config|
config.eager_load_i18n = true
end

Acme::API.compile!

run Acme::App.instance
5 changes: 5 additions & 0 deletions lib/grape.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ module Grape
# Let an error response that cannot be rendered propagate out of the
# middleware stack instead of being answered with a failsafe 500.
setting :raise_rendering_errors, default: false
# Load I18n's translations when an API is compiled instead of on the first
# lookup, usually the first error response. Off by default: Rails, where
# Grape is most often mounted, loads them itself when config.eager_load is
# on and keeps them lazy otherwise.
setting :eager_load_i18n, default: false

# The HTTP QUERY method (RFC 10008): a safe, idempotent request whose content
# carries the query. Rack has no constant for it yet, hence the literal.
Expand Down
10 changes: 10 additions & 0 deletions lib/grape/api/instance.rb
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ def inherited(subclass)
# Builds the routes from the defined endpoints, effectively compiling
# this API into a usable form.
def initialize
eager_load_i18n if Grape.config[:eager_load_i18n]
@router = Router.new
add_head_not_allowed_methods_and_options_methods
self.class.endpoints.each do |endpoint|
Expand Down Expand Up @@ -157,6 +158,15 @@ def cascade?

private

# I18n reads its files on the first lookup, which would be the first
# error a process answers. A backend that does not inherit
# I18n::Backend::Base, or one from an older i18n, may have no
# +eager_load!+, and is left to load on that first lookup.
def eager_load_i18n
backend = I18n.backend
backend.eager_load! if backend.respond_to?(:eager_load!)
end

# For every resource add a 'OPTIONS' route that returns an HTTP 204 response
# with a list of HTTP methods that can be called. Also add a route that
# will return an HTTP 405 response for any HTTP method that the resource
Expand Down
60 changes: 60 additions & 0 deletions spec/grape/api_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,66 @@

let(:app) { subject }

describe 'loading translations' do
around do |example|
backend = I18n.backend
I18n.backend = I18n::Backend::Simple.new
example.run
ensure
I18n.backend = backend
end

it 'leaves them to the first lookup by default' do
subject.get('/x') { 'x' }
subject.compile!

expect(I18n.backend).not_to be_initialized
end

context 'when Grape.config.eager_load_i18n is on' do
around do |example|
Grape.config.eager_load_i18n = true
example.run
ensure
Grape.config.eager_load_i18n = false
end

it 'reads them when the API is compiled rather than on the first error' do
subject.get('/x') { 'x' }
expect(I18n.backend).not_to be_initialized

subject.compile!

expect(I18n.backend).to be_initialized
end

it 'still answers the first error in the message it is translated to' do
subject.params { requires :id }
subject.get('/x') { 'x' }

get '/x'

expect(last_response.status).to eq 400
expect(last_response.body).to eq 'id is missing'
end

it 'answers through a backend that cannot load eagerly' do
simple = I18n.backend
I18n.backend = Class.new do
define_method(:translate) { |*args, **options| simple.translate(*args, **options) }
define_method(:available_locales) { simple.available_locales }
end.new
subject.params { requires :id }
subject.get('/x') { 'x' }

get '/x'

expect(last_response.status).to eq 400
expect(last_response.body).to eq 'id is missing'
end
end
end

describe 'thread-safe (re)compilation' do
# Regression: building the HEAD/OPTIONS/405 helper routes used to delete the
# root-prefix/versioning keys from the shared class-level settings and
Expand Down
Loading