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

Sulu Translated Media Bundle Laravel Package

alengo/sulu-translated-media-bundle

SEO-friendly translated media filenames for Sulu CMS 3.x. Serves uploads under locale-specific names like /uploads/red-shoes-de.jpg while storing originals unchanged. Adds an Additional Data admin tab, Twig URL helpers with WebP, and format cache warming; zero-config.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Steps to First Use

  1. Check compatibility before installing. As of 3.2.0 the bundle targets Sulu 3.1 / Symfony 8.1. If your project is still on Sulu 2.x, pin to the older release line; if you are upgrading an existing install, bump Sulu/Symfony first, then the bundle.

    composer require alengo/sulu-translated-media-bundle:^3.2
    
  2. Register the bundle in config/bundles.php:

    Alengo\SuluTranslatedMediaBundle\TranslatedMediaBundle::class => ['all' => true],
    
  3. Import the admin API routes in config/routes/sulu_admin.yaml:

    TranslatedMediaBundle:
        resource: "@TranslatedMediaBundle/Resources/config/routing_admin_api.yaml"
        prefix: /admin/api
    
  4. Update the schema (the bundle adds the media-translation table and joins):

    bin/adminconsole doctrine:schema:update --force
    
  5. First media translation

    • Upload a media file in the Sulu admin.
    • Open the "Additional Data" tab (auto-registered) on the media item.
    • Set seoFilename (e.g. red-shoes-de), title, and description for a locale (e.g. de).
    • Save.
  6. First Twig usage

    {{ sulu_translated_media_url(media, '800x', app.request.locale) }}
    

    The rendered URL should reflect the translated filename (e.g. /uploads/red-shoes-de.jpg). If you still see the original filename, the translation was not persisted — check the Additional Data tab again before debugging anything else.

Where to Look First

  • Admin UI: The "Additional Data" tab under any media item is the day-to-day surface for translations.
  • Twig functions: sulu_translated_media_url() and sulu_translated_media_urls() are the frontend entry points.
  • Entities: Entity\Media and Entity\MediaTranslations define the data model; extend these for project-specific fields.
  • Compiler pass / TranslatedFormatManager: replaces Sulu's default FormatManager. Start here when URLs come out wrong or you need custom filename logic.
  • composer.json of the bundle: on this release, verify the Sulu/Symfony constraints before filing any bug that looks like "service not found" or "method signature mismatch" — those usually mean a version skew, not a bundle bug.

First Use Case: Localized Product Images

  1. Upload a product image (red-shoes.jpg) to Sulu.
  2. In Additional Data, set seoFilename to red-shoes-de, title to Rote Schuhe, description to Bequeme Herbstschuhe for de.
  3. In the product template:
    <img src="{{ sulu_translated_media_url(productImage, '600x', app.request.locale) }}"
         alt="{{ productImage.title[app.request.locale] }}">
    
    German visitors get /uploads/red-shoes-de.jpg plus localized alt text; other locales fall back to the original filename.

Implementation Patterns

Core Workflow: Managing Translated Media

  1. Upload — use Sulu's standard media workflow; the bundle adds no steps.
  2. Translate metadata — in Additional Data, per locale set:
    • seoFilename: URL-safe filename (e.g. product-guide-fr).
    • title: human-readable name (e.g. Guide du Produit).
    • description: SEO description.
    • Flags: verifyDownload (gated content), aiGenerated (metadata provenance).
  3. Render URLs
    {# single #}
    {{ sulu_translated_media_url(media, '400x', 'de') }}
    
    {# responsive #}
    {% set urls = sulu_translated_media_urls(media, '800x', app.request.locale) %}
    <picture>
        <source srcset="{{ urls.webp }}" type="image/webp">
        <img src="{{ urls.default }}" alt="{{ media.title[app.request.locale] }}">
    </picture>
    
  4. Fallbacks — a missing translation falls back to the original filename. Override by decorating/replacing TranslatedFormatManager (see below).

Integration Tips

1. Extending the Media entity

// src/Entity/CustomMedia.php
use Alengo\SuluTranslatedMediaBundle\Model\MediaAdditionalDataTrait;
use Alengo\SuluTranslatedMediaBundle\Model\MediaTranslationsTrait;

class CustomMedia extends \Alengo\SuluTranslatedMediaBundle\Entity\Media
{
    use MediaTranslationsTrait;
    use MediaAdditionalDataTrait;

    private ?string $customField = null;
    // getters/setters...
}
# config/packages/sulu_media.yaml
sulu_media:
    objects:
        media:
            model: App\Entity\CustomMedia

When you swap the model, re-run the schema diff — the bundle's table is joined to me_media, and a stale schema is the number one cause of "translation saved but not shown".

2. Customizing the admin tab

The Additional Data tab is auto-registered; adjust its presentation via sulu_admin navigation config rather than editing the bundle's templates.

3. Overriding the TranslatedFormatManager

// src/DependencyInjection/Compiler/TranslatedFormatManagerPass.php
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

class TranslatedFormatManagerPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container): void
    {
        $definition = $container->findDefinition(\Alengo\SuluTranslatedMediaBundle\Format\TranslatedFormatManager::class);
        $definition->setClass(\App\Format\CustomTranslatedFormatManager::class);
    }
}

