New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

intl-schematic

Package Overview
Dependencies
Maintainers
1
Versions
12
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

intl-schematic - npm Package Compare versions

Comparing version 0.0.3 to 1.0.0-rc.0

dist/es/plugins/arrays/index.js

91

dist/es/index.js

@@ -1,38 +0,57 @@

import { callPlugins } from './plugins/core';
export * from './ts.schema.d';
/**
* Creates a translation function (commonly known as `t()` or `$t()`)
*
* @param getLocaleDocument should return a translation document
* @param currentLocaleId should return a current Intl.Locale
* @param options
* @returns a tranlation function that accepts a key to look up in the translation document
*/
export const createTranslator = (getLocaleDocument, currentLocaleId, options = {}) => {
const { processors = {}, plugins = [], } = options;
const translate = function (key, input, parameter) {
export function createTranslator(getLocaleDocument, plugins) {
return (function translate(key, ...args) {
const doc = getLocaleDocument();
const callHook = (hook, value, _input = input) => callPluginsHook(hook, value, _input, parameter, currentLocaleId, key, doc) ?? key;
if (!doc) {
return callHook('docNotFound');
const contextPlugins = this.plugins ?? plugins ?? [];
for (const [index, plugin] of contextPlugins.entries())
if (plugin.match(doc[key], key, doc)) {
const pluginContext = createPluginContext.call(this, plugin, index);
try {
const pluginResult = plugin.translate.call(pluginContext, ...args);
if (typeof pluginResult === 'string') {
return pluginResult;
}
}
catch { }
}
const plainKey = doc[key];
return typeof plainKey === 'string' ? plainKey : key;
function createPluginContext(plugin, index) {
const contextualPlugins = contextPlugins.reduce((obj, pl) => ({
...obj,
[pl.name]: createPluginInterface(pl),
}), {});
const createdContext = {
name: plugin.name,
originalCallArgs: args,
originalKey: key,
originalValue: doc[key],
...this.pluginContext,
plugins: contextualPlugins,
doc,
key,
value: doc[key],
translate: translateFromContext,
};
return createdContext;
function translateFromContext(subkey, ...args) {
return translate.call({
plugins: subkey !== key
? contextPlugins
: contextPlugins?.slice(index),
pluginContext: createdContext,
}, subkey, ...args);
}
function createPluginInterface(pt) {
return {
translate: (subkey, ...args) => (pt.translate.call({
...createdContext,
key: subkey,
value: doc[subkey]
}, ...args)),
match: pt.match,
info: pt.info,
};
}
}
const currentKey = doc[key];
if (currentKey == null) {
return callHook('keyNotFound', key);
}
// Process a plain-string
if (typeof currentKey === 'string') {
return callHook('keyProcessed', currentKey);
}
// Process a function record
// TODO: move into a plugin
if (typeof currentKey === 'function') {
return callHook('keyProcessed', currentKey(...(Array.isArray(input) ? input : [])));
}
return callHook('keyFound', currentKey);
};
const callPluginsHook = callPlugins(translate, plugins);
// Initialize plugins
callPluginsHook('initPlugin', processors, undefined, undefined, currentLocaleId, '', undefined, undefined);
return translate;
};
}).bind({ plugins });
}

@@ -1,43 +0,1 @@

export const callPlugins = (translate, plugins = []) => {
const pluginsPerHook = plugins.reduce((obj, plugin) => {
for (const _hookName in plugin)
if (typeof plugin[_hookName] === 'function') {
const hookName = _hookName;
const hook = plugin[hookName];
if (hookName in obj) {
obj[hookName].push(hook);
}
else {
obj[hookName] = [hook];
}
}
return obj;
}, {});
const callPluginsForHook = (hook, ...[value, input, parameter, currentLocaleId, key, doc, initiatorPlugin]) => {
if (!pluginsPerHook[hook]) {
return value == null ? undefined : String(value);
}
let val = value;
for (const pluginHook of pluginsPerHook[hook]) {
const pluginResult = pluginHook.call({
callHook(_hook, value) {
if (hook === _hook) {
// Prevent recursion
return;
}
return callPluginsForHook(_hook, value, input, parameter, currentLocaleId, key, doc, pluginHook.name);
},
translate
}, val, input, parameter, currentLocaleId, key, doc, initiatorPlugin);
if (typeof pluginResult === 'string') {
return pluginResult;
}
if (pluginResult != null) {
val = pluginResult;
}
}
return val == null ? undefined : String(val);
};
return callPluginsForHook;
};
export const createPlugin = (plugin) => plugin;
export const createPlugin = (name, match, options) => ({ name, match, translate: options.translate ?? (() => undefined), info: options.info });

@@ -1,10 +0,8 @@

import { ArrayRecordPlugin } from './array-record';
import { ObjectRecordPlugin } from './object-record';
import { ProcessorPlugin } from './processed-record';
import { ResolveMissingKeyPlugin } from './resolve-missing';
export const defaultPlugins = [
ProcessorPlugin,
ArrayRecordPlugin,
ObjectRecordPlugin,
ResolveMissingKeyPlugin,
import { LocaleProviderPlugin } from './locale';
import { ArraysPlugin } from './arrays';
import { ProcessorsPlugin } from './processors/plugin';
export const defaultPlugins = (currentLocale, processors) => [
LocaleProviderPlugin(currentLocale),
ArraysPlugin,
ProcessorsPlugin(processors),
];

@@ -1,38 +0,57 @@

import { callPlugins } from './plugins/core';
export * from './ts.schema.d';
/**
* Creates a translation function (commonly known as `t()` or `$t()`)
*
* @param getLocaleDocument should return a translation document
* @param currentLocaleId should return a current Intl.Locale
* @param options
* @returns a tranlation function that accepts a key to look up in the translation document
*/
export const createTranslator = (getLocaleDocument, currentLocaleId, options = {}) => {
const { processors = {}, plugins = [], } = options;
const translate = function (key, input, parameter) {
export function createTranslator(getLocaleDocument, plugins) {
return (function translate(key, ...args) {
const doc = getLocaleDocument();
const callHook = (hook, value, _input = input) => callPluginsHook(hook, value, _input, parameter, currentLocaleId, key, doc) ?? key;
if (!doc) {
return callHook('docNotFound');
const contextPlugins = this.plugins ?? plugins ?? [];
for (const [index, plugin] of contextPlugins.entries())
if (plugin.match(doc[key], key, doc)) {
const pluginContext = createPluginContext.call(this, plugin, index);
try {
const pluginResult = plugin.translate.call(pluginContext, ...args);
if (typeof pluginResult === 'string') {
return pluginResult;
}
}
catch { }
}
const plainKey = doc[key];
return typeof plainKey === 'string' ? plainKey : key;
function createPluginContext(plugin, index) {
const contextualPlugins = contextPlugins.reduce((obj, pl) => ({
...obj,
[pl.name]: createPluginInterface(pl),
}), {});
const createdContext = {
name: plugin.name,
originalCallArgs: args,
originalKey: key,
originalValue: doc[key],
...this.pluginContext,
plugins: contextualPlugins,
doc,
key,
value: doc[key],
translate: translateFromContext,
};
return createdContext;
function translateFromContext(subkey, ...args) {
return translate.call({
plugins: subkey !== key
? contextPlugins
: contextPlugins?.slice(index),
pluginContext: createdContext,
}, subkey, ...args);
}
function createPluginInterface(pt) {
return {
translate: (subkey, ...args) => (pt.translate.call({
...createdContext,
key: subkey,
value: doc[subkey]
}, ...args)),
match: pt.match,
info: pt.info,
};
}
}
const currentKey = doc[key];
if (currentKey == null) {
return callHook('keyNotFound', key);
}
// Process a plain-string
if (typeof currentKey === 'string') {
return callHook('keyProcessed', currentKey);
}
// Process a function record
// TODO: move into a plugin
if (typeof currentKey === 'function') {
return callHook('keyProcessed', currentKey(...(Array.isArray(input) ? input : [])));
}
return callHook('keyFound', currentKey);
};
const callPluginsHook = callPlugins(translate, plugins);
// Initialize plugins
callPluginsHook('initPlugin', processors, undefined, undefined, currentLocaleId, '', undefined, undefined);
return translate;
};
}).bind({ plugins });
}

@@ -1,43 +0,1 @@

export const callPlugins = (translate, plugins = []) => {
const pluginsPerHook = plugins.reduce((obj, plugin) => {
for (const _hookName in plugin)
if (typeof plugin[_hookName] === 'function') {
const hookName = _hookName;
const hook = plugin[hookName];
if (hookName in obj) {
obj[hookName].push(hook);
}
else {
obj[hookName] = [hook];
}
}
return obj;
}, {});
const callPluginsForHook = (hook, ...[value, input, parameter, currentLocaleId, key, doc, initiatorPlugin]) => {
if (!pluginsPerHook[hook]) {
return value == null ? undefined : String(value);
}
let val = value;
for (const pluginHook of pluginsPerHook[hook]) {
const pluginResult = pluginHook.call({
callHook(_hook, value) {
if (hook === _hook) {
// Prevent recursion
return;
}
return callPluginsForHook(_hook, value, input, parameter, currentLocaleId, key, doc, pluginHook.name);
},
translate
}, val, input, parameter, currentLocaleId, key, doc, initiatorPlugin);
if (typeof pluginResult === 'string') {
return pluginResult;
}
if (pluginResult != null) {
val = pluginResult;
}
}
return val == null ? undefined : String(val);
};
return callPluginsForHook;
};
export const createPlugin = (plugin) => plugin;
export const createPlugin = (name, match, options) => ({ name, match, translate: options.translate ?? (() => undefined), info: options.info });

@@ -1,10 +0,8 @@

import { ArrayRecordPlugin } from './array-record';
import { ObjectRecordPlugin } from './object-record';
import { ProcessorPlugin } from './processed-record';
import { ResolveMissingKeyPlugin } from './resolve-missing';
export const defaultPlugins = [
ProcessorPlugin,
ArrayRecordPlugin,
ObjectRecordPlugin,
ResolveMissingKeyPlugin,
import { LocaleProviderPlugin } from './locale';
import { ArraysPlugin } from './arrays';
import { ProcessorsPlugin } from './processors/plugin';
export const defaultPlugins = (currentLocale, processors) => [
LocaleProviderPlugin(currentLocale),
ArraysPlugin,
ProcessorsPlugin(processors),
];
{
"name": "intl-schematic",
"version": "0.0.3",
"version": "1.0.0-rc.0",
"license": "MIT",

@@ -11,6 +11,9 @@ "repository": {

".": "./src/index",
"./processors": "./src/processors/index",
"./plugins/processors": "./src/plugins/processors/plugin",
"./processors": "./src/plugins/processors/index",
"./processors/*": "./src/plugins/processors/*",
"./plugins": "./src/plugins/index",
"./plugins/*": "./src/plugins/*",
"./translation.schema": "./src/translation.schema",
"./ts.schema": "./src/ts.schema"
"./schema": "./src/ts.schema"
},

@@ -23,9 +26,21 @@ "typesVersions": {

"processors": [
"./src/processors/index.ts"
"./src/plugins/processors/index.ts"
],
"plugins/processors": [
"./src/plugins/processors/plugin.ts"
],
"processors/*": [
"./src/plugins/processors/*"
],
"plugins": [
"./src/plugins/index.ts"
],
"plugins/*": [
"./src/plugins/*"
],
"translation.schema": [
"./src/translation.schema.d.ts"
],
"schema": [
"./src/ts.schema.d.ts"
]

@@ -32,0 +47,0 @@ }

@@ -1,34 +0,15 @@

# intl-schematic (WIP)
<h1 align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Raiondesu/intl-schematic/main/logo/Dark%20Logo.svg">
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/Raiondesu/intl-schematic/main/logo/Light%20Logo.svg">
<img alt="intl-schematic" src="https://raw.githubusercontent.com/Raiondesu/intl-schematic/main/logo/Light%20Logo.svg">
</picture>
</h1>
<p align="center">
A tiny library (3kb, zero-dependency) that allows to localize and format strings while sparingly using the browser-standard [`Intl` API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl).
Key features include:
- **Full type-safety**: full autocomplete on translation keys, typed translation parameters and more;
- **Tree-shakable**: only take what you need;
- **Pluginable**: extend any processing step without limits;
- **JSON-validation using a JSON-schema**: intellisense and popup hints right in the translation document;
- **Dynamic strings with custom pre-processors**: write custom translation logic right in JSON;
- **Reference translation keys inside of other translation keys**: all with JSON-compatible syntax;
- **No string-interpolation**: translation strings will never be processed or mangled by-default, so all unicode symbols are safe to use;
- **Basic localized formatters**: declare formatting rules and translations in the same place.
</p>
## Why
I've grown frustrated with current implementations of popular l10n/i18n libraries, many of which:
- lack runtime JSON support,
- rely on custom-written localization logic (a lot of which is already implemented in `Intl`),
- are over-tailored to specific frameworks or SaaS solutions,
- lack support for modular translation documents or asynchronous/real-time localization,
- interpolate over translated strings - resulting in overreliance on custom string template syntax - different for each library,
- force a certain architecture on a project.
This library will try to avoid these common pitfalls, while retaining a small size and good performance.
## No-goals
This library will **not** support:
- **Translation key nesting**: needlessly complicates key lookup and maintenance, use namespaced keys instead;
- **String interpolation**: while custom processors can do anything with the translated string, the library by-itself does not and will not do any processing on the strings.
## Usage

@@ -44,13 +25,10 @@

const en = {
"hello": "Hello, World!",
"hello-name": name => `Hello, ${name}!`
"hello": "Hello, World!"
};
```
### Define functions that return a translation document and a locale
### Define a function that return a translation document
```js
const getDocument = () => en;
const getLocale = () => new Intl.Locale('en')
```

@@ -63,3 +41,3 @@

const t = createTranslator(getDocument, getLocale);
const t = createTranslator(getDocument);
```

@@ -71,3 +49,2 @@

console.log(t('hello')); // `Hello, World!`
console.log(t('hello-name', ['Bob'])); // `Hello, Bob!`
```

@@ -78,4 +55,5 @@

These allow to use standard [Intl](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl) features,
like [`DateTimeFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat)
or [`PluralRules`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules).
like [`DateTimeFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat),
[`PluralRules`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules)
and [`DisplayNames`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames).

@@ -100,8 +78,10 @@ ```js

const t = createTranslator(getDocument, getLocale, {
plugins: defaultPlugins,
processors: defaultProcessors,
});
const getLocale = () => new Intl.Locale('en');
const t = createTranslator(getDocument, defaultPlugins(
getLocale
defaultProcessors
));
console.log(t('price', 123)); // "US$123"
```

@@ -1,12 +0,7 @@

import type { LocaleInputParameter, LocaleKey, LocaleOptionsParameter, Translation, TranslationProxy } from './ts.schema';
import type { Processors, defaultProcessors } from './processors';
import { callPlugins } from './plugins/core';
import type { Plugin } from './plugins/core';
import type { LocaleKey } from './ts.schema';
import type { PluginContext, PluginInterface, Plugin, PluginRegistry } from './plugins/core';
export * from './ts.schema.d';
// TODO: decouple processor architecture from plugins
interface Options<P extends Processors, Locale extends Translation> {
processors?: P;
plugins?: Plugin<Locale, P>[];
interface TranslationContext {
plugins?: readonly Plugin[];
pluginContext?: PluginContext;
}

@@ -18,71 +13,157 @@

* @param getLocaleDocument should return a translation document
* @param currentLocaleId should return a current Intl.Locale
* @param options
* @returns a tranlation function that accepts a key to look up in the translation document
*/
export const createTranslator: {
<Locale extends Translation>(
getLocaleDocument: () => Locale | undefined,
currentLocaleId: () => Intl.Locale | undefined,
options?: Omit<Options<typeof defaultProcessors, Locale>, 'processors'>,
): TranslationProxy<Locale, typeof defaultProcessors>;
<Locale extends Translation, P extends Processors>(
getLocaleDocument: () => Locale | undefined,
currentLocaleId: () => Intl.Locale | undefined,
options?: Options<P, Locale>
): TranslationProxy<Locale, P>;
} = <Locale extends Translation>(
export function createTranslator<Locale extends Record<string, string>>(
getLocaleDocument: () => Locale | undefined,
currentLocaleId: () => Intl.Locale | undefined,
options: Options<Processors, Locale> = {},
) => {
const {
processors = {} as Processors,
plugins = [],
} = options;
): SimpleTranslationFunction<Locale>;
const translate = function <K extends LocaleKey<Locale>>(
key: K,
input?: LocaleInputParameter<Locale, LocaleKey<Locale>, Processors>,
parameter?: LocaleOptionsParameter<Locale, LocaleKey<Locale>, Processors>
): string {
/**
* Creates a translation function (commonly known as `t()` or `$t()`)
*
* @param getLocaleDocument should return a translation document
* @param plugins an array of plugins, each will be applied to the translation key in their respective order
* @returns a tranlation function that accepts a key to look up in the translation document
*/
export function createTranslator<
const P extends readonly Plugin[],
LocaleDoc extends Record<string, PMatch<P> | string>,
>(
getLocaleDocument: () => LocaleDoc,
plugins: P,
): TranslationFunction<LocaleDoc, P>;
export function createTranslator<
const P extends readonly Plugin[],
LocaleDoc extends Record<string, PMatch<P> | string>,
>(
getLocaleDocument: () => LocaleDoc,
plugins?: P,
): any {
return (function translate(this: TranslationContext, key: string, ...args: unknown[]) {
const doc = getLocaleDocument();
const callHook = (
hook: keyof Omit<Plugin<Locale, Processors>, 'name'>,
value?: unknown,
_input: typeof input = input,
) => callPluginsHook(hook, value, _input, parameter, currentLocaleId, key, doc) ?? key;
const contextPlugins = this.plugins ?? plugins ?? [];
if (!doc) {
return callHook('docNotFound');
}
for (const [index, plugin] of contextPlugins.entries())
if (plugin.match(doc[key], key, doc)) {
const pluginContext: PluginContext = createPluginContext.call(this, plugin, index);
const currentKey = doc[key];
// Do not break if a plugin stops working
try {
const pluginResult = plugin.translate.call(pluginContext, ...args);
if (currentKey == null) {
return callHook('keyNotFound', key);
}
if (typeof pluginResult === 'string') {
return pluginResult;
}
} catch {}
}
// Process a plain-string
if (typeof currentKey === 'string') {
return callHook('keyProcessed', currentKey);
}
const plainKey = doc[key];
// Process a function record
// TODO: move into a plugin
if (typeof currentKey === 'function') {
return callHook('keyProcessed', currentKey(...(Array.isArray(input) ? input : [])));
return typeof plainKey === 'string' ? plainKey : key;
function createPluginContext(
this: TranslationContext,
plugin: Plugin,
index: number
): PluginContext {
const contextualPlugins = contextPlugins.reduce<PluginContext['plugins']>((obj, pl) => ({
...obj,
[pl.name]: createPluginInterface(pl),
}), {});
const createdContext: PluginContext = {
name: plugin.name,
originalCallArgs: args,
originalKey: key,
originalValue: doc[key],
...this.pluginContext,
plugins: contextualPlugins,
doc,
key,
value: doc[key],
translate: translateFromContext,
};
return createdContext;
function translateFromContext(subkey: string, ...args: unknown[]) {
return translate.call({
plugins: subkey !== key
? contextPlugins
: contextPlugins?.slice(index),
pluginContext: createdContext,
}, subkey, ...args)
}
function createPluginInterface(pt: Plugin): PluginInterface | undefined {
return {
translate: (subkey, ...args) => (
pt.translate.call({
...createdContext,
key: subkey,
value: doc[subkey]
}, ...args)
),
match: pt.match,
info: pt.info,
};
}
}
}).bind({ plugins });
}
return callHook('keyFound', currentKey);
} as TranslationProxy<Locale, Processors>;
export type PMatch<P extends readonly Plugin[]> = (
[] extends P ? never : P extends readonly Plugin<infer Match, any>[] ? Match : never
);
const callPluginsHook = callPlugins(translate, plugins);
type NamePerPlugin<P extends readonly Plugin[]> = {
[key in keyof P]: P[key] extends Plugin<any, any, infer Name> ? Name : never;
};
// Initialize plugins
callPluginsHook('initPlugin', processors, undefined, undefined, currentLocaleId, '', undefined, undefined);
type MatchPerPlugin<P extends readonly Plugin[], Names extends NamePerPlugin<P> = NamePerPlugin<P>> = {
[key in keyof Names & keyof P & `${number}` as Names[key]]: P[key] extends Plugin<infer Match, any> ? Match : never;
};
return translate;
type InfoPerPlugin<P extends readonly Plugin[], Names extends NamePerPlugin<P> = NamePerPlugin<P>> = {
[key in keyof Names & keyof P & `${number}` as Names[key]]: P[key] extends Plugin<any, any, any, infer Info> ? Info : never;
};
type PluginPerPlugin<P extends readonly Plugin[], Names extends NamePerPlugin<P> = NamePerPlugin<P>> = {
[key in keyof Names & keyof P & `${number}` as Names[key]]: P[key] extends Plugin ? P[key] : never;
};
type KeysOfType<O, T> = {
[K in keyof O]: T extends O[K] ? K : never
}[keyof O];
export type SimpleTranslationFunction<LocaleDoc extends Record<string, any>> = {
(key: LocaleKey<LocaleDoc>): string;
};
type FlatType<T> = T extends object ? { [K in keyof T]: FlatType<T[K]> } : T;
export type TranslationFunction<
LocaleDoc extends Record<string, any>,
P extends readonly Plugin[]
> = {
/**
* Translate a key from a translation document
*
* @param key a key to translate from
* @param args optional parameters for plugins used for the chosen key
*/
<
K extends LocaleKey<LocaleDoc>,
PluginKey extends KeysOfType<MatchPerPlugin<P>, LocaleDoc[K]> = KeysOfType<MatchPerPlugin<P>, LocaleDoc[K]>,
_Signature = FlatType<PluginRegistry<LocaleDoc, K, InfoPerPlugin<P>[PluginKey], PluginPerPlugin<P>>[PluginKey]['signature']>
>(
key: K,
...args: PluginRegistry<LocaleDoc, K, InfoPerPlugin<P>[PluginKey], PluginPerPlugin<P>>[PluginKey]['args']
): string;
}

@@ -1,96 +0,157 @@

import { Translation, LocaleInputParameter, LocaleKey, TranslationProxy, LocaleOptionsParameter } from '../ts.schema';
import { LocaleKey } from '../ts.schema';
export type PluginHook<Locale extends Translation, Processors> = (
this: {
translate: TranslationProxy<Locale, Processors>
callHook: (
hook: PluginHooks,
value?: unknown
) => string | undefined;
},
value: unknown | undefined,
input: LocaleInputParameter<Locale, LocaleKey<Locale>, Processors> | undefined,
parameter: LocaleOptionsParameter<Locale, LocaleKey<Locale>, Processors> | undefined,
currentLocaleId: () => Intl.Locale | undefined,
key: string,
translationDocument: Locale | undefined,
initiatorPlugin?: string | undefined
) => string | undefined;
/**
* Opt-in global plugin registry,
* tracks all plugins included throughout the project to simplify type-checking
*/
export interface PluginRegistry<
Locale extends Record<string, any> = Record<string, any>,
Key extends LocaleKey<Locale> = LocaleKey<Locale>,
PluginInfo = unknown,
ContextualPlugins extends Record<string, Plugin> = Record<string, Plugin>
> {
[name: string]: {
/**
* Arguments to require when translating a key that matches this plugin
*
* Should be a named tuple
*/
args: unknown[];
export interface Plugin<Locale extends Translation, Processors> {
name: string;
initPlugin?: PluginHook<Locale, Processors>;
docNotFound?: PluginHook<Locale, Processors>;
keyNotFound?: PluginHook<Locale, Processors>;
keyFound?: PluginHook<Locale, Processors>;
processorFound?: PluginHook<Locale, Processors>;
processorNotFound?: PluginHook<Locale, Processors>;
keyProcessed?: PluginHook<Locale, Processors>;
keyNotProcessed?: PluginHook<Locale, Processors>;
/**
* Miscellanious information that the plugin uses
*
* This is a generic type that is then used as a type guard in PluginRegistry evaluation
*/
info?: unknown;
/**
* This is displayed in a type hint when the user hovers over the translation function invocation
*
* Allows to display any important information (for example, original key signature) to the user
*/
signature?: unknown;
};
}
type PluginHooks = keyof Omit<Plugin<any, any>, 'name'>;
/**
* An interface that other plugins use
* to reference another plugin in their code
*/
export interface PluginInterface<
LocaleDoc extends Record<string, any> = Record<string, any>,
Key extends LocaleKey<LocaleDoc> = LocaleKey<LocaleDoc>,
Name extends keyof PluginRegistry<LocaleDoc> = keyof PluginRegistry<LocaleDoc>,
> {
translate(key: LocaleKey<LocaleDoc>, ...args: PluginRegistry<LocaleDoc, Key>[Name]['args']): string | undefined;
match(value: unknown, key: string, doc: Record<string, unknown>): boolean;
info: Exclude<PluginRegistry<LocaleDoc, Key>[Name]['info'], undefined>;
}
export const callPlugins = <Locale extends Translation, Processors>(
translate: TranslationProxy<Locale, Processors>,
plugins: Plugin<Locale, Processors>[] = [],
) => {
const pluginsPerHook = plugins.reduce((obj, plugin) => {
for (const _hookName in plugin) if (typeof plugin[_hookName as PluginHooks] === 'function') {
const hookName = _hookName as PluginHooks;
const hook = plugin[hookName] as PluginHook<Locale, Processors>;
/**
* Context of the plugin's `translate` function
*/
export interface PluginContext<
Match = any,
LocaleDoc extends Record<string, any> = Record<string, any>,
Key extends LocaleKey<LocaleDoc> = LocaleKey<LocaleDoc>,
Name extends keyof PluginRegistry = string,
> {
name: Name;
key: Key;
value: Match;
doc: LocaleDoc;
originalCallArgs: unknown[];
originalKey: LocaleKey<LocaleDoc>;
originalValue: unknown;
translate(key: LocaleKey<LocaleDoc>, ...args: unknown[]): string;
plugins: {
[name in keyof PluginRegistry<LocaleDoc, Key>]?: PluginInterface<LocaleDoc, Key, name>;
};
}
if (hookName in obj) {
obj[hookName].push(hook);
} else {
obj[hookName] = [hook];
}
}
export interface Plugin<
Match = any,
Args extends any[] = any,
Name extends keyof PluginRegistry = string,
PluginInfo = unknown,
LocaleDoc extends Record<string, any> = Record<string, any>,
Key extends LocaleKey<LocaleDoc> = LocaleKey<LocaleDoc>,
> {
name: Name;
info: PluginInfo;
match(value: unknown, key: Key, doc: LocaleDoc): value is Match;
translate(this: PluginContext<Match, LocaleDoc, Key, Name>, ...args: Args): string | undefined;
}
return obj;
}, {} as Record<PluginHooks, PluginHook<Locale, Processors>[]>);
const callPluginsForHook = (hook: PluginHooks, ...[
value,
input,
parameter,
currentLocaleId,
key,
doc,
initiatorPlugin
]: Parameters<PluginHook<Locale, Processors>>): string | undefined => {
if (!pluginsPerHook[hook]) {
return value == null ? undefined : String(value);
/**
* A plugin factory, mostly used for type-checking
*
* @param name A name for the plugin, will be used as a global plugin registry shortcut for other plugins,
* must match with the name used in the plugin registry definition
*
* @param match A {@link https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates type predicate}
* that decides whether or not the plugin should be used on a specific key-value pair
*
* @param options Allows to define the functionality of a plugin. It can do 2 things:
* 1. Provide info and context to other plugins, using the `info` property
* 2. Provide additional ways of translating a key-value pair from a translation document, using the `translate` method
*
* @returns a ready-to-use plugin
*/
export const createPlugin: {
/**
* A plugin factory, mostly used for type-checking
*
* @param name A name for the plugin, will be used as a global plugin registry shortcut for other plugins,
* must match with the name used in the plugin registry definition
*
* @param match A {@link https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates type predicate}
* that decides whether or not the plugin should be used on a specific key-value pair
*
* @param options Allows to define the functionality of a plugin. It can do 2 things:
* 1. Provide info and context to other plugins, using the `info` property
* 2. Provide additional ways of translating a key-value pair from a translation document, using the `translate` method
*
* @returns a ready-to-use plugin
*/
<Name extends keyof PluginRegistry, Match, PluginInfo, Args extends any[] = PluginRegistry[Name]['args']>(
name: Name,
match: (value: unknown) => value is Match,
options: {
info?: PluginInfo,
translate?: (this: PluginContext<Match>, ...args: Args) => string | undefined,
}
): Plugin<Match, Args, Name, PluginInfo>;
let val = value;
for (const pluginHook of pluginsPerHook[hook]) {
const pluginResult = pluginHook.call({
callHook(_hook, value) {
if (hook === _hook) {
// Prevent recursion
return;
}
return callPluginsForHook(_hook, value, input, parameter, currentLocaleId, key, doc, pluginHook.name);
},
translate
}, val, input, parameter, currentLocaleId, key, doc, initiatorPlugin);
if (typeof pluginResult === 'string') {
return pluginResult;
}
if (pluginResult != null) {
val = pluginResult;
}
/**
* A plugin factory, mostly used for type-checking
*
* @param name A name for the plugin, will be used as a global plugin registry shortcut for other plugins,
* must match with the name used in the plugin registry definition
*
* @param match A {@link https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates type predicate}
* that decides whether or not the plugin should be used on a specific key-value pair
*
* @param options Allows to define the functionality of a plugin.
* It can provide additional ways of translating a key-value pair
* from a translation document, using the `translate` method
*
* @returns a ready-to-use plugin
*/
<Match, Args extends any[]>(
name: string,
match: (value: unknown) => value is Match,
options: {
translate?: (this: PluginContext<Match>, ...args: Args) => string | undefined,
}
): Plugin<Match, Args>;
} = <Match, Args extends any[]>(
name: string,
match: (value: unknown) => value is Match,
options: {
info?: unknown,
translate?: (this: PluginContext<Match>, ...args: Args) => string | undefined,
}
): Plugin<Match, Args> => ({ name, match, translate: options.translate ?? (() => undefined), info: options.info });
return val == null ? undefined : String(val);
};
return callPluginsForHook;
};
export const createPlugin = <T extends Plugin<any, any>>(plugin: T): T => plugin;

@@ -1,11 +0,21 @@

import { ArrayRecordPlugin } from './array-record';
import { ObjectRecordPlugin } from './object-record';
import { ProcessorPlugin } from './processed-record';
import { ResolveMissingKeyPlugin } from './resolve-missing';
import { LocaleProviderPlugin } from './locale';
import { ArraysPlugin } from './arrays';
import { Processors, ProcessorsPlugin } from './processors/plugin';
import { defaultProcessors } from './processors';
export const defaultPlugins = [
ProcessorPlugin,
ArrayRecordPlugin,
ObjectRecordPlugin,
ResolveMissingKeyPlugin,
];
/**
* Default schematic plugins
*
* Allow to:
* - Access a user's locale in other plugins
* - Process translation keys with custom processors
* - Join and cross-reference translation records using arrays and object
*/
export const defaultPlugins = <P extends Processors = typeof defaultProcessors>(
currentLocale: () => Intl.Locale | undefined,
processors: P
) => [
LocaleProviderPlugin(currentLocale),
ArraysPlugin,
ProcessorsPlugin(processors),
] as const;

@@ -1,73 +0,9 @@

import type {
PlainStringTranslationRecord,
ParametrizedTranslationRecord,
PlainStringTranslationRecordWithReferences,
} from './translation.schema';
import type { InputParameter, OptionsParameter } from './processors';
export interface TranslationModule {
[k: string]:
| PlainStringTranslationRecord
| ParametrizedTranslationRecord
| PlainStringTranslationRecordWithReferences
| ((...args: any[]) => string);
export interface TranslationDocuemnt {
[key: string]: unknown;
}
export type Translation = TranslationModule;
export type LocaleKey<Locale extends TranslationDocuemnt> = Exclude<keyof Locale, '$schema'>;
export type LocaleKey<Locale extends Translation> = Extract<keyof Omit<Locale, '$schema'>, string>;
type ExtraPartial<I> = {
export type ExtraPartial<I> = {
[P in keyof I]?: I[P] | null | undefined;
};
export type LocaleInputParameter<
Locale extends Translation,
K extends LocaleKey<Locale>,
P extends Processors,
> = null | (
Locale[K] extends { processor: infer O; input: infer I; }
? keyof O extends keyof P
? ExtraPartial<InputParameter<P, keyof O>>
: ExtraPartial<I>
: Locale[K] extends Array<Record<infer Key, any> | string>
? { [key in LocaleKey<Locale> & Key]?: LocaleInputParameter<Locale, key, P>; }
: Locale[K] extends Record<infer Key, any>
? { [key in LocaleKey<Locale> & Key]?: LocaleInputParameter<Locale, key, P>; }
: string
);
export type LocaleOptionsParameter<
Locale extends Translation,
K extends LocaleKey<Locale>,
P extends Processors,
> = null | (
Locale[K] extends { processor: infer O; parameter: infer I; }
? keyof O extends keyof P
? ExtraPartial<OptionsParameter<P, keyof O>>
: ExtraPartial<I>
: Locale[K] extends Record<string, any>
? { [key in LocaleKey<Locale> & keyof Locale[K]]?: LocaleOptionsParameter<Locale, key, P>; }
: string
);
export type TranslationFunction<Locale extends Translation, P extends Processors, R = string> = {
/**
* A translation function, looks for a specified key in the local translation document to provide a localized string.
* @param key
* - a key from a translation document (usually `{locale}.json`)
* @param input
* -- a default value (in case a plain-string translation for the key isn't found)
* - an input parameter, if the locale key is parametrized
* @returns a translated string
*/
<K extends LocaleKey<Locale>>(
key: K,
input?: Locale[K] extends (...args: infer A) => string
? { args: A }
: LocaleInputParameter<Locale, K, P> | null,
parameter?: LocaleOptionsParameter<Locale, K, P>,
): R;
};
export type TranslationProxy<Locale extends Translation, P extends Processors> = TranslationFunction<Locale, P>;

@@ -12,4 +12,4 @@ {

"anyOf": [{
"title": "Reference to an input of another paramterized record",
"description": "Value must refer to a paramterized record that is already referenced in this array",
"title": "Reference to an input of another parametrized record",
"description": "Value must refer to a parametrized record that is already referenced in this array",
"type": "string",

@@ -16,0 +16,0 @@ "pattern": "input:.+"

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc