Dynamic Content in Translations: Placeholders Done Right
Static translations are easy:
Welcome to our app
But what about:
Welcome back, John! You have 5 new messages.
That "John" and "5" need to be dynamic. And the moment they are, you're dealing with placeholders, which is exactly where translations quietly break: a variable gets renamed in one language, a plural form goes missing in another, and suddenly your Spanish users see a literal :count on screen. Here's how to do placeholders right in Laravel, and how LangSyncer keeps them intact when AI translates for you.
The Problem in a Real Laravel App
The tempting shortcut is concatenation:
// Don't do this
$greeting = 'Welcome back, '.$user->name.'! You have '.$count.' new messages.';
This works in English and nowhere else. In German, the sentence structure changes. In Japanese, the name may need an honorific. In Spanish, you want an opening exclamation mark before the sentence, not just one at the end. Concatenation hardcodes English word order into your PHP, and no translator can fix that without touching your code.
Laravel solves this with named placeholders, and it supports two syntaxes:
// lang/en/messages.php
return [
'greeting' => 'Welcome back, :name! You have :count new messages.',
'invited' => '{user} invited {invitee} to {project}',
];
// Anywhere in your app
__('messages.greeting', ['name' => $user->name, 'count' => $count]);
The colon style (:name) is what Laravel itself uses everywhere, most visibly in validation messages, where :attribute gets filled with the field name at runtime:
// lang/en/validation.php
return [
'required' => 'The :attribute field is required.',
'max' => [
'string' => 'The :attribute field must not be greater than :max characters.',
],
];
Laravel even adapts capitalization for you: :attribute stays lowercase, :Attribute capitalizes the first letter, and :ATTRIBUTE uppercases the whole value. Curly braces ({name}) work too. Both are fine. Pick one style and be consistent.
Pluralization with trans_choice
Counts deserve their own mechanism, because "You have 1 new messages" is the kind of bug users screenshot. Laravel handles plural forms with pipe-separated segments and trans_choice:
// lang/en/messages.php
return [
'inbox' => '{0} No new messages|{1} One new message|[2,*] :count new messages',
];
// Usage
trans_choice('messages.inbox', $count, ['count' => $count]);
The {0}, {1}, and [2,*] markers select a segment based on the count, and :count interpolates the number into whichever segment wins. Keep in mind that other languages need different segment sets entirely: French treats zero as singular, Polish has a separate form for 2 through 4, Arabic has six plural forms. Your English string is a starting point, not a template every language can copy.
Dates and Other Formatted Values
One more category deserves a mention: values that are themselves locale-dependent. A translation like Last updated on {date} should receive a date that's already formatted for the user's locale, not a raw string:
__('messages.updated', [
'date' => $post->updated_at->isoFormat('LL'),
]);
The translation stays clean and the formatting logic stays in PHP, where it belongs. The same rule applies to currency and numbers: pass them in pre-formatted. Placeholders are slots for finished values, not places to do formatting work.
Keeping Placeholders Intact in LangSyncer
Translation values in LangSyncer are stored as plain strings, so placeholders travel with them untouched. Here's the workflow:
-
Create the key with placeholders in your source language. In the translations manager, add the group
messages, the keygreeting, and the English valueWelcome back, {name}! You have {count} new messages. -
Click AI Autofill. The AI translates into every empty language field while preserving variables (
{name},:count), line breaks, and punctuation. The Spanish result:
¡Hola de nuevo, {name}! Tienes {count} mensajes nuevos.
Notice that {name} stayed {name} (not {nombre}), {count} stayed {count}, and the word order changed naturally for Spanish. The AI also uses the key name for context, so checkout.button.confirm gets translated like a button label, not a sentence.
-
Review before saving. Verify every placeholder survived. A value like
Hola, {nombre}would save fine and then print a literal{nombre}at runtime, because your code passesname. It's a ten-second check that prevents a production bug. -
Save and publish. Saves create drafts, and nothing is live until you click Publish, so you can batch several keys and release them together.
-
Catch the strings you missed. Run the code scanner:
php artisan translator:scan --ai
When it finds hardcoded text like "Welcome, John!", the AI recognizes "John" as dynamic content. It suggests the key greeting with the value Welcome, {name}! and a note to replace John with a $name variable. Smart enough to know "John" is a placeholder, not literal text.
Each AI translation costs 1 quota unit per target language, so a key translated into five languages costs five units. The AI translations guide covers quota in detail.
When Not to Rely on Automation
AI placeholder preservation is reliable for variable tokens, but it isn't a substitute for review everywhere:
- Plural forms. The AI translates the string you give it. Don't count on any machine translation pass to add the extra plural segments that Polish or Arabic grammar requires; have a native speaker define those segment sets for languages with complex plural rules.
- Gendered grammar.
:nameslots in fine, but the words around it (adjectives, greetings) may need gender-aware or gender-neutral phrasing that an automated pass won't flag. - Cost of iteration. Re-translating the same text consumes quota again. Get the source string right, placeholders included, before autofilling five languages.
And if your app is English-only and will stay that way, plain lang files with placeholders may be all you need. When you're weighing the switch, here's how LangSyncer compares to raw Laravel lang files.
Gotchas
- Placeholder names are code, not content. Use descriptive names (
{userName}, not{u}), and keep them identical in every language. LangSyncer's AI enforces this automatically; human translators sometimes don't. - Placeholder values render exactly as passed. If
:namecomes from user input, output it through escaped Blade syntax ({{ }}), never{!! !!}. - Test with long values. "Dr. Alexandra von Rothschild III" might break a layout that looked perfect with "Ana".
Related Posts
- Find Hardcoded Text in Your Laravel App with Code Scanner
- Translate Your Entire App in Seconds with AI