googlei18n/myanmar-tools
A PHP library from Google i18n for working with Myanmar text. It detects Zawgyi versus Unicode, converts between the two encodings, and normalizes Burmese text for reliable display, search, and sorting. Useful for cleaning user input and migrating legacy Zawgyi data.
googlei18n/myanmar-tools solves one very specific problem: Myanmar text can be encoded as Zawgyi-One (a legacy, font-dependent encoding still common in Myanmar) or Unicode (the correct standard). The two look similar but are byte-wise incompatible, so mixing them in a database, search index, or API silently breaks sorting, search, and rendering. This package gives you two things:
For a Laravel app, you almost always want the detector on input and the converter only when confidence is high.
The repo is a multi-language monorepo; the PHP implementation lives under php/. Check whether your Composer resolution works first:
composer require googlei18n/myanmar-tools
If it is not on Packagist for your version constraints (the last tagged release is from 2020), add it as a VCS or path repository rather than copying files ad hoc:
{
"repositories": [
{ "type": "vcs", "url": "https://github.com/googlei18n/myanmar-tools" }
]
}
Then vendor the PHP source into a stable namespace you control:
mkdir -p app/Support/MyanmarTools
# copy php/src/*.php and the model .dat file into this directory
The detector loads a trained model file (zawgyiUnicodeModel.dat). Whatever install path you choose, verify the model file is shipped and resolvable at runtime — this is the #1 cause of “it works locally, fails in production.”
The highest-value first integration is a single write path, e.g. a user's display name or bio:
namespace App\Services;
use MyanmarTools\ZawgyiConverter;
use MyanmarTools\ZawgyiDetector;
class MyanmarText
{
public function __construct(
private ZawgyiDetector $detector,
private ZawgyiConverter $converter,
) {}
public function isZawgyi(string $text): bool
{
return $this->detector->detectZawgyiProbability($text) > 0.95;
}
public function toUnicode(string $text): string
{
return $this->isZawgyi($text)
? $this->converter->zawgyiToUnicode($text)
: $text;
}
}
Bind it as a singleton in a service provider (the detector is stateful/expensive to construct):
$this->app->singleton(MyanmarText::class);
Then in a FormRequest:
protected function prepareForValidation(): void
{
if ($this->filled('bio')) {
$this->merge([
'bio' => app(MyanmarText::class)->toUnicode($this->input('bio')),
]);
}
}
Where to look first in the repo: php/ for ZawgyiDetector, ZawgyiConverter, and the model file; the README.md for the language-specific API names, which differ slightly between the C++, Java, JS, and PHP ports.
Even if you never swap implementations, wrapping it lets you fake it in tests and change thresholds in one place:
interface EncodingNormalizer
{
public function detect(string $text): float;
public function normalize(string $text): string;
}
Expose thresholds in config/myanmar.php:
return [
'zawgyi_threshold' => env('MYANMAR_ZAWGYI_THRESHOLD', 0.95),
'unicode_threshold' => env('MYANMAR_UNICODE_THRESHOLD', 0.05),
];
Treat the range between those as “unknown” and do not auto-convert — see Gotchas.
For apps with many write endpoints, a middleware that normalizes a whitelist of fields is cleaner than touching every controller:
public function handle($request, Closure $next)
{
$fields = config('myanmar.normalize_fields', ['name', 'bio', 'address']);
foreach ($fields as $field) {
if (is_string($request->input($field))) {
$request->merge([
$field => app(MyanmarText::class)->toUnicode($request->input($field)),
]);
}
}
return $next($request);
}
Keep it opt-in per route group; do not globally rewrite every string in every request.
Auto-conversion is a judgement call, and users may disagree. Persist both:
$table->text('bio');
$table->text('bio_original')->nullable();
$table->string('bio_encoding', 16)->nullable(); // unicode|zawgyi|unknown
This gives you an audit trail, a rollback path, and data to tune thresholds against later.
For model-level normalization, a custom cast keeps controllers thin:
class MyanmarTextCast implements CastsAttributes
{
public function get($model, $key, $value, $attributes) { return $value; }
public function set($model, $key, $value, $attributes)
{
return [$key => app(MyanmarText::class)->toUnicode((string) $value)];
}
}
Prefer this for content fields you own; avoid silently mutating fields where the raw bytes matter (e.g. imported legacy data).
The detector is not cheap per row. Chunk aggressively and push conversion into jobs:
User::whereNull('bio_normalized')
->chunkById(500, function ($users) {
foreach ($users as $user) {
NormalizeUserBio::dispatch($user->id);
}
});
Inside the job, re-fetch the model, detect, update bio, set bio_encoding, and log anything in the grey zone instead of converting.
Detection/conversion matters most for search. Normalize both sides:
MyanmarText service before hitting the index.If you cannot convert legacy content immediately, you still need to render it. Store the detected encoding and emit it to the frontend so you can switch font-family per record:
'font' => $encoding === 'zawgyi' ? 'Zawgyi-One' : 'Noto Sans Myanmar',
For APIs, return the encoding hint as metadata rather than converting responses on the fly; clients can decide how to render.
The JS port (npm) is useful when you want to detect in the browser before submitting — good UX for showing “this looks like Zawgyi; convert it?” and reducing server-side rewrite surprises. Keep the server-side conversion as the source of truth.
> 0.5 will corrupt legitimate Unicode text, which is worse than leaving Zawgyi in place.zawgyiToUnicode then unicodeToZawgyi (or vice versa) can change text. Never use it as a normalizer on already-Unicode content, and don't run the pipeline twice.zawgyiUnicodeModel.dat with the deployment, resolve its path with base_path() or a config value, and add a smoke test that instantiates the detector in CI. Paths that depend on the current working directory break under queue workers and Octane.new ZawgyiDetector() inside loops or Blade templates.Normalizer::normalize($text, Normalizer::FORM_C) (ext-intl) if you also deal with combining marks and ordering issues.\u{...} escapes or fixture files loaded with file_get_contents, and assert on probabilities with tolerances rather than exact floats.EncodingNormalizer boundary lets you move detection to the frontend or swap the model later without touching controllers.How can I help you explore Laravel packages today?