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(1y ago)21.2k↑93.3%MITPHPPHP &gt;=8.3CI passing

Since Jul 10Pushed 1y 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 2w 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

36

—

LowBetter than 79% of packages

Maintenance46

Moderate activity, may be stable

Popularity22

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity57

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

400d 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

[smarty-gettext/smarty-gettext

Gettext plugin enabling internationalization in Smarty Package files

752.5M8](/packages/smarty-gettext-smarty-gettext)[devaslanphp/auto-translate

Auto generate translation JSON files

6627.3k](/packages/devaslanphp-auto-translate)

PHPackages © 2026

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