Register it in Kernel::build(). Prefer a decorator (decorates: in services.yaml) over setClass() — it survives refactors of the bundle's service ID and keeps the original manager injectable.

4. Bulk updating translations

Wire an importer and write straight to the media-translation table, reusing Sulu's Data Importer config. Validate that seoFilename values stay unique per locale — the bundle does not enforce uniqueness for you.

5. Caching translated URLs

class CachedTranslatedMediaUrlGenerator
{
    public function __construct(
        private TranslatedFormatManager $manager,
        private CacheInterface $cache
    ) {}

    public function generate(string $mediaId, string $format, string $locale): string
    {
        return $this->cache->get(
            "media_url_{$mediaId}_{$format}_{$locale}",
            fn () => $this->manager->generate($mediaId, $format, $locale)
        );
    }
}

Expose it as sulu_cached_translated_media_url, and invalidate on save (Doctrine listener on the translation entity) — otherwise edited slugs stay stale until the cache expires.

Common Patterns

Pattern Example
Locale-aware URLs sulu_translated_media_url(media, '400x', app.request.locale)
Responsive images sulu_translated_media_urls() + <picture>
Fallback handling missing translations fall back to the original filename
Admin metadata seoFilename / title / description per locale
Custom entities extend Entity\Media for project fields
Cache optimization cache generated URLs, invalidate on translation save

Gotchas and Tips

Pitfalls

  1. Version skew after this release. 3.2.0 adds Sulu 3.1 / Symfony 8.1 support. The most common failure after a partial upgrade is a container build error or a signature mismatch in TranslatedFormatManager because the bundle resolved against a different Sulu minor. Fix: bump Sulu/Symfony and the bundle together, then bin/adminconsole cache:clear (not just the website cache).

  2. Schema not updated after upgrade. The compiler pass replaces Sulu's FormatManager at container build time, so a stale schema surfaces as an empty translation rather than an error. Run doctrine:schema:update --force (or your migrations) after every bundle bump.

  3. Missing locale translations. An empty seoFilename silently falls back to the original filename — nothing is logged. Fix: always set seoFilename for every target locale; audit rows where it is null/empty before going live.

  4. seoFilename is not sanitized for you. Spaces, umlauts, or slashes produce URLs that look fine in the admin but break in <img src> or when a CDN re-encodes the path. Normalize on save (listener or admin form constraint) rather than at render time.

  5. Asset cache and CDN invalidation. Renaming a translated file changes the URL, so old URLs die and new ones are cold. If you front Sulu's /uploads with a CDN, purge on translation save; if you don't, expect 404s on cached pages until they are re-rendered.

  6. Route prefix collisions. The admin API route is registered under /admin/api; if another bundle claims the same path, the Additional Data tab loads but its XHRs 404. Check bin/adminconsole debug:router | grep translated first.

  7. Twig alt/title access. media.title[app.request.locale] returns null (not the fallback) when a translation is missing — the fallback applies to URLs, not to arbitrary property access. Guard with |default(media.title.default) or a translated_media_title() helper.

  8. Overriding the format manager. setClass() drops the original constructor arguments unless you reset them; a decorator avoids this entirely. After any override, verify both the admin preview and the frontend render — they resolve the manager through different paths.

  9. Debugging checklist for "wrong filename". In order: (a) is the translation actually persisted (me_media_translations row for that locale)? (b) is the schema up to date? (c) is the container using your entity (sulu_media.objects.media.model)? (d) is the cache warm/stale? (e) only then look at TranslatedFormatManager.

Tips

  • Keep a small admin-only command that lists media missing a translation for a given locale — it turns "why is this page English on the German site?" into a one-liner.
  • Use aiGenerated on translations produced by importers/LLMs so editors can filter and review them later.
  • Prefer sulu_translated_media_urls() over hand-building <picture> sources; format names must match Sulu's configured formats or you get an exception at render time.
  • Pin the bundle version in composer.json alongside your Sulu minor so the compatibility matrix in this section stays meaningful.
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.
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
adachsoft/php-code-style