PHPackages                             lhcze/bcp47-tag - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Localization &amp; i18n](/categories/localization)
4. /
5. lhcze/bcp47-tag

ActiveLibrary[Localization &amp; i18n](/categories/localization)

lhcze/bcp47-tag
===============

BCP47Tag parser and validator

2.0.0(11mo ago)21.1k↑92.9%MITPHPPHP &gt;=8.3CI passing

Since Jul 10Pushed 11mo agoCompare

[ Source](https://github.com/lhcze/bcp47-tag)[ Packagist](https://packagist.org/packages/lhcze/bcp47-tag)[ RSS](/packages/lhcze-bcp47-tag/feed)WikiDiscussions main Synced 2d ago

READMEChangelog (3)Dependencies (9)Versions (8)Used By (0)

🌐 BCP47Tag
==========

[](#-bcp47tag)

🪐 **Don’t panic. Your tag is valid.**
-------------------------------------

[](#-dont-panic-your-tag-is-valid)

### Validate, Normalize &amp; Canonicalize BCP 47 Language Tags (`en`, `en-US`, `zh-Hant-CN`, etc.)

[](#validate-normalize--canonicalize-bcp-47-language-tags-en-en-us-zh-hant-cn-etc)

[![License](https://camo.githubusercontent.com/7013272bd27ece47364536a221edb554cd69683b68a46fc0ee96881174c4214c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d626c75652e737667)](https://camo.githubusercontent.com/7013272bd27ece47364536a221edb554cd69683b68a46fc0ee96881174c4214c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d626c75652e737667)[![PHP](https://camo.githubusercontent.com/22d1e0962d83aee7b515b9d8f7fc7768aca0bb041a6daf6ce95255573e0b2356/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048502d2533453d382e332d373737626234)](https://camo.githubusercontent.com/22d1e0962d83aee7b515b9d8f7fc7768aca0bb041a6daf6ce95255573e0b2356/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048502d2533453d382e332d373737626234)[![GitHub Actions Workflow Status](https://camo.githubusercontent.com/9490073bcc233be347153340511509cc9a15e1b9cc26459eecc93a4b2295eec7/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f6c68637a652f62637034372d7461672f7068702e796d6c)](https://camo.githubusercontent.com/9490073bcc233be347153340511509cc9a15e1b9cc26459eecc93a4b2295eec7/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f6c68637a652f62637034372d7461672f7068702e796d6c)[![Packagist](https://camo.githubusercontent.com/dc7c89900aa28d66ca92473b71261bedd4bd239263697562b93e52a7cc3f1320/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6c68637a652f62637034372d746167)](https://camo.githubusercontent.com/dc7c89900aa28d66ca92473b71261bedd4bd239263697562b93e52a7cc3f1320/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6c68637a652f62637034372d746167)[![Downloads](https://camo.githubusercontent.com/328d499b591271619ac8d6e86c202ca0837dc9ae2863ad8e7fbe5e0b9b4ae189/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6c68637a652f62637034372d746167)](https://camo.githubusercontent.com/328d499b591271619ac8d6e86c202ca0837dc9ae2863ad8e7fbe5e0b9b4ae189/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6c68637a652f62637034372d746167)[![IANA Registry](https://camo.githubusercontent.com/81bf41c81551d333108502bea49172f6fcead5f5aab67cc8675abcd9bf12ab10/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f536f757263652d49414e412532304c616e677561676525323053756274616725323052656769737472792d677265656e)](https://camo.githubusercontent.com/81bf41c81551d333108502bea49172f6fcead5f5aab67cc8675abcd9bf12ab10/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f536f757263652d49414e412532304c616e677561676525323053756274616725323052656769737472792d677265656e)

**BCP47Tag** is a robust PHP library for working with BCP 47 language tags:

- ✔️ Validates against the real IANA Language Subtag Registry
- ✔️ ABNF-compliant (RFC 5646)
- ✔️ Supports language, script, region, variant, grandfathered tags
- ✔️ Auto-normalizes casing &amp; separators (`en_us` → `en-US`)
- ✔️ Automatically expands collapsed ranges from the registry
- ✔️ Resolves partial language tags (e.g., `en` → `en-US`) using custom canonical matching, with scoring
- ✔️ Error handling via clear exception types
- ✔️ Lightweight `LanguageTag` VO for validated tags
- ✔️ Works perfectly with `ext-intl`—no surprises upon feeding ICU
- ✔️ Easy fallback mechanism
- ️🫧 Supports grandfathered tags so old, they still remember when Unicode 2.0 was hot
- 🖖 Accepts `i-klingon` and `i-enochian` for your occult projects
- 🤓 `ABNF` so clean, linguists shed a single tear

---

❓ Why not just use `ext-intl`?
------------------------------

[](#-why-not-just-use-ext-intl)

Good question — and the answer is: you **should** keep using it! `ext-intl` (ICU) is brilliant at formatting *if* your tag is clean.

However, it does **not**:

- ✅ Validate that your tag fully follows the **BCP 47 ABNF** rules.
- ✅ Reject or warn about **grandfathered** or **deprecated subtags**.
- ✅ Match your tags against the authoritative **IANA Language Subtag Registry**.
- ✅ **Resolve partial input** (`en` → `en-US`) to a known canonical list.
- ✅ Enforce **known tags only** with `knownTags` + `requireCanonical`.

> If you’re in Symfony, you might also use `#[Assert\Locale]` for basic input validation.
> And that’s fine for checking user input — but it stops at *structure*. It won’t canonicalize, resolve, or check IANA.

👉 **So the best practice:**

- ✅ Use **BCP47Tag** to *validate &amp; normalize*.
- ✅ Hand the cleaned tag to `ext-intl` or whatever else you have for formatting &amp; display.
- ✅ Trust you’ll never feed ICU any garbage.
- ✅ Carry around immutable LanguageTag value object across your code base instead of string

**BCP47Tag**: RFC 5646 + IANA + real normalization + fallback + resolution.
No hustle with regex, `str_replace()` or guesswork.

---

⚡️ **Installation**
-------------------

[](#️-installation)

```
composer require lhcze/bcp47-tag
```

---

🚀 **Basic Usage**
-----------------

[](#-basic-usage)

```
use LHcze\BCP47\BCP47Tag;

// Just normalize & validate
$tag = new BCP47Tag('en_us');
echo $tag->getNormalized();    // "en-US"
echo $tag->getICUformat();   // "en_US"

// With canonical matching
$tag = new BCP47Tag('en', useCanonicalMatchTags: ['de-DE', 'en-US']);
echo $tag->getNormalized();    // "en-US"

// Use fallback if invalid
$tag = new BCP47Tag('notreal', 'fr-FR');
echo $tag->getNormalized(); // fr-FR

// Invalid input → exception
try {
    new BCP47Tag('invalid!!');
} catch (BCP47InvalidLocaleException $e) {
    echo $e->getMessage();
}

// Feed to ext-intl
$icu = $tag->getICULocale(); // en_US
echo Locale::getDisplayLanguage($icu); // English

// LanguageTag VO
$langTag = $tag->getLanguageTag();
echo $langTag->getLanguage();  // "en"
echo $langTag->getRegion();    // "US"
echo (string) $langTag;        // "en-US"
```

---

🔍 **Features &amp; Flow**
-------------------------

[](#-features--flow)

1. **Normalize + parse**
    Clean casing/formatting and parse into components.
2. **Validate against IANA**
    Broken input or fallback triggers explicit exceptions:

    - `BCP47InvalidLocaleException`
    - `BCP47InvalidFallbackLocaleException`
3. **Canonical matching (optional)**

    - Pass an array of `useCanonicalMatchTags`
    - Each is matched and scored:
        +100 language match, +10 region, +1 script
    - Highest score wins.
    - Same score makes the first one to have it to make a home run
4. **LanguageTag VO**
    Immutable, validated, `Stringable` &amp; `JsonSerializable`.

---

📜 Supported Tags
----------------

[](#-supported-tags)

BCP47Tag uses a **precompiled static PHP snapshot** of the latest **IANA Language Subtag Registry** to validate languages, scripts, regions, variants, and grandfathered tags. The registry is loaded **once per process**, kept hot in OPcache for maximum speed.

- ✅ ISO language, script, region, variants
- ✅ Grandfathered/deprecated tags (e.g., `i-klingon`)
- ✅ Collapsed registry ranges are auto-expanded
- ⚠️ Extensions &amp; private-use subtags (future)

---

🧩 **Key API**
-------------

[](#-key-api)

MethodDescription`__construct(string $input, ?string $fallback, ?array $useCanonicalMatchTags)`Main entry`getInputLocale()`Original input string`getNormalized()`RFC‑5646 formatted tag`getICUformat()`Underscore variant (`xx_XX`)`getLanguageTag()`Returns `LanguageTag` VO`__toString()` / `jsonSerialize()`Returns normalized string---

📜 The Official BCP 47 ABNF
--------------------------

[](#-the-official-bcp-47-abnf)

The syntax tags must follow is defined by [RFC 5646](https://datatracker.ietf.org/doc/html/rfc5646) in ABNF:

```
langtag = language
   ["-" script]
   ["-" region]
   *("-" variant)
   *("-" extension)
   ["-" privateuse]
```

Examples:

- ✅ `en` → valid
- ✅ `en-US` → valid
- ✅ `zh-Hant-CN` → valid
- ✅ `i-klingon` → valid (grandfathered)
- ✅ `en-US-x-private` → valid (extension/private use)
- ❌ `en-US--US` → invalid

BCP47Tag respects this ABNF, so your tags match the real spec — no hidden assumptions.

---

❓ **Why is this useful?**
-------------------------

[](#-why-is-this-useful)

Use cases include:

- Validating API `Accept-Language` headers
- Multi-regional CMS deployments
- Internationalization pipelines
- Locale-dependent services where mis-typed tags lead to silent failures

---

⚙️ **Requirements**
-------------------

[](#️-requirements)

- PHP 8.3+
- `ext-intl`

---

🧪 **Tests**
-----------

[](#-tests)

```
composer qa
```

---

📌 **Roadmap**
-------------

[](#-roadmap)

- ✅ IANA Language Subtag Registry integration
- ✅ Language, script, region, variant validation
- ✅ Lazy singleton registry loader
- ✅ Static PHP snapshot of the IANA registry for ultra-fast lookups
- ✅ Canonical matching with scoring
- ✅ Typed exceptions for flow control
- ⚙️ Extension/subtag support (planned)
- ⚙️ Additional data use from IANA registry (suppress-script subtag, preferred, prefix)
- ⚙️ Auto-registry refresh script

---

📖 License
---------

[](#-license)

[MIT](LICENSE)

---

🔗 References
------------

[](#-references)

- [RFC 5646 – BCP 47 ABNF](https://tools.ietf.org/html/rfc5646)
- [IANA Language Subtag Registry](https://www.iana.org/assignments/language-subtag-registry)

---

🧬 Now go and **boldly canonicalize strange new tags the BCP 47 way!** 🌍✨

###  Health Score

37

—

LowBetter than 81% of packages

Maintenance50

Moderate activity, may be stable

Popularity22

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity56

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~2 days

Total

3

Last Release

355d ago

Major Versions

1.0.1 → 2.0.02025-07-13

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/5402084?v=4)[Lukas](/maintainers/lhcze)[@lhcze](https://github.com/lhcze)

---

Top Contributors

[![lhcze](https://avatars.githubusercontent.com/u/5402084?v=4)](https://github.com/lhcze "lhcze (6 commits)")

---

Tags

bcp-47bcp47ianaicuintllanguage-taglanguage-tagslocalelocalizationphprfc-5646

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/lhcze-bcp47-tag/health.svg)

```
[![Health](https://phpackages.com/badges/lhcze-bcp47-tag/health.svg)](https://phpackages.com/packages/lhcze-bcp47-tag)
```

###  Alternatives

[smmoosavi/php-gettext

Wrapper for php-gettext by danilo segan. This library provides PHP functions to read MO files even when gettext is not compiled in or when appropriate locale is not present on the system.

1927.0k1](/packages/smmoosavi-php-gettext)[saeven/circlical-po-editor

Gettext \*.PO file editor and parser for PHP.

118.1k1](/packages/saeven-circlical-po-editor)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
