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

Bcp 47 Code Laravel Package

wikimedia/bcp-47-code

A lightweight PHP library offering value objects for BCP 47 language codes, letting you parse, validate, and format tags like en, de-AT, or zh-Hant safely instead of passing raw strings around.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a very small, single-purpose package: it gives you a Bcp47Code interface and a Bcp47CodeValue value object so that "a language code" can be passed around as a type instead of a bare string that might be either a BCP 47 tag or some application-internal code. It is not a parser, a negotiator, or an IANA registry lookup — treat it as a type-safety primitive.

composer require wikimedia/bcp-47-code

The whole surface area is two files under Wikimedia\Bcp47Code\:

  • Bcp47Code — the interface (toBcp47Code(): string, plus __toString()).
  • Bcp47CodeValue — a final implementation with a named constructor.

First use case — make invalid codes fail at the edge, not deep in a formatter:

use Wikimedia\Bcp47Code\Bcp47Code;
use Wikimedia\Bcp47Code\Bcp47CodeValue;

function renderGreeting( Bcp47Code $lang ): string {
    return match ( $lang->toBcp47Code() ) {
        'en', 'en-GB' => 'Hello',
        'fr'          => 'Bonjour',
        'de'          => 'Hallo',
        default       => 'Hello',
    };
}

renderGreeting( Bcp47CodeValue::newFromBcp47( 'en-GB' ) );

Read Bcp47CodeValue's constructor/newFromBcp47() first — it tells you exactly which tags are accepted and whether invalid input throws, which drives most of the advice below.

Implementation Patterns

Type-hint at your module boundaries, construct at the edges. The value of the package comes from forcing every language code crossing an API, service, or event boundary to be a Bcp47Code. Parse/normalize once where input arrives (HTTP header, DB row, config), then pass the object inward.

Wrap legacy/internal codes with an adapter. The whole reason this package exists in MediaWiki is that the platform's internal codes (en-gb, zh-hans, simple) are not BCP 47. Do the same: keep your internal representation in one place and expose a thin adapter.

final class InternalLanguage implements Bcp47Code {
    public function __construct( private string $internalCode ) {}

    public function toBcp47Code(): string {
        return str_replace( '_', '-', $this->internalCode ); // your mapping rules
    }

    public function __toString(): string {
        return $this->toBcp47Code();
    }
}

Implementing the interface yourself is the intended extension point since Bcp47CodeValue is final.

Normalize once, compare on the lowercase form. BCP 47 tags are case-insensitive, but the object preserves whatever casing you gave it. If you key caches, array_unique, or compare, normalize explicitly:

$key = strtolower( $lang->toBcp47Code() ); // safe cache key

Persist the string, rehydrate on read. Don't serialize the object. Store toBcp47Code() in the column, and rebuild with Bcp47CodeValue::newFromBcp47() on hydration — that also gives you a natural place to surface corrupt legacy rows.

Compose with a real negotiator. For Accept-Language handling you still need a parser/negotiator (negotiation, symfony/intl, or your own). Use this package afterward to guarantee that whatever the negotiator selects is a well-formed tag before it reaches business logic.

$selected = $negotiator->getBest( $available, $request->getHeaderLine( 'Accept-Language' ) );
$lang = Bcp47CodeValue::newFromBcp47( $selected->getValue() );

Test with realistic tags. Because construction validates, your fixtures should include a plain tag (fr), a region tag (pt-BR), a script tag (zh-Hans), and — if you rely on it — a private-use tag (x-mycompany). Tests for the unhappy path should assert the exception type rather than the message.

Gotchas and Tips

  • Validation semantics are the whole library — verify them for your version. newFromBcp47() validates against a BCP 47-shaped grammar and throws InvalidArgumentException on garbage. Tags that look "wrong" but are legal (private use x-…, grandfathered forms) may pass; things like en_US or EN gb will not. Pin the version and read the regex rather than assuming.
  • It does not parse. There is no accessor for language subtag, script, region, or extensions. If you need those components, use symfony/intl or a dedicated RFC 5646 library and only use this package for the type boundary.
  • Don't confuse BCP 47 with your platform's internal codes. Feeding a BCP 47 tag into an API that expects MediaWiki-style lowercase codes (or vice versa) is the classic bug this library was invented to prevent — lean on the type system, but don't assume conversion is identity. zh-hans ≠ zh-Hans as far as the other system is concerned.
  • __toString() is a convenience, not a contract. Prefer explicit toBcp47Code() in production code; __toString() is there so the object behaves in string interpolation and log contexts. Never rely on implicit casting for anything security- or storage-sensitive.
  • Case matters for equality unless you make it not matter. Two Bcp47CodeValue instances wrapping en-GB and en-gb are different strings. Normalize before comparing or hashing.
  • Bcp47CodeValue is final. Add behavior through a decorator or your own implementation of the interface, not by subclassing.
  • License is GPL-2.0. Fine (and unsurprising) inside MediaWiki and GPL projects, but a real consideration for MIT/Apache-licensed applications. Check whether that's acceptable before adopting, or vendor a small equivalent interface of your own.
  • Contributions go through Gerrit, not GitHub. The GitHub repo is a read-only mirror; the developer instructions point at mediawiki.org/wiki/Developer_account. Expect a low-traffic project (single-digit stars, essentially zero public dependents outside MediaWiki) — budget for the possibility that you fix issues yourself rather than waiting on upstream.
  • Debugging tip: when a code is rejected, var_dump() the raw bytes first — trailing whitespace, non-breaking spaces, and _ vs - substitutions from copy-pasted locale data are the overwhelmingly common causes, and the exception message won't always make that obvious.
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.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource