Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Api Platform Translation Bundle Laravel Package

tunet/api-platform-translation-bundle

A Symfony bundle that adds multilingual translation support to API Platform resources, helping you expose localized field values through REST and GraphQL APIs with Doctrine integration and configurable fallbacks.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a Symfony bundle (not Laravel) — it targets Symfony 6/7 + API Platform 3 + Doctrine ORM. If you're coming from Laravel, treat it as the API Platform equivalent of dimsav/laravel-translatable / Spatie-style translation tables: one base entity, one *Translation entity per locale, joined by a OneToMany.

Minimum setup:

composer require tunet/api-platform-translation-bundle
// config/bundles.php
Tunet\ApiPlatformTranslationBundle\ApiPlatformTranslationBundle::class => ['all' => true],
# config/packages/tunet_api_platform_translation.yaml
tunet_api_platform_translation:
    locales: ['en', 'fr', 'de']
    default_locale: en
    fallback_locale: en

Where to look first:

  1. The TranslatableInterface / TranslationInterface contracts shipped in the bundle — these mirror Sylius. Your entity implements the former, your *Translation entity implements the latter.
  2. The TranslatableTrait / TranslationTrait — copy their property layout (typically $translations collection + $currentLocale, and $id, $locale, $translatable on the translation side).
  3. The LocaleProvider service — this is what decides which translation gets serialized on read. Check it before wiring anything else; it drives most of the "why is the wrong language coming back" issues.

First use case — the canonical one is a Product with a ProductTranslation that holds name and description. You keep price, sku, createdAt on the parent (locale-agnostic) and move everything human-readable into the translation entity.

Implementation Patterns

Entity shape. Parent holds scalars that don't vary per locale; translation holds the localised text. Give the parent a getTranslations() / addTranslation() / removeTranslation() and a getTranslation(string $locale = null) helper returning the requested one (or the current locale, or the fallback).

#[ApiResource(
    normalizationContext: ['groups' => ['product:read']],
    denormalizationContext: ['groups' => ['product:write']],
)]
class Product implements TranslatableInterface
{
    use TranslatableTrait;

    #[ORM\OneToMany(targetEntity: ProductTranslation::class, mappedBy: 'translatable', cascade: ['persist', 'remove'])]
    protected Collection $translations;
}

#[ApiResource(
    uriTemplate: '/products/{id}/translations',
    operations: [new Get(), new GetCollection(), new Put(), new Delete()],
)]
class ProductTranslation implements TranslationInterface { /* name, description, locale, translatable */ }

Read pattern. Clients ask for a language via Accept-Language (preferred) or ?locale=fr. The bundle's locale provider resolves it and the serializer emits the matching translation, flattened so name/description appear at the product level — the client never sees the join. Document this in your OpenAPI description; consumers will be confused otherwise.

Write pattern. You generally have two working styles:

  • Embedded write — accept {"name": ..., "description": ...} on POST /products with the locale from the header, and have a state processor create the ProductTranslation. This is the ergonomic API and what you want for admin UIs.
  • Subresource write — POST /products/{id}/translations with an explicit locale field. Verbose but ideal for bulk import / translation tooling.

Support at least one explicitly; mixing the two silently is the single biggest source of confusion.

Serializer groups. Keep product:read and product:write separate, and put name/description in both — but only emit the flattened fields via the locale-aware normalizer. translations collection itself should only be exposed in an admin group; otherwise you leak every locale and defeat the whole point.

Validation. Use Symfony's Valid constraint on the $translations collection, and NotBlank on the translation's name. The UniqueEntity on (translatable_id, locale) matters — the bundle will let you POST two en translations otherwise.

Integration with API Platform state processors. If you need to derive slug from the localised name, do it in a Processor that reads $request->attributes->get('locale') — the bundle sets this before your processor runs.

Gotchas and Tips

  • It is not a Laravel package. No config/translatable.php, no Eloquent magic. If you're actually on Laravel with api-platform/laravel, expect to wire the Doctrine mapping yourself; the bundle's service definitions assume the Symfony container.
  • Zero users, zero stars. Treat any version as pre-1.0. Pin to an exact tag in composer.json, and write integration tests around the read/write flows you depend on before shipping. Don't assume semantic versioning guarantees.
  • Locale resolution order is the #1 debug target. When the wrong language comes back, dump what the provider resolved. The typical culprit is an Accept-Language header that API Platform's _locale negotiation picked up before your bundle did — or a caching layer stripping the header entirely.
  • Fallback-only-if-missing. If fr translation exists but name is empty, you usually want fallback to en, not an empty string. The bundle follows Sylius's getTranslation() semantics which return the entity, not a merged copy — do the field-level fallback in a custom normalizer if you need it.
  • Doctrine cascade. cascade: ['persist', 'remove'] on $translations is required; without remove, deleting a product leaves orphan translations and Doctrine throws a FK violation on a fresh migration.
  • Serialization group bleed. If you expose both the flattened name and the full translations collection in the same group, JSON will contain both — and clients will build features around the wrong one. Be strict: flattened only, or collection only.
  • Extension point worth knowing: the locale provider. Replace it with a LocaleProviderInterface implementation that reads from a custom header (e.g. X-Store-Locale) if you're multi-tenant or building a store-switcher. This is cleaner than fighting Accept-Language.
  • CurrentLocale is request-scoped. If you spawn async work (messenger, queue) mid-request and read a translatable, the locale may be lost. Re-hydrate it explicitly from a job argument.
  • Missing migrations on the translation table are silent until the first serialization of a Product. Run doctrine:migrations:diff immediately after wiring the entity; the bundle ships no migrations of its own.
  • Debugging tip: bin/console debug:container tunet_api_platform_translation (or grep services.xml in the bundle) to see exactly which services are registered and their tags — faster than reasoning from the README, which is thin.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle