Skip to main content

Overview

Rails GraphQL provides comprehensive internationalization (i18n) support for documenting your API. While field and type names cannot be translated, all descriptions can be provided in multiple languages using Rails’ I18n system.
Performance Impact: I18n is a heavy process. Enable only when delivering API documentation or in development mode.

Basic Concept

YAML-based translations for GraphQL components:
en:
  graphql:
    field:
      query:
        hello: "Returns a greeting message"
    object:
      user: "Represents a user in the system"
Translations are retrieved during introspection or when generating documentation.

I18n Scopes

The gem uses configurable scopes to find translations:
# From docs/guides/i18n.md - default configuration
config.i18n_scopes = [
  'graphql.%{namespace}.%{kind}.%{parent}.%{name}',
  'graphql.%{namespace}.%{kind}.%{name}',
  'graphql.%{namespace}.%{name}',
  'graphql.%{kind}.%{parent}.%{name}',
  'graphql.%{kind}.%{name}',
  'graphql.%{name}',
]

Scope Variables

namespace
string
The namespace of the component (e.g., base, admin, public)
kind
string
Component type: scalar, object, interface, union, enum, input_object, schema, field, argument, or directive
parent
string
Parent component name (e.g., type name for fields, field name for arguments)
name
string
Symbolized component name

Translation Examples

Schema Translation

Given this schema:
# app/graphql/sample_schema.rb
module GraphQL
  class SampleSchema < GraphQL::Schema
    namespace :sample
    
    object 'User' do
      field(:name)
    end
    
    query_fields do
      field(:hello) { argument(:world) }
    end
  end
end

Schema Description

Scope order checked:
[
  "graphql.sample.schema.schema",
  "graphql.sample.schema",
  "graphql.schema.schema",
  "graphql.schema",
]
en:
  graphql:
    sample:
      schema: "Sample API Schema"

Field Description

Variables: namespace: "sample", kind: "field", parent: "query", name: "hello" Scope order:
[
  "graphql.sample.field.query.hello",
  "graphql.sample.field.hello",
  "graphql.sample.hello",
  "graphql.field.query.hello",
  "graphql.field.hello",
  "graphql.hello",
]
en:
  graphql:
    sample:
      field:
        query:
          hello: "Returns a hello message"

Argument Description

Variables: namespace: "sample", kind: "argument", parent: "hello", name: "world" Scope order:
[
  "graphql.sample.argument.hello.world",
  "graphql.sample.argument.world",
  "graphql.sample.world",
  "graphql.argument.hello.world",
  "graphql.argument.world",
  "graphql.world",
]
en:
  graphql:
    argument:
      hello:
        world: "The world to greet"

Object Description

Variables: namespace: "sample", kind: "object", parent: nil, name: "user" Scope order:
[
  "graphql.sample.object.user",
  "graphql.sample.user",
  "graphql.object.user",
  "graphql.user",
]
en:
  graphql:
    sample:
      object:
        user: "A user in the system"
      field:
        user:
          name: "User's full name"

Object Field Description

Variables: namespace: "sample", kind: "field", parent: "user", name: "name" Scope order:
[
  "graphql.sample.field.user.name",
  "graphql.sample.field.name",
  "graphql.sample.name",
  "graphql.field.user.name",
  "graphql.field.name",
  "graphql.name",
]

Complete Example

module GraphQL
  class BlogSchema < GraphQL::Schema
    namespace :blog
    
    object 'Post' do
      field :id, :id
      field :title, :string
      field :body, :string
      field :published, :boolean
    end
    
    query_fields do
      field(:posts, 'Post', array: true) do
        argument :published, :boolean, null: true
      end
      
      field(:post, 'Post') do
        argument :id, :id, null: false
      end
    end
  end
end

Dynamic Translations

Use interpolation for dynamic content:
en:
  graphql:
    field:
      user:
        posts: "Posts by %{user_name}"

Namespace-Specific Translations

Separate translations for different API versions:
en:
  graphql:
    v1:
      field:
        query:
          users: "List users (deprecated)"
    
    v2:
      field:
        query:
          users: "List users with pagination"

Configuration

Custom Scopes

Modify the scope resolution order:
# config/initializers/graphql.rb
Rails::GraphQL.config.i18n_scopes = [
  'api.graphql.%{namespace}.%{kind}.%{name}',
  'api.graphql.%{kind}.%{name}',
  'api.graphql.%{name}',
]

Conditional Enabling

Enable i18n only when needed:
# config/initializers/graphql.rb
Rails::GraphQL.config.enable_i18n = Rails.env.development? || ENV['GRAPHQL_DOCS']

Collision Handling

Non-string values are skipped:
# This will be skipped
en:
  graphql:
    field:
      user:
        metadata:
          key: "value"  # Hash, not String
If a scope returns anything other than a plain String, that key is skipped and the next scope is checked.

Best Practices

Organize by Namespace

Keep namespace translations in separate files:
config/locales/
  graphql/
    en/
      base.yml
      admin.yml
      public.yml

Use Fallbacks

Structure translations from specific to general:
en:
  graphql:
    # Specific
    admin:
      field:
        query:
          users: "Admin user list"
    
    # General fallback
    field:
      query:
        users: "User list"

Document in YAML

Use YAML for documentation instead of code:
# Instead of this:
field(:name, :string, desc: "User's name")

# Do this:
field(:name, :string)
# Description in config/locales/graphql/en.yml

Test Translations

Verify translations are loaded:
I18n.with_locale(:es) do
  description = GraphQL::BlogSchema[:query][:posts].description
  assert_equal "Listar todas las publicaciones", description
end

Performance Optimization

Cache Translations

Cache description lookups:
def description(locale = I18n.locale)
  @descriptions ||= {}
  @descriptions[locale] ||= I18n.t(i18n_key, default: @description)
end

Eager Load

Preload translations for introspection:
# Before introspection
I18n.backend.eager_load!

Type Map

How types are resolved for i18n

Introspection

Using i18n with introspection

Build docs developers (and LLMs) love