@oomfware/polyglot #
yet another localization library.
polyglot takes message resources authored in MessageFormat 2.0 and turns them into tree-shakeable TypeScript modules.
npm install --save-dev @oomfware/polyglot
quick start #
resources are written in the draft message-resource format.
@locale en
---
greeting = Hello!
welcome = Welcome, {$name}!
unread =
.input {$count :integer}
.match $count
0 {{No new messages.}}
one {{You have {$count} new message.}}
* {{You have {$count} new messages.}}
[account]
role =
.input {$kind :string}
.match $kind
admin {{Administrator}}
* {{Member}}
[account.billing]
status = Your plan renews on {$date :date}.
compile the resources
pnpm exec polyglot --base en --in locales --out src/polyglot
each entry becomes one message keyed by its full dotted identifier.
import { m } from './polyglot/messages.ts';
m['greeting']();
m['account.role']({ kind: 'admin' });
// -> "Administrator"
m['account.billing.status']({ date: new Date() });
see the MessageFormat 2.0 specification for the full message syntax — selectors, functions, markup, and escaping.
CLI #
polyglot --base <locale> --out <dir> [--in <dir>] [--functions <file>]
| flag | description |
|---|---|
-b, --base |
base locale, used as the fallback branch in generated messages |
-f, --functions |
module exporting custom functions (see below) |
-i, --in |
directory searched recursively for .poly files (default: .) |
-o, --out |
directory to write generated modules into |
polyglot replaces its output directory on each run to remove stale modules. it rejects non-empty
directories without a .polyglot marker and directories containing its inputs. compilation and
staging writes finish before existing output is moved or removed.
custom functions #
custom functions are imported by name from the module passed to --functions. declare them with
defineFunction, implementing format for placeholders and select for selectors.
literal options are strings; variable options retain their input values. generated message input types are inferred from the custom function's parameter types.
// src/l10n/functions.ts
import { defineFunction } from '@oomfware/polyglot';
export const relativetime = defineFunction({
format(
value: number,
options: { numeric?: 'always' | 'auto'; unit: Intl.RelativeTimeFormatUnit },
{ locale },
) {
return new Intl.RelativeTimeFormat(locale, { numeric: options.numeric }).format(
value,
options.unit,
);
},
});
export const dialect = defineFunction({
format(value: string) {
return value;
},
// return the unique matching keys, most preferred first.
select(value: string, _options, keys) {
return [...new Set([value, value.split('-')[0]])].filter((key) => keys.includes(key));
},
});
joined = {$name} joined {$value :relativetime unit=$unit numeric=auto}
spelling =
.input {$lang :dialect}
.match $lang
en-GB {{colour}}
en {{color}}
* {{colour}}
if later selectors rule out all variants for a preferred key, selection tries the next key, then
*. a namespaced function such as :ns:fn uses a string-named export: export { fn as 'ns:fn' }.
programmatic API #
to drive compilation from your own build, you can import from @oomfware/polyglot/compiler.
import { compile, CompileError, formatDiagnostic } from '@oomfware/polyglot/compiler';
try {
const { files, warnings } = compile({
baseLocale: 'en',
resources: [
{ path: 'locales/en.poly', text: enSource },
{ path: 'locales/fr.poly', text: frSource },
],
});
for (const { path, content } of files) {
// ...
}
for (const warning of warnings) {
console.warn(formatDiagnostic(warning));
}
} catch (error) {
if (!(error instanceof CompileError)) {
throw error;
}
console.error(formatDiagnostic(error));
process.exitCode = 1;
}