openapi: 3.1.0
info:
  version: 1.0.0
  title: Stratastax I18n API
  contact:
    name: Contact our support
    email: info@buijs.dev
    url: https://buijs.dev/
  license:
    name: Apache-2.0
    url: https://github.com/buijs-dev/stratastax-contract/blob/main/LICENSE
  description: |
    Translations of Stratastax applications. An application owns a catalog of message keys, imported from the translation files it is built with. In the CMS an administrator writes a draft translation per key and locale and publishes the drafts as a release. A consumer application fetches the published bundle of a locale with a service account (client credentials) and lays it over its built-in translations.
servers:
  - url: https://stratastax.dev/rest
security:
  - bearerAuth: []
tags:
  - name: applications
    description: Applications that are translated, with their locales.
  - name: bundles
    description: Published translations of a locale, read by consumer applications.
  - name: keys
    description: The message keys of an application and their draft translations.
  - name: releases
    description: Publication of the draft translations.
paths:
  /api/v1/applications:
    get:
      summary: List applications
      description: Filterable on `id` and `baseLocale`; sortable on `id`, `displayName` and `createdAt`.
      operationId: listApplications
      security:
        - bearerAuth:
            - stx-i18n:application-read
      tags:
        - applications
      parameters:
        - $ref: '#/components/parameters/query_limit'
        - $ref: '#/components/parameters/query_cursor'
        - $ref: '#/components/parameters/query_filter'
        - $ref: '#/components/parameters/query_sort'
      responses:
        '200':
          $ref: '#/components/responses/application_list'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
    post:
      summary: Create application
      operationId: createApplication
      security:
        - bearerAuth:
            - stx-i18n:application-create
      tags:
        - applications
      parameters:
        - $ref: '#/components/parameters/header_idempotency_key'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/create_application'
      responses:
        '201':
          $ref: '#/components/responses/created_application'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '409':
          $ref: '#/components/responses/conflict_error'
        '422':
          $ref: '#/components/responses/unprocessable_content_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
  /api/v1/applications/{applicationId}:
    parameters:
      - $ref: '#/components/parameters/path_application_id'
    get:
      summary: Get application
      operationId: getApplication
      security:
        - bearerAuth:
            - stx-i18n:application-read
      tags:
        - applications
      responses:
        '200':
          $ref: '#/components/responses/application'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
    patch:
      summary: Update application
      operationId: updateApplication
      security:
        - bearerAuth:
            - stx-i18n:application-update
      tags:
        - applications
      parameters:
        - $ref: '#/components/parameters/header_idempotency_key'
        - $ref: '#/components/parameters/header_if_match'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/update_application'
      responses:
        '200':
          $ref: '#/components/responses/application'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '409':
          $ref: '#/components/responses/conflict_error'
        '412':
          $ref: '#/components/responses/precondition_failed_error'
        '422':
          $ref: '#/components/responses/unprocessable_content_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
  /api/v1/applications/{applicationId}/locales:
    parameters:
      - $ref: '#/components/parameters/path_application_id'
    get:
      summary: List published locales
      description: |
        The locales of the application that have a published bundle, with the release that last changed each of them. Called by consumer applications with a service account.
      operationId: listPublishedLocales
      security:
        - bearerAuth:
            - stx-i18n:bundle-read
      tags:
        - bundles
      responses:
        '200':
          $ref: '#/components/responses/published_locale_list'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
  /api/v1/applications/{applicationId}/bundles/{locale}:
    parameters:
      - $ref: '#/components/parameters/path_application_id'
      - $ref: '#/components/parameters/path_locale'
    get:
      summary: Get published bundle
      description: |
        The published translations of a locale. Called by consumer applications with a service account, at startup and when the locale changes. Pass the ETag of a cached bundle as If-None-Match to get `304 Not Modified` while it is still current. Drafts are never part of a bundle.
      operationId: getBundle
      security:
        - bearerAuth:
            - stx-i18n:bundle-read
      tags:
        - bundles
      parameters:
        - $ref: '#/components/parameters/header_if_none_match'
      responses:
        '200':
          $ref: '#/components/responses/translation_bundle'
        '304':
          $ref: '#/components/responses/not_modified'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
  /api/v1/applications/{applicationId}/keys:
    parameters:
      - $ref: '#/components/parameters/path_application_id'
    get:
      summary: List message keys
      description: |
        Filterable on `key` (`key==match*`), `obsolete` and `status` with the locale (`status.nl==MISSING`); sortable on `key`.
      operationId: listMessageKeys
      security:
        - bearerAuth:
            - stx-i18n:key-read
      tags:
        - keys
      parameters:
        - $ref: '#/components/parameters/query_limit'
        - $ref: '#/components/parameters/query_cursor'
        - $ref: '#/components/parameters/query_filter'
        - $ref: '#/components/parameters/query_sort'
      responses:
        '200':
          $ref: '#/components/responses/message_key_list'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
  /api/v1/applications/{applicationId}/keys/import:
    parameters:
      - $ref: '#/components/parameters/path_application_id'
    post:
      summary: Import key catalog
      description: |
        Replaces the key catalog with the keys, base texts and arguments compiled from the translation files of the application (`stx i18n push`). New keys are added, changed keys are updated and missing keys become obsolete; translations are never deleted.
      operationId: importMessageKeys
      security:
        - bearerAuth:
            - stx-i18n:key-import
      tags:
        - keys
      parameters:
        - $ref: '#/components/parameters/header_idempotency_key'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/import_keys'
      responses:
        '200':
          $ref: '#/components/responses/import_keys_result'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '409':
          $ref: '#/components/responses/conflict_error'
        '422':
          $ref: '#/components/responses/unprocessable_content_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
  /api/v1/applications/{applicationId}/keys/{messageKey}:
    parameters:
      - $ref: '#/components/parameters/path_application_id'
      - $ref: '#/components/parameters/path_message_key'
    get:
      summary: Get message key
      operationId: getMessageKey
      security:
        - bearerAuth:
            - stx-i18n:key-read
      tags:
        - keys
      responses:
        '200':
          $ref: '#/components/responses/message_key'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
  /api/v1/applications/{applicationId}/keys/{messageKey}/translations/{locale}:
    parameters:
      - $ref: '#/components/parameters/path_application_id'
      - $ref: '#/components/parameters/path_message_key'
      - $ref: '#/components/parameters/path_locale'
    put:
      summary: Write draft translation
      description: |
        Sets the draft text of the key in the locale. The text must be valid ICU and may only use the arguments of the key, with a compatible type; otherwise the response is 400 with the offending argument. The draft is published with the next release.
      operationId: updateTranslation
      security:
        - bearerAuth:
            - stx-i18n:translation-update
      tags:
        - keys
      parameters:
        - $ref: '#/components/parameters/header_idempotency_key'
        - $ref: '#/components/parameters/header_if_match'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/update_translation'
      responses:
        '200':
          $ref: '#/components/responses/message_key'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '409':
          $ref: '#/components/responses/conflict_error'
        '412':
          $ref: '#/components/responses/precondition_failed_error'
        '422':
          $ref: '#/components/responses/unprocessable_content_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
    delete:
      summary: Discard draft translation
      description: Discards the draft; the published text, if any, stays.
      operationId: discardTranslation
      security:
        - bearerAuth:
            - stx-i18n:translation-update
      tags:
        - keys
      parameters:
        - $ref: '#/components/parameters/header_idempotency_key'
        - $ref: '#/components/parameters/header_if_match'
      responses:
        '204':
          $ref: '#/components/responses/no_content'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '409':
          $ref: '#/components/responses/conflict_error'
        '412':
          $ref: '#/components/responses/precondition_failed_error'
        '422':
          $ref: '#/components/responses/unprocessable_content_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
  /api/v1/applications/{applicationId}/releases:
    parameters:
      - $ref: '#/components/parameters/path_application_id'
    get:
      summary: List releases
      description: Sortable on `version` and `publishedAt`; newest first by default.
      operationId: listReleases
      security:
        - bearerAuth:
            - stx-i18n:release-read
      tags:
        - releases
      parameters:
        - $ref: '#/components/parameters/query_limit'
        - $ref: '#/components/parameters/query_cursor'
        - $ref: '#/components/parameters/query_sort'
      responses:
        '200':
          $ref: '#/components/responses/release_list'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
    post:
      summary: Publish drafts
      description: |
        Publishes the drafts of the given locales (all locales with drafts when omitted) as a new release; consumer applications get them with their next bundle request. Returns 409 when there is nothing to publish, or when a draft no longer matches the arguments of its key after an import.
      operationId: publishRelease
      security:
        - bearerAuth:
            - stx-i18n:release-publish
      tags:
        - releases
      parameters:
        - $ref: '#/components/parameters/header_idempotency_key'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/create_release'
      responses:
        '201':
          $ref: '#/components/responses/created_release'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '409':
          $ref: '#/components/responses/conflict_error'
        '422':
          $ref: '#/components/responses/unprocessable_content_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
  parameters:
    query_limit:
      name: limit
      in: query
      required: false
      description: Maximum number of items to return
      schema:
        type: integer
        minimum: 1
        maximum: 100
    query_cursor:
      name: cursor
      in: query
      required: false
      description: |
        Opaque cursor from a previous response's nextCursor. Omit to fetch the first page. Clients must treat this value as opaque and only pass back what was previously returned.
      schema:
        type: string
    query_filter:
      name: filter
      in: query
      required: false
      description: |
        RSQL filter expression evaluated against this resource's filterable fields. Combine multiple conditions with `;` (AND) or `,` (OR), e.g. `gameId==3fa85f64-...;playedAt=gt=2026-01-01T00:00:00Z`. The set of filterable fields is documented per endpoint in the API reference; filtering on a field that isn't in that whitelist returns 400 Bad Request.
      schema:
        type: string
    query_sort:
      name: sort
      in: query
      required: false
      description: |
        Sort order. Format: `field:direction`. Direction is ASC or DESC (default ASC if omitted). 
        Use a comma-separated list for multi-column sorting; priority follows the parameter order.
      schema:
        type: string
        pattern: ^([a-zA-Z0-9_]+(:(ASC|DESC))?)(,([a-zA-Z0-9_]+(:(ASC|DESC))?))*$
      examples:
        singleColumn:
          summary: Sort by a single column
          value: playedAt:DESC
        multiColumn:
          summary: Sort by multiple columns
          value: playedAt:DESC,status:ASC
        implicitDirection:
          summary: Omitted direction defaults to ASC
          value: status,playedAt:DESC
    header_idempotency_key:
      name: Idempotency-Key
      in: header
      required: true
      description: |
        Client-generated UUIDv7, required on every mutating request (POST, PUT, PATCH, DELETE), that makes the request safe to retry. A repeated request with the same key and the same payload returns the stored response (marked with the `Idempotent-Replayed: true` header) instead of executing again; the same key with a different payload is refused with 422. Keys older than the retention window (30 days) are rejected. A key that is not a UUID is refused with 400.
      schema:
        type: string
        examples:
          - 0192f100-0000-7000-8000-0000000000a5
    path_application_id:
      name: applicationId
      in: path
      required: true
      description: |
        Identifier of the application, a lowercase slug chosen at creation (`match-tracker`). Consumer applications configure it, so it never changes.
      schema:
        $ref: '#/components/schemas/application_id'
    header_if_match:
      name: If-Match
      in: header
      required: true
      description: |
        ETag of the resource being modified, for optimistic concurrency control: the request is refused with 412 when the resource changed since. Obtain it from the ETag response header of a previous GET or mutation.
      schema:
        type: string
    path_locale:
      name: locale
      in: path
      required: true
      description: BCP 47 language tag of the locale (`nl`, `en-GB`)
      schema:
        $ref: '#/components/schemas/locale'
    header_if_none_match:
      name: If-None-Match
      in: header
      required: false
      description: |
        ETag of a bundle the client already has. When it is still the published version the response is `304 Not Modified` without a body.
      schema:
        type: string
    path_message_key:
      name: messageKey
      in: path
      required: true
      description: Dotted message key (`matchCreateCommand.gameId.label`)
      schema:
        $ref: '#/components/schemas/message_key'
  schemas:
    paged_metadata:
      type: object
      required:
        - limit
      properties:
        limit:
          type: integer
          format: int32
          description: Maximum number of items returned in this page
        prevCursor:
          type:
            - string
            - 'null'
          description: |
            Opaque cursor to pass as the `cursor` query parameter to fetch the previous page. Null when there is no previous results. Clients must not parse or construct this value.
        nextCursor:
          type:
            - string
            - 'null'
          description: |
            Opaque cursor to pass as the `cursor` query parameter to fetch the next page. Null when there are no more results. Clients must not parse or construct this value.
    application_id:
      type: string
      description: Lowercase slug that identifies an application
      pattern: ^[a-z][a-z0-9-]{1,62}$
      examples:
        - match-tracker
    display_name:
      type: string
      minLength: 1
      maxLength: 100
    locale:
      type: string
      description: BCP 47 language tag (language, optionally a script and a region)
      pattern: ^[a-z]{2,3}(-[A-Z][a-z]{3})?(-([A-Z]{2}|[0-9]{3}))?$
      examples:
        - nl
        - en-GB
        - zh-Hant-TW
        - es-419
    date_time:
      description: date-time (ISO 8601)
      type: string
      format: date-time
    application:
      type: object
      required:
        - id
        - displayName
        - baseLocale
        - locales
        - createdAt
      properties:
        id:
          $ref: '#/components/schemas/application_id'
        displayName:
          $ref: '#/components/schemas/display_name'
        baseLocale:
          $ref: '#/components/schemas/locale'
        locales:
          type: array
          description: Every locale of the application, the base locale first
          items:
            $ref: '#/components/schemas/locale'
        latestRelease:
          type:
            - integer
            - 'null'
          format: int32
          description: Version of the latest release; null before the first publication
        createdAt:
          $ref: '#/components/schemas/date_time'
    application_list:
      type: object
      allOf:
        - $ref: '#/components/schemas/paged_metadata'
        - type: object
          required:
            - items
          properties:
            items:
              type: array
              description: Applications in this page
              items:
                $ref: '#/components/schemas/application'
    error_type:
      type: string
      description: |
        Canonical error type identifier (URI). The values are the problem types of stratastax-http's problem-core; stratastax-codegen fails the build of a service when the two disagree.
      enum:
        - https://errors.stratastax.dev/authentication
        - https://errors.stratastax.dev/authorization
        - https://errors.stratastax.dev/validation
        - https://errors.stratastax.dev/not-found
        - https://errors.stratastax.dev/conflict
        - https://errors.stratastax.dev/precondition-failed
        - https://errors.stratastax.dev/internal
    problem_detail:
      type: object
      required:
        - type
        - title
        - status
        - detail
      properties:
        type:
          $ref: '#/components/schemas/error_type'
        title:
          type: string
          description: Short, human-readable summary
        status:
          type: integer
          format: int32
          description: HTTP status code
        detail:
          type: string
          description: Human-readable explanation
        instance:
          type: string
          format: uri
          description: URI of the request that caused the error
    validation_error:
      type: object
      required:
        - field
        - code
        - message
      properties:
        field:
          type: string
          description: Field or property that failed validation
        code:
          type: string
          description: |
            Machine-readable validation error code, e.g. REQUIRED, VALUE_TOO_LONG, INVALID_FORMAT, OUT_OF_RANGE. Canonical list is maintained per resource in the documentation.
        message:
          type: string
          description: Human readable validation message
        rejectedValue:
          description: The actual value that failed validation. Omitted if the value is sensitive.
    validation_problem_detail:
      allOf:
        - $ref: '#/components/schemas/problem_detail'
        - type: object
          properties:
            errors:
              type: array
              items:
                $ref: '#/components/schemas/validation_error'
    create_application:
      type: object
      required:
        - id
        - displayName
        - baseLocale
      properties:
        id:
          $ref: '#/components/schemas/application_id'
        displayName:
          $ref: '#/components/schemas/display_name'
        baseLocale:
          $ref: '#/components/schemas/locale'
        locales:
          type: array
          description: Further locales; the base locale is always included
          items:
            $ref: '#/components/schemas/locale'
    update_application:
      type: object
      description: |
        Every field is optional. A locale that is removed keeps its translations, but is no longer published.
      properties:
        displayName:
          $ref: '#/components/schemas/display_name'
        locales:
          type: array
          items:
            $ref: '#/components/schemas/locale'
    published_locale:
      type: object
      required:
        - locale
        - version
        - publishedAt
        - messageCount
      properties:
        locale:
          $ref: '#/components/schemas/locale'
        version:
          type: integer
          format: int32
          description: Release that last changed this locale
        publishedAt:
          $ref: '#/components/schemas/date_time'
        messageCount:
          type: integer
          format: int32
          description: Number of keys with a published translation
    published_locale_list:
      type: object
      required:
        - items
      properties:
        items:
          type: array
          description: The locales with a published bundle
          items:
            $ref: '#/components/schemas/published_locale'
    message_text:
      type: string
      description: |
        Text of a message in ICU MessageFormat: plain text, `{name}`, `{n, number}`, `{d, date, short}`, `{n, plural, one {...} other {...}}` and `{x, select, a {...} other {...}}`. It may only use the arguments of its key.
      maxLength: 4000
      examples:
        - '{count, plural, =0 {Nog geen spelers} one {# speler} other {# spelers}}'
    translation_bundle:
      type: object
      description: |
        The published translations of one locale. Holds only the keys that have a published translation; a consumer keeps its built-in text for every other key.
      required:
        - application
        - locale
        - version
        - publishedAt
        - messages
      properties:
        application:
          $ref: '#/components/schemas/application_id'
        locale:
          $ref: '#/components/schemas/locale'
        version:
          type: integer
          format: int32
          description: Release that published this bundle
        publishedAt:
          $ref: '#/components/schemas/date_time'
        messages:
          type: object
          description: Message key to text
          additionalProperties:
            $ref: '#/components/schemas/message_text'
    message_key:
      type: string
      description: Dotted message key, as in the translation files of the application
      pattern: ^[A-Za-z0-9_]+(\.[A-Za-z0-9_]+)*$
      maxLength: 200
      examples:
        - matchCreateCommand.gameId.label
    message_argument_type:
      type: string
      description: |
        Type of a message argument: ANY (formatted with toString), NUMBER (number, plural and `#`), DATE (date and time), STRING (select).
      enum:
        - ANY
        - NUMBER
        - DATE
        - STRING
    message_argument:
      type: object
      required:
        - name
        - type
      properties:
        name:
          type: string
          pattern: ^[A-Za-z][A-Za-z0-9_]*$
        type:
          $ref: '#/components/schemas/message_argument_type'
    translation_status:
      type: string
      description: |
        Status of a translation: MISSING (no text yet), DRAFT (a text that is not published), PUBLISHED (the published text, without a newer draft), CHANGED (published, with a newer draft).
      enum:
        - MISSING
        - DRAFT
        - PUBLISHED
        - CHANGED
    user_ref:
      type: object
      required:
        - id
        - displayName
      properties:
        id:
          type: string
          format: uuid
        displayName:
          type: string
    translation:
      type: object
      required:
        - locale
        - status
      properties:
        locale:
          $ref: '#/components/schemas/locale'
        status:
          $ref: '#/components/schemas/translation_status'
        draft:
          description: Text that is published with the next release; absent without a draft
          $ref: '#/components/schemas/message_text'
        published:
          description: Text of the latest release; absent before the key is published
          $ref: '#/components/schemas/message_text'
        updatedAt:
          $ref: '#/components/schemas/date_time'
        updatedBy:
          $ref: '#/components/schemas/user_ref'
    message_key_detail:
      type: object
      description: A message key with its base text and the translation per locale
      required:
        - key
        - baseText
        - arguments
        - obsolete
        - translations
      properties:
        key:
          $ref: '#/components/schemas/message_key'
        baseText:
          description: Text in the base locale, from the last import
          $ref: '#/components/schemas/message_text'
        description:
          type: string
          description: Context for translators, from the last import
        arguments:
          type: array
          description: Arguments a translation may use
          items:
            $ref: '#/components/schemas/message_argument'
        obsolete:
          type: boolean
          description: |
            The last import no longer contained this key. An obsolete key is not published.
        translations:
          type: array
          items:
            $ref: '#/components/schemas/translation'
    message_key_list:
      type: object
      allOf:
        - $ref: '#/components/schemas/paged_metadata'
        - type: object
          required:
            - items
          properties:
            items:
              type: array
              description: Message keys in this page
              items:
                $ref: '#/components/schemas/message_key_detail'
    key_catalog_entry:
      type: object
      required:
        - key
        - baseText
      properties:
        key:
          $ref: '#/components/schemas/message_key'
        baseText:
          $ref: '#/components/schemas/message_text'
        description:
          type: string
          maxLength: 1000
        arguments:
          type: array
          items:
            $ref: '#/components/schemas/message_argument'
    import_keys:
      type: object
      description: |
        The complete key catalog of the application in its base locale, as compiled from its translation files. Keys that are not in the catalog become obsolete.
      required:
        - baseLocale
        - keys
      properties:
        baseLocale:
          $ref: '#/components/schemas/locale'
        keys:
          type: array
          maxItems: 10000
          items:
            $ref: '#/components/schemas/key_catalog_entry'
    import_keys_result:
      type: object
      required:
        - added
        - updated
        - obsoleted
        - unchanged
      properties:
        added:
          type: integer
          format: int32
          description: New keys
        updated:
          type: integer
          format: int32
          description: Keys with a new base text, description or arguments
        obsoleted:
          type: integer
          format: int32
          description: Keys that were not in the catalog and are now obsolete
        unchanged:
          type: integer
          format: int32
        invalidated:
          type: array
          description: |
            Translations whose arguments no longer match their key after the import. They are kept, but cannot be published until they are fixed.
          items:
            type: object
            required:
              - key
              - locale
            properties:
              key:
                $ref: '#/components/schemas/message_key'
              locale:
                $ref: '#/components/schemas/locale'
    update_translation:
      type: object
      required:
        - text
      properties:
        text:
          $ref: '#/components/schemas/message_text'
    release:
      type: object
      required:
        - version
        - locales
        - messageCount
        - publishedAt
        - publishedBy
      properties:
        version:
          type: integer
          format: int32
          description: Sequence number of the release, starting at 1
        locales:
          type: array
          description: Locales whose drafts this release published
          items:
            $ref: '#/components/schemas/locale'
        messageCount:
          type: integer
          format: int32
          description: Number of translations this release changed
        note:
          type: string
        publishedAt:
          $ref: '#/components/schemas/date_time'
        publishedBy:
          $ref: '#/components/schemas/user_ref'
    release_list:
      type: object
      allOf:
        - $ref: '#/components/schemas/paged_metadata'
        - type: object
          required:
            - items
          properties:
            items:
              type: array
              description: Releases in this page, newest first by default
              items:
                $ref: '#/components/schemas/release'
    create_release:
      type: object
      properties:
        locales:
          type: array
          description: Locales to publish; all locales with drafts when omitted
          items:
            $ref: '#/components/schemas/locale'
        note:
          type: string
          maxLength: 500
  responses:
    application_list:
      description: List of applications
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/application_list'
    validation_error:
      description: Request validation failed
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/validation_problem_detail'
          examples:
            bad_request:
              value:
                type: https://errors.stratastax.dev/validation
                title: Bad Request
                status: 400
                detail: Received invalid request data
                errors:
                  - field: displayName
                    code: REQUIRED
                    message: missing required field
            duplicate:
              value:
                type: https://errors.stratastax.dev/validation
                title: Bad Request
                status: 400
                detail: Received invalid request data
                errors:
                  - field: participants[2].ghostPlayerId
                    code: DUPLICATE_GHOST_PLAYER
                    message: the ghost player already takes part in this match
            filter_field_not_allowed:
              value:
                type: https://errors.stratastax.dev/validation
                title: Bad Request
                status: 400
                detail: Request validation failed
                errors:
                  - field: query
                    code: INVALID_SEARCH
                    message: '''nonsense'' is not a filterable field'
    unauthorized_error:
      description: Authentication required or token invalid
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/problem_detail'
          examples:
            authentication:
              value:
                type: https://errors.stratastax.dev/authentication
                title: Unauthorized
                status: 401
                detail: Authentication required
    forbidden_error:
      description: Insufficient permissions
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/problem_detail'
          examples:
            forbidden:
              value:
                type: https://errors.stratastax.dev/authorization
                title: Access denied
                status: 403
                detail: Insufficient permissions
    internal_server_error:
      description: Unexpected server error
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/problem_detail'
          examples:
            internal:
              value:
                type: https://errors.stratastax.dev/internal
                title: Oops! Something went wrong.
                status: 500
                detail: Try again later.
    created_application:
      description: Application created, or the stored response of an earlier request with the same Idempotency-Key
      headers:
        Location:
          $ref: '#/components/headers/location'
        ETag:
          $ref: '#/components/headers/etag'
        Idempotent-Replayed:
          $ref: '#/components/headers/idempotent_replayed'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/application'
    conflict_error:
      description: |
        The requested mutation is not allowed in the resource's current state, or it conflicts with existing data (a unique value that is already taken)
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/problem_detail'
          examples:
            already_exists:
              value:
                type: https://errors.stratastax.dev/conflict
                title: Already exists
                status: 409
                detail: A game with source bgg:199792 already exists.
            self_ghost_player:
              value:
                type: https://errors.stratastax.dev/conflict
                title: Self ghost player
                status: 409
                detail: The ghost player that represents yourself cannot be deleted or unclaimed.
            finalized_match:
              value:
                type: https://errors.stratastax.dev/conflict
                title: Match is finalized
                status: 409
                detail: This match is FINALIZED; propose a change via POST /matches/{matchId}/edits instead.
    unprocessable_content_error:
      description: The Idempotency-Key was already used for a request with a different payload
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/validation_problem_detail'
          examples:
            idempotency_key_reused:
              value:
                type: https://errors.stratastax.dev/validation
                title: Unprocessable Content
                status: 422
                detail: Idempotency key reused for a different request
                errors:
                  - field: Idempotency-Key
                    code: IDEMPOTENCY_KEY_REUSED
                    message: 0192f100-0000-7000-8000-0000000000a5 was already used for a different request
    application:
      description: Application retrieved or changed successfully
      headers:
        ETag:
          $ref: '#/components/headers/etag'
        Idempotent-Replayed:
          $ref: '#/components/headers/idempotent_replayed'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/application'
    not_found_error:
      description: Resource not found
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/problem_detail'
          examples:
            not_found:
              value:
                type: https://errors.stratastax.dev/not-found
                title: Resource not found
                status: 404
                detail: The requested resource could not be found
    precondition_failed_error:
      description: The If-Match header did not match the current resource's ETag
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/problem_detail'
          examples:
            precondition_failed:
              value:
                type: https://errors.stratastax.dev/precondition-failed
                title: Resource has changed
                status: 412
                detail: The resource was modified since you last fetched it. Reload and try again.
    published_locale_list:
      description: The locales with a published bundle
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/published_locale_list'
    translation_bundle:
      description: The published translations of the locale
      headers:
        ETag:
          schema:
            type: string
          description: Version of the bundle, use as If-None-Match on a subsequent GET
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/translation_bundle'
    not_modified:
      description: The bundle of the If-None-Match header is still the published version
      headers:
        ETag:
          schema:
            type: string
          description: Version of the bundle
    message_key_list:
      description: List of message keys
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/message_key_list'
    import_keys_result:
      description: Catalog imported, or the stored response of an earlier request with the same Idempotency-Key
      headers:
        Idempotent-Replayed:
          $ref: '#/components/headers/idempotent_replayed'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/import_keys_result'
    message_key:
      description: Message key retrieved or changed successfully
      headers:
        ETag:
          $ref: '#/components/headers/etag'
        Idempotent-Replayed:
          $ref: '#/components/headers/idempotent_replayed'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/message_key_detail'
    no_content:
      description: Done; there is no content to return
      headers:
        Idempotent-Replayed:
          $ref: '#/components/headers/idempotent_replayed'
    release_list:
      description: List of releases
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/release_list'
    created_release:
      description: Release published, or the stored response of an earlier request with the same Idempotency-Key
      headers:
        Location:
          $ref: '#/components/headers/location'
        Idempotent-Replayed:
          $ref: '#/components/headers/idempotent_replayed'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/release'
  headers:
    location:
      description: URL of the created resource
      schema:
        type: string
        format: uri-reference
    etag:
      description: Current version of the resource; send it as If-Match to change or delete it
      schema:
        type: string
    idempotent_replayed:
      description: |
        Present (`true`) when the response was replayed from an earlier request with the same Idempotency-Key, instead of being executed again
      schema:
        type: string
        enum:
          - 'true'
