In Laravel, how do you define a translation line in lang/{locale}/messages.php and read it with __(), and what comes back when the key is missing?
answer
- no lang folder in the new skeleton
- lang:publish scaffolds lang/en
- each file returns a keyed PHP array
- file name, dot, key
- fallback locale tried before giving up
basics
~10 sCreate lang/{locale}/messages.php returning a keyed array and call __('messages.welcome'). If neither the current nor the fallback locale has that line, __() returns the key string itself instead of throwing.
solid answer
~40 sA Laravel 13 skeleton ships no `lang` directory; `php artisan lang:publish` creates `lang/en` with the framework's `auth`, `pagination`, `passwords` and `validation` files, and you add your own group files beside them, such as `lang/es/messages.php` returning `['welcome' => 'Bienvenido']`. You read a line with `__('messages.welcome')` - file name, a dot, then the key, with more dots for nested arrays. `trans()` performs the same lookup, and Blade offers `{{ __('messages.welcome') }}` or `@lang('messages.welcome')`. For a short key the translator tries the current locale, then the fallback locale, and when both miss it returns the key itself, `messages.welcome`, so a missing line shows up on the page as a raw key rather than an exception. Asking for a whole group, `__('messages')`, returns the array.
code
php · 9 lines<?php
// lang/es/messages.php
return [
'welcome' => 'Bienvenido a nuestra campaña',
'nav' => [
'donate' => 'Donar',
],
];go deeper
Recall the file layout, lang/{locale}/group.php returning an array, the dot syntax of __(), and that a missing key comes back as the key text.
Explain the lookup order, current locale then fallback then the key, and what lang:publish copies and why the framework's lines work without it.
Show how you stop silent raw keys reaching users: key tests, a missing-key handler, and review habits for renamed groups.
Weigh short-key files against source-text JSON for a team's workflow, and set conventions for group naming so translators and developers share one vocabulary.
## Where translation files live Laravel's translator reads **translation files** from the application's `lang` directory at the project root. A new Laravel 13 application does not contain that directory at all: the framework keeps its own English lines inside `Illuminate/Translation/lang`, and the translator's file loader reads that framework directory first and your `lang` directory second, so the default validation and password messages work before you create anything. When you want to change those defaults or add your own lines, run: ```bash php artisan lang:publish ``` The command creates `lang/en` and copies four files into it: `auth.php`, `pagination.php`, `passwords.php` and `validation.php`. Two options change its behaviour: - `--existing` re-copies only the files you already published, which is how you pull in new framework lines after an upgrade. - `--force` overwrites every published file, losing your edits. You do not need the command to start translating; creating `lang/es/messages.php` by hand works just as well. ## Defining short-key files In the **short-key** style each language gets a subdirectory named after its locale (`en`, `es`, `pl`, or a territory form such as `en_GB`), and each PHP file in it is a **group** that returns an array: ```php <?php // lang/es/messages.php return [ 'welcome' => 'Bienvenido a nuestra campaña', 'nav' => [ 'donate' => 'Donar', ], ]; ``` The file name becomes the first segment of every key, so this file provides `messages.welcome` and `messages.nav.donate`. ## Retrieving lines | Call | Where | Returns | |---|---|---| | `__('messages.welcome')` | PHP or Blade | the line, or the key if missing | | `trans('messages.welcome')` | PHP or Blade | the same lookup; `trans()` with no argument returns the `Translator` | | `@lang('messages.welcome')` | Blade | echoes the line | | `Lang::get('messages.welcome')` | PHP, facade | the same lookup | `__()` simply delegates to `trans()`, and both call the translator's `get()` method. The only difference is at the edges: `__(null)` returns `null`, while `trans()` with no key hands you the translator instance. Two details catch people out: - `@lang` compiles to a plain `echo` of the translator's result, without the HTML escaping that `{{ }}` applies, so `{{ __('...') }}` is the safer everyday form when a line carries user-supplied placeholder values. - A key that names only a group, `__('messages')`, returns the whole array. Laravel 13.15 added `Lang::string()` and `Lang::array()`, which throw an `InvalidArgumentException` when the value is not the type you asked for. ## When a key is missing The lookup order for a short key is: 1. Look in the **current locale's** group file (`lang/es/messages.php`). 2. Look in the **fallback locale's** group file (`lang/en/messages.php`), when a fallback is configured. 3. Give up and return the **key string itself**, after applying any placeholder replacements. No exception is raised. That is deliberate: a raw `messages.welcome` on a page is easy to spot in review, while a crash on every page with one untranslated line would be worse. The price is that a typo in a key fails silently, so teams usually add a test or a missing-key handler to catch it. ## Common pitfalls - Naming a territory directory `en-gb`: the docs call for the ISO 15897 form, `en_GB`. - Assuming translations live in `resources/lang`: new applications use `lang/` at the root (the framework still reads `resources/lang` if that directory exists). - Treating a returned key as proof the file is broken: first check the active locale and the spelling of the group and key. - Editing files under `vendor/` to change framework lines instead of publishing them with `lang:publish`. ## How loading works at runtime - The translator is a **singleton**, built the first time something asks for it, and it keeps every group it has loaded in memory for the rest of the process. - A group is read once per locale: the first Spanish `__('messages.welcome')` loads `messages.php` for `es` from the framework directory and from yours, merges the two, and later lookups hit the in-memory array. - Group files are plain PHP and may hold nested arrays, but they should hold only strings and arrays; logic in a translation file runs at load time and surprises the next reader. ## A worked example for a charity site A Spanish donation page might use three groups: `messages.php` for navigation and headings, `donations.php` for the donation form, and the published `validation.php` for form errors. Each view reads its lines through `__()` with the group prefix, so a translator can open one file and see every line of one feature. When a developer adds a line to `lang/en/donations.php` but not yet to `lang/es/donations.php`, Spanish visitors see the English line from the fallback locale, assuming the fallback is English, until the translation lands.
- Do the framework's validation messages need lang:publish before they work?No. The translator's loader reads the framework's own lang directory first and your `lang` path second, merging the two so your lines override. `lang:publish` only copies the English files into `lang/en` so you can edit them; `--existing` refreshes files you already published and `--force` overwrites all of them.
- Is there any behavioural difference between __() and trans()?For a key, no: `__()` calls `trans()`, which calls the translator's `get()`, and both read JSON and PHP files the same way. They differ only without a key: `trans()` returns the `Translator` instance, `__(null)` returns `null`.
- What does __('messages') return when lang/es/messages.php exists?The whole array the file returns, with placeholders replaced in every string. Code that expects a string then breaks, which is why Laravel 13.15 added `Lang::string()` and `Lang::array()` to assert the type and throw an `InvalidArgumentException` otherwise.
saying these in an interview costs you the question
- __() throws an exception when a translation key does not exist
- You must run lang:publish before any app translation file can be read
- A new Laravel 13 app keeps its translations under resources/lang
- trans() and __() read different files, so pick one per file type
- @lang escapes its output exactly like {{ __() }} does