Skip to main content

Locale

Larasell writes every locale as a BCP 47 language tag, like en, de-DE or zh-Hans. It's the same format browsers send in Accept-Language, HTML uses in lang and hreflang, and search engines expect. You use the same string everywhere: in translations, in the config, in URLs and in HTTP headers.

Locale tags

A tag has up to three parts, separated by hyphens:

  • Language: 2 or 3 lowercase letters, like de or agq.
  • Script: 4 letters with the first one uppercase, like Hans or Latn. Optional.
  • Region: 2 uppercase letters, like DE or CH. Optional.

So de is German, de-CH is German as spoken in Switzerland, zh-Hans is Chinese written in simplified characters and es-MX is Spanish as spoken in Mexico.

Larasell supports these three parts. Variants and extensions, like de-CH-1996 or en-US-u-ca-buddhist, are not supported.

The Locale enum

The Locale enum lists every locale Larasell knows. Each case's value is its tag:

1use Larasell\Larasell\Enums\Locale;
2
3Locale::GERMAN->value; // 'de'
4Locale::GERMAN_GERMANY->value; // 'de-DE'
5Locale::CHINESE_SIMPLIFIED->value; // 'zh-Hans'
6Locale::from('es-MX'); // Locale::SPANISH_MEXICO

The enum lists every locale that exists, not the locales your shop supports. Use the locale config for that.

Canonical form

Tags are case-insensitive, so de-de and DE-de mean the same locale. Larasell stores each tag in one canonical form, so you never end up with two translations for the same locale.

Locale::canonicalize returns the canonical form of a tag. It also accepts Laravel's underscore style, which is handy for values that come from App::currentLocale() or lang/ folders:

1use Larasell\Larasell\Enums\Locale;
2
3Locale::canonicalize('de-de'); // 'de-DE'
4Locale::canonicalize('de_DE'); // 'de-DE'
5Locale::canonicalize('ZH-hans'); // 'zh-Hans'
6Locale::canonicalize('germ'); // throws InvalidArgumentException

Use it before Locale::from when the tag comes from user input:

1Locale::from(Locale::canonicalize($request->input('locale')));

Lookup chain

Locale::lookupChain returns a tag followed by each shorter tag, which is the order Larasell falls back in:

1use Larasell\Larasell\Enums\Locale;
2
3Locale::lookupChain('zh-Hans-CN'); // ['zh-Hans-CN', 'zh-Hans', 'zh']
4Locale::lookupChain('de'); // ['de']

Configuration

config/larasell/locale.php defines which locales your shop serves. Publish it with the rest of the Larasell config:

1php artisan vendor:publish --tag=larasell-config

This copies the file to config/larasell/locale.php in your app. List your locales as Locale cases and pick one of them as the default. A store that serves English, German and Canadian French, with English as the default, looks like this:

1use Larasell\Larasell\Enums\Locale;
2
3return [
4    'supported' => [Locale::ENGLISH, Locale::GERMAN, Locale::FRENCH_CANADA],
5    'default' => Locale::ENGLISH,
6];
  • supported: the locales your shop serves. Defaults to [Locale::ENGLISH].
  • default: the locale for visitors that match no supported locale, and the locale translations fall back to. It must be one of supported. Defaults to Locale::ENGLISH.

Both keys also accept tags as strings, which is useful when the value comes from the environment:

1'supported' => explode(',', env('SHOP_LOCALES', 'en,de')),

Reading the configuration

LocaleSettings reads and validates the config. Resolve it from the container:

1use Larasell\Larasell\Settings\LocaleSettings;
2
3$locales = app(LocaleSettings::class);
4
5$locales->supported(); // [Locale::ENGLISH, Locale::GERMAN, Locale::FRENCH_CANADA]
6$locales->default(); // Locale::ENGLISH

Both methods throw an InvalidArgumentException when the config holds an unknown locale, when supported is empty, or when default is not one of supported.

Negotiating a locale

negotiate picks the supported locale that best matches an Accept-Language header:

1$locales->negotiate('de-CH,de;q=0.9,en;q=0.8'); // Locale::GERMAN
2$locales->negotiate('fr'); // Locale::FRENCH_CANADA
3$locales->negotiate('es'); // Locale::ENGLISH, the default
4$locales->negotiate(null); // Locale::ENGLISH, the default

It goes through the languages in the header, highest quality first. Languages with the same quality keep the order the browser sent them in. For each language, it tries:

  1. The tag itself and each shorter tag. de-CH matches a supported de.
  2. The first supported locale that starts with the tag. fr matches a supported fr-CA.

The first match wins. Regions are not matched against each other, so de-CH alone does not match de-DE. Browsers usually send the bare language too, as in de-CH,de;q=0.9, and de then matches de-DE. Languages with q=0 are skipped, and a * stops the search and returns the default.

The localization guide shows how to use it in a middleware.

Translatable fields

These fields hold a Translatable with one text per locale:

  • Product: name, slug and description. description can be null.
  • Category: name and slug.
  • ProductAttribute: name, like "Color".
  • ProductAttributeValue: name, like "Red".
  • OrderItem: product_name, a copy of every translation of the product name at checkout.

Read them with get, and write them with an array keyed by locale or a plain string for the current locale:

1use Larasell\Larasell\Models\ProductAttribute;
2
3$color = ProductAttribute::create([
4    'slug' => 'color',
5    'name' => ['en' => 'Color', 'de' => 'Farbe'],
6]);
7
8$color->name->get('de'); // 'Farbe'

Slugs of attributes and attribute values are plain strings. They identify a value in $product->variantFor(['color' => 'red']), so they're the same in every language.

ProductVariant::name() and ProductVariant::options() return strings in the current locale, like "Klein / Schwarz". Order items keep variant_name and variant_options in the locale of the checkout, which is the language the customer bought in.

Fallback

When a translatable field has no translation for the requested locale, get returns the first translation it finds in this order:

  1. The requested locale and each shorter tag, from Locale::lookupChain.
  2. The default locale from larasell.locale.default, and each shorter tag.
  3. The first translation the field has.
1use Larasell\Larasell\Translatable;
2
3config(['larasell.locale.default' => 'en']);
4
5$name = new Translatable(['en' => 'Bicycle', 'de' => 'Fahrrad', 'zh-Hans' => '自行车']);
6
7$name->get('de-AT'); // 'Fahrrad', from 'de'
8$name->get('zh-Hans-CN'); // '自行车', from 'zh-Hans'
9$name->get('fr'); // 'Bicycle', from the default

get without an argument uses App::currentLocale().

Where Larasell uses the current locale

Larasell reads App::currentLocale() in these places:

  • get on translatable fields, like $product->name->get().
  • Finding products and categories by slug in ProductDetailRoute, ProductDetailRequest and ProductListingRequest. Slugs are matched in the current locale only. They don't fall back.
  • Sorting product listings by name.
  • Price::format, when you don't pass a locale.