Localisation & Translations
The bundle leverages the Symfony Translation component to provide full internationalization (i18n) support for your Progressive Web App. This allows you to create multilingual PWAs that automatically adapt to the user's language preferences.
Overview
The PWA Bundle automatically translates manifest content based on the user's locale. When you provide text values in your configuration (names, descriptions, labels, etc.), they are treated as translation keys that are resolved using Symfony's translation system.
Key benefits:
Automatic manifest localization per user locale
Support for all Symfony translation formats (YAML, XLIFF, PHP, JSON)
Separate manifest files per locale (e.g.,
site.en.webmanifest,site.fr.webmanifest)Seamless integration with Symfony's translation workflow
Full Unicode support for all languages
RTL (Right-to-Left) language support
Translation domain: All PWA translations use the pwa domain by default.
How Translation Works
When you configure your PWA manifest:
Text values become translation keys: Values like
"app.name"are translation keys, not literal textBundle checks for translations: The bundle looks for translations in the
pwadomainLocale-specific manifests generated: A separate manifest is created for each enabled locale
Automatic locale detection: The
pwa()Twig function automatically serves the correct manifest based onapp.request.locale
pwa:
manifest:
enabled: true
name: "app.name" # Translation key, not literal text!
short_name: "app.short_name" # Translation key
description: "app.description" # Translation keyapp:
name: "My Awesome App"
short_name: "AwesomeApp"
description: "The best productivity app"app:
name: "Mon Application Géniale"
short_name: "AppGéniale"
description: "La meilleure application de productivité"Translatable Properties
The following properties are automatically translated by the bundle:
Manifest
name, short_name, description
Screenshot
label
Share Target
title, text
Shortcut
name, short_name, description
Widget
name, short_name, description
Not Translatable: The categories property should not be translated. This capability is deprecated and will be removed in version 2.0.0.
Breaking Change: The start_url property cannot be translated since version 1.3.0. See the migration guide for details.
Basic Configuration
Step 1: Enable Locales in Symfony
First, configure the locales your app supports:
framework:
enabled_locales: ['en', 'fr', 'de', 'es']
default_locale: 'en'
translator:
default_path: '%kernel.project_dir%/translations'
fallbacks: ['en']See the Symfony Translation documentation for more information about configuring locales.
Step 2: Configure Locale Placeholder
Add the {locale} placeholder to your manifest public URL:
pwa:
manifest:
enabled: true
public_url: "/site.{locale}.webmanifest"
name: "app.name"
short_name: "app.short_name"
description: "app.description"Step 3: Use the pwa() Twig Function
The pwa() Twig function automatically serves the correct localized manifest:
Automatic Detection: Since version 1.3.0, you don't need to pass the locale to pwa(). It's automatically detected from app.request.locale.
Step 4: Create Translation Files
Create translation files for each locale in the pwa domain:
app:
name: "TaskMaster Pro"
short_name: "TaskMaster"
description: "Professional task management for teams"app:
name: "TaskMaster Pro"
short_name: "TaskMaster"
description: "Gestion professionnelle des tâches pour équipes"app:
name: "TaskMaster Pro"
short_name: "TaskMaster"
description: "Professionelles Aufgabenmanagement für Teams"Translation File Formats
The PWA Bundle supports all Symfony translation formats.
YAML Format (Recommended)
YAML is human-readable and easy to edit:
app:
name: "My App"
short_name: "MyApp"
description: "A great application"
shortcuts:
dashboard:
name: "Dashboard"
description: "View your dashboard"
settings:
name: "Settings"
description: "Manage settings"
screenshots:
home: "Home screen with features"
dashboard: "Analytics dashboard"XLIFF Format
XLIFF is ideal for professional translation workflows:
<?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
<file source-language="en" target-language="en" datatype="plaintext" original="file.ext">
<header>
<tool tool-id="symfony" tool-name="Symfony"/>
</header>
<body>
<trans-unit id="app.name" resname="app.name">
<source>app.name</source>
<target>TaskMaster Pro</target>
</trans-unit>
<trans-unit id="app.short_name" resname="app.short_name">
<source>app.short_name</source>
<target>TaskMaster</target>
</trans-unit>
<trans-unit id="app.description" resname="app.description">
<source>app.description</source>
<target>Professional task management</target>
</trans-unit>
</body>
</file>
</xliff>PHP Format
PHP format allows for dynamic translations:
<?php
return [
'app' => [
'name' => 'TaskMaster Pro',
'short_name' => 'TaskMaster',
'description' => 'Professional task management',
],
'shortcuts' => [
'dashboard' => [
'name' => 'Dashboard',
'description' => 'View your dashboard',
],
],
];JSON Format
JSON is compatible with JavaScript tooling:
{
"app.name": "TaskMaster Pro",
"app.short_name": "TaskMaster",
"app.description": "Professional task management",
"shortcuts.dashboard.name": "Dashboard",
"shortcuts.dashboard.description": "View your dashboard"
}Complete Use Cases
Use Case 1: Multilingual E-Commerce App
pwa:
manifest:
enabled: true
public_url: "/manifest.{locale}.webmanifest"
name: "shop.name"
short_name: "shop.short_name"
description: "shop.description"
screenshots:
- src: "screenshots/home-en.png"
label: "shop.screenshots.home"
- src: "screenshots/products-en.png"
label: "shop.screenshots.products"
- src: "screenshots/cart-en.png"
label: "shop.screenshots.cart"
shortcuts:
- name: "shop.shortcuts.cart.name"
short_name: "shop.shortcuts.cart.short"
description: "shop.shortcuts.cart.description"
url: "/cart"
icons:
- src: "icons/cart.png"
sizes: [96]
- name: "shop.shortcuts.orders.name"
short_name: "shop.shortcuts.orders.short"
description: "shop.shortcuts.orders.description"
url: "/orders"
icons:
- src: "icons/orders.png"
sizes: [96]shop:
name: "ShopMaster"
short_name: "Shop"
description: "Your favorite online shopping destination"
screenshots:
home: "Browse thousands of products"
products: "Detailed product information"
cart: "Easy checkout process"
shortcuts:
cart:
name: "Shopping Cart"
short: "Cart"
description: "View your cart"
orders:
name: "My Orders"
short: "Orders"
description: "Track your orders"shop:
name: "ShopMaster"
short_name: "Shop"
description: "Votre destination shopping en ligne préférée"
screenshots:
home: "Parcourez des milliers de produits"
products: "Informations détaillées sur les produits"
cart: "Processus de paiement facile"
shortcuts:
cart:
name: "Panier"
short: "Panier"
description: "Voir votre panier"
orders:
name: "Mes commandes"
short: "Commandes"
description: "Suivez vos commandes"shop:
name: "ShopMaster"
short_name: "Shop"
description: "Ihr bevorzugtes Online-Shopping-Ziel"
screenshots:
home: "Durchsuchen Sie Tausende von Produkten"
products: "Detaillierte Produktinformationen"
cart: "Einfacher Checkout-Prozess"
shortcuts:
cart:
name: "Warenkorb"
short: "Korb"
description: "Warenkorb anzeigen"
orders:
name: "Meine Bestellungen"
short: "Bestellungen"
description: "Bestellungen verfolgen"Use Case 2: Global News App with RTL Support
framework:
enabled_locales: ['en', 'ar', 'he', 'fr', 'es']
default_locale: 'en'pwa:
manifest:
enabled: true
public_url: "/news.{locale}.webmanifest"
name: "news.app.name"
short_name: "news.app.short"
description: "news.app.description"
shortcuts:
- name: "news.shortcuts.breaking.name"
url: "/breaking"
icons:
- src: "icons/breaking.png"
sizes: [96]news:
app:
name: "Global News"
short: "News"
description: "Breaking news from around the world"
shortcuts:
breaking:
name: "Breaking News"news:
app:
name: "الأخبار العالمية"
short: "أخبار"
description: "آخر الأخبار من جميع أنحاء العالم"
shortcuts:
breaking:
name: "الأخبار العاجلة"news:
app:
name: "חדשות עולמיות"
short: "חדשות"
description: "חדשות מהירות מכל העולם"
shortcuts:
breaking:
name: "חדשות מהירות"RTL Support: The bundle automatically sets the dir property based on the locale. For RTL languages like Arabic (ar) and Hebrew (he), the direction is set to "rtl" automatically.
Use Case 3: SaaS Platform with Region-Specific Content
pwa:
manifest:
enabled: true
public_url: "/app.{locale}.webmanifest"
name: "saas.name"
short_name: "saas.short"
description: "saas.description"
screenshots:
- src: "screenshots/dashboard-{locale}.png"
label: "saas.screenshots.dashboard"
- src: "screenshots/analytics-{locale}.png"
label: "saas.screenshots.analytics"Note: The {locale} placeholder in screenshot paths allows for locale-specific screenshots.
Generated Manifest Files
When you configure translations with the {locale} placeholder, the bundle generates separate manifest files for each enabled locale.
Example with locales ['en', 'fr', 'de']:
public/
├── site.en.webmanifest
├── site.fr.webmanifest
└── site.de.webmanifestsite.en.webmanifest:
{
"name": "TaskMaster Pro",
"short_name": "TaskMaster",
"description": "Professional task management",
"lang": "en",
"dir": "ltr",
...
}site.fr.webmanifest:
{
"name": "TaskMaster Pro",
"short_name": "TaskMaster",
"description": "Gestion professionnelle des tâches",
"lang": "fr",
"dir": "ltr",
...
}site.de.webmanifest:
{
"name": "TaskMaster Pro",
"short_name": "TaskMaster",
"description": "Professionelles Aufgabenmanagement",
"lang": "de",
"dir": "ltr",
...
}Translation Workflow
1. Extract Translation Keys
Use Symfony's translation extraction command to find untranslated keys:
# Extract all translation keys to YAML files
php bin/console translation:extract --force en --domain=pwa
# Extract to XLIFF format
php bin/console translation:extract --force --format=xlf en --domain=pwa
# Extract for multiple locales
php bin/console translation:extract --force en --domain=pwa
php bin/console translation:extract --force fr --domain=pwa
php bin/console translation:extract --force de --domain=pwa2. Translate Content
Option A: Manual Translation
Edit YAML/XLIFF files directly
Use your preferred text editor
Option B: Professional Translation
Export XLIFF files
Send to translation service
Import translated XLIFF files
Option C: Translation Management Platform
Use platforms like Crowdin, Lokalise, or Phrase
Import/export via API
Collaborative translation workflow
3. Validate Translations
Check for missing translations:
# List missing translations
php bin/console translation:extract --dump-messages en --domain=pwa
# Check all locales
php bin/console debug:translation --domain=pwa4. Clear Cache
After updating translations, clear the cache:
# Clear cache
php bin/console cache:clear
# Warm up cache
php bin/console cache:warmupTesting Translations
1. Test Manifest Generation
Verify that manifests are generated for each locale:
# Check for generated manifests
ls -la public/*.webmanifest
# Expected:
# site.en.webmanifest
# site.fr.webmanifest
# site.de.webmanifest2. Validate Manifest Content
Check that translations are applied correctly:
# View English manifest
cat public/site.en.webmanifest | jq '.name, .description'
# View French manifest
cat public/site.fr.webmanifest | jq '.name, .description'
# Compare manifests
diff <(jq -S . public/site.en.webmanifest) <(jq -S . public/site.fr.webmanifest)3. Test in Browser
Manual Testing:
1. Change browser language to target locale
2. Clear browser cache and service worker
3. Visit your app
4. Check DevTools → Application → Manifest
5. Verify translated content appearsAutomated Testing with Puppeteer:
const puppeteer = require('puppeteer');
async function testManifestTranslations() {
const browser = await puppeteer.launch();
const page = await browser.newPage();
// Test English
await page.setExtraHTTPHeaders({
'Accept-Language': 'en-US,en;q=0.9'
});
await page.goto('https://your-app.com/');
const manifestUrlEn = await page.evaluate(() => {
const link = document.querySelector('link[rel="manifest"]');
return link ? link.href : null;
});
console.log('English manifest URL:', manifestUrlEn);
// Expected: https://your-app.com/site.en.webmanifest
// Test French
await page.setExtraHTTPHeaders({
'Accept-Language': 'fr-FR,fr;q=0.9'
});
await page.goto('https://your-app.com/');
const manifestUrlFr = await page.evaluate(() => {
const link = document.querySelector('link[rel="manifest"]');
return link ? link.href : null;
});
console.log('French manifest URL:', manifestUrlFr);
// Expected: https://your-app.com/site.fr.webmanifest
await browser.close();
}
testManifestTranslations();4. Lighthouse Audit
Run Lighthouse with different locales:
# Audit with English locale
LANG=en npx lighthouse https://your-app.com --view
# Audit with French locale
LANG=fr npx lighthouse https://your-app.com --viewBest Practices
1. Use Meaningful Translation Keys
# ✅ Good - Clear, hierarchical keys
app:
name: "TaskMaster Pro"
shortcuts:
dashboard:
name: "Dashboard"
description: "View your dashboard"
# ❌ Avoid - Unclear, flat keys
name: "TaskMaster Pro"
shortcut1: "Dashboard"
desc1: "View your dashboard"2. Keep Brand Names Consistent
# ✅ Good - Preserve brand name across locales
# pwa.en.yaml
app:
name: "TaskMaster Pro" # Brand name unchanged
description: "Professional task management"
# pwa.fr.yaml
app:
name: "TaskMaster Pro" # Same brand name
description: "Gestion professionnelle des tâches" # Translated description3. Provide Fallbacks
framework:
translator:
fallbacks: ['en'] # Fallback to English if translation missing4. Use Translation Parameters for Dynamic Content
While the PWA manifest doesn't support dynamic parameters, you can use them in your translation files:
app:
name: "MyApp"
description: "Available in 20 countries" # Static for manifest
# For dynamic content in your app (not manifest):
messages:
welcome: "Welcome, {username}!" # Parameters work in regular translations5. Keep Translations Synchronized
Use a consistent structure across all locales:
# All locales should have the same structure
# ✅ Good - Same keys in all files
# pwa.en.yaml
app:
name: "..."
short_name: "..."
description: "..."
# pwa.fr.yaml
app:
name: "..."
short_name: "..."
description: "..."
# ❌ Avoid - Missing keys in some locales
# pwa.fr.yaml (missing short_name)
app:
name: "..."
description: "..."6. Test All Locales
Before deploying, verify all locale-specific manifests:
# Check all generated manifests exist
for locale in en fr de es; do
if [ -f "public/site.${locale}.webmanifest" ]; then
echo "✓ $locale manifest exists"
else
echo "✗ $locale manifest missing!"
fi
doneCommon Issues
1. Translations Not Applied
Problem: Manifest shows translation keys instead of translated text
Solutions:
# ✅ Check 1: Verify translation files exist
ls -la translations/pwa.*
# ✅ Check 2: Ensure correct domain
# Translation files must use 'pwa' domain: pwa.en.yaml, pwa.fr.yaml, etc.
# ✅ Check 3: Clear cache
php bin/console cache:clear
# ✅ Check 4: Check enabled locales
php bin/console debug:config framework enabled_locales2. Wrong Locale Manifest Served
Problem: English manifest served when French is expected
Solutions:
# ✅ Ensure {locale} placeholder is present
pwa:
manifest:
public_url: "/site.{locale}.webmanifest" # Must include {locale}# ✅ Check request locale
# In your Twig template:
{{ dump(app.request.locale) }}
# ✅ Verify locale detection
# Check your LocaleSubscriber or locale configuration3. Missing Translations for Specific Locale
Problem: Some keys not translated in certain locales
Solutions:
# ✅ Check for missing translations
php bin/console debug:translation --domain=pwa fr
# ✅ Extract missing keys
php bin/console translation:extract --force fr --domain=pwa
# ✅ Compare locales
diff translations/pwa.en.yaml translations/pwa.fr.yaml4. Special Characters Not Displaying
Problem: Accented characters or non-Latin scripts display incorrectly
Solutions:
# ✅ Ensure UTF-8 encoding
# Save file as UTF-8 without BOM
app:
name: "Application Géniale" # é should display correctly
description: "Optimisée pour vous" # é should display correctly# ✅ Check file encoding
file -i translations/pwa.fr.yaml
# Expected: charset=utf-85. RTL Languages Not Working
Problem: Arabic/Hebrew text displays left-to-right
Solutions:
# ✅ Don't manually set 'dir' or 'lang'
pwa:
manifest:
# dir: "ltr" ← Remove this! Let bundle auto-detect
# lang: "en" ← Remove this! Let bundle auto-detect
public_url: "/site.{locale}.webmanifest"The bundle automatically sets dir: "rtl" for RTL locales (ar, he, fa, ur).
Migration Guide
Migrating from Non-Translated Setup
Before (Hard-coded text):
pwa:
manifest:
enabled: true
public_url: "/manifest.json"
name: "My App" # Hard-coded
short_name: "MyApp" # Hard-coded
description: "A great app" # Hard-codedAfter (Translation keys):
pwa:
manifest:
enabled: true
public_url: "/site.{locale}.webmanifest" # Add {locale} placeholder
name: "app.name" # Translation key
short_name: "app.short_name" # Translation key
description: "app.description" # Translation keyapp:
name: "My App"
short_name: "MyApp"
description: "A great app"app:
name: "Mon Application"
short_name: "MonApp"
description: "Une excellente application"Migration Steps
Add
{locale}placeholder topublic_urlRemove manual
langsetting (if present)Replace hard-coded text with translation keys in config
Create translation files for each locale
Configure enabled locales in
framework.yamlClear cache and test
# After migration, clear cache
php bin/console cache:clear
# Extract translation keys
php bin/console translation:extract --force en --domain=pwa
# Test manifest generation
php bin/console cache:warmup
ls -la public/*.webmanifestTroubleshooting Checklist
Before deploying multilingual PWA:
Related Documentation
Direction and Language - Language and RTL configuration
Screenshots - Translating screenshot labels
Shortcuts - Translating shortcut names
Symfony Translation - Official Symfony Translation docs
Resources
Symfony Translation: https://symfony.com/doc/current/translation.html
Translation Component: https://symfony.com/doc/current/components/translation.html
Enabled Locales: https://symfony.com/doc/current/reference/configuration/framework.html#enabled-locales
ICU Message Format: https://unicode-org.github.io/icu/userguide/format_parse/messages/
XLIFF Format: https://docs.oasis-open.org/xliff/xliff-core/v2.0/xliff-core-v2.0.html
Crowdin: https://crowdin.com/ - Translation management platform
Lokalise: https://lokalise.com/ - Localization platform
Phrase: https://phrase.com/ - Translation platform
Last updated
Was this helpful?