intl-schematic
Advanced tools
Comparing version 0.0.0 to 0.0.1
import type { Translation, TranslationProxy } from './ts.schema'; | ||
import type { Processors, defaultProcessors } from './processors'; | ||
import type { Plugin } from './plugins'; | ||
import type { Plugin } from './plugins/core'; | ||
export * from './ts.schema.d'; | ||
interface Options<P extends Processors, Locale extends Translation> { | ||
processors?: P; | ||
plugins?: Plugin<Locale>[]; | ||
plugins?: Plugin<Locale, P>[]; | ||
} | ||
/** | ||
* 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 declare const createTranslator: { | ||
@@ -10,0 +18,0 @@ <Locale extends Translation>(getLocaleDocument: () => Locale | undefined, currentLocaleId: () => Intl.Locale | undefined, options?: Omit<Options<typeof defaultProcessors, Locale>, 'processors'>): TranslationProxy<Locale, typeof defaultProcessors>; |
@@ -1,58 +0,26 @@ | ||
import { ResolveMissingKeyPlugin, callPlugins } from './plugins'; | ||
import { callPlugins } from './plugins/core'; | ||
export * from './ts.schema.d'; | ||
const isProcessedKey = (k) => 'processor' in k && typeof k['processor'] === 'object'; | ||
/** | ||
* 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 = [ResolveMissingKeyPlugin], } = options; | ||
const plugin = callPlugins(plugins); | ||
const localizedProcessorsByLocale = {}; | ||
function getLocalizedProcessors() { | ||
const localeId = currentLocaleId(); | ||
if (!localeId) { | ||
return {}; | ||
} | ||
return Object.keys(processors).reduce((obj, key) => ({ | ||
...obj, | ||
[key]: processors[key](localeId), | ||
}), {}); | ||
} | ||
return function translate(key, input, parameter) { | ||
var _a; | ||
const { processors = {}, plugins = [], } = options; | ||
const translate = function (key, input, parameter) { | ||
const doc = getLocaleDocument(); | ||
const call = (hook, value, processor, _input = input) => plugin(hook, key, _input, value, processor, doc) ?? key; | ||
const callHook = (hook, value, processor, _input = input) => callPluginsHook(hook, value, _input, parameter, currentLocaleId, key, doc, processor) ?? key; | ||
if (!doc) { | ||
return call('docNotFound'); | ||
return callHook('docNotFound'); | ||
} | ||
const localizedProcessors = (localizedProcessorsByLocale[_a = String(currentLocaleId()?.baseName)] ?? (localizedProcessorsByLocale[_a] = getLocalizedProcessors())); | ||
const currentKey = doc[key]; | ||
if (typeof currentKey === 'undefined') { | ||
return call('keyNotFound'); | ||
return callHook('keyNotFound'); | ||
} | ||
// Process an array record (["Some text", "translation-key"]) | ||
// TODO: move into a plugin | ||
if (Array.isArray(currentKey)) { | ||
const result = currentKey.reduce((arr, refK) => { | ||
if (typeof refK !== 'string') { | ||
const refParamK = Object.keys(refK)[0]; | ||
if (refParamK.startsWith('input:')) { | ||
const key = refParamK.replace('input:', ''); | ||
const value = input?.[key]; | ||
return [ | ||
...arr, | ||
// TOOD: add a way to get a stringifier for a processors input | ||
String(value) | ||
]; | ||
} | ||
if (refK.__ignore) { | ||
return arr; | ||
} | ||
return [...arr, translate(refParamK, input?.[refParamK], parameter?.[refParamK])]; | ||
} | ||
if (!refK.startsWith('input:')) { | ||
return [...arr, translate(refK, input?.[refK], parameter?.[refK])]; | ||
} | ||
const _input = input; | ||
const inputKey = refK.replace('input:', ''); | ||
return [...arr, _input[inputKey]]; | ||
}, []).join(' '); | ||
return call('keyProcessed', result); | ||
// Process a plain-string | ||
if (typeof currentKey !== 'object' && typeof currentKey !== 'function') { | ||
return currentKey ? callHook('keyProcessed', currentKey) : callHook('keyNotFound'); | ||
} | ||
@@ -62,51 +30,10 @@ // Process a function record | ||
if (typeof currentKey === 'function') { | ||
const inputContainsArgs = typeof input === 'object' && input && 'args' in input && Array.isArray(input.args); | ||
return call('keyProcessed', currentKey(...(inputContainsArgs ? input.args : []))); | ||
return callHook('keyProcessed', currentKey(...(Array.isArray(input) ? input : []))); | ||
} | ||
// Process a plain-string | ||
if (typeof currentKey !== 'object') { | ||
return currentKey ? call('keyFound', currentKey) : call('keyNotFound'); | ||
} | ||
// Process an object record that doesn't specify a processor | ||
// TODO: move into a plugin | ||
if (!isProcessedKey(currentKey)) { | ||
return call('keyProcessed', Object.keys(currentKey).map((refKey) => { | ||
const inputForKey = typeof input === 'object' && input ? input[refKey] : {}; | ||
const translated = translate(refKey, (typeof input === 'object' && input | ||
? mergeInputs(currentKey[refKey], inputForKey ?? null) | ||
: currentKey[refKey])); | ||
return translated; | ||
}).join(' ').replace(/\s+/, ' ')); | ||
} | ||
// Process a parametrized record using a processor: | ||
// TODO: move into a plugin | ||
const processorName = Object.keys(currentKey.processor)[0]; | ||
const processor = localizedProcessors[processorName]; | ||
if (!processor) { | ||
return call('processorNotFound', key, processorName); | ||
} | ||
// Delete undefined keys to make defaults bypass them in the spread later | ||
const mergedInput = mergeInputs(currentKey.input, input); | ||
const mergedParameter = { | ||
...currentKey.parameter, | ||
...parameter, | ||
}; | ||
const getProcessedResult = processor(mergedParameter, key, doc); | ||
const result = getProcessedResult(mergedInput, mergedParameter); | ||
return result | ||
? call('keyProcessed', result, processorName) | ||
: (call('keyNotProcessed', result, processorName) ?? call('keyNotFound', result, processorName)); | ||
return callHook('keyFound', currentKey); | ||
}; | ||
const callPluginsHook = callPlugins(translate, plugins); | ||
// Initialize plugins | ||
callPluginsHook('initPlugin', processors, undefined, undefined, currentLocaleId, '', undefined, undefined); | ||
return translate; | ||
}; | ||
function mergeInputs(baseInput, input) { | ||
if (typeof input === 'object' && input != null) { | ||
for (const prop in input) | ||
if (input[prop] == null) { | ||
delete input[prop]; | ||
} | ||
} | ||
const mergedInput = typeof baseInput === 'object' && typeof input === 'object' | ||
? { ...baseInput, ...input } | ||
: (input ?? baseInput); | ||
return mergedInput; | ||
} |
@@ -5,4 +5,4 @@ import { writeFile } from 'fs/promises'; | ||
const schema = await compileFromFile('./src/translation.schema.json'); | ||
const schema = await compileFromFile('./translation.schema.json'); | ||
await writeFile('./src/translation.schema.d.ts', schema); |
{ | ||
"name": "intl-schematic", | ||
"version": "0.0.0", | ||
"version": "0.0.1", | ||
"license": "MIT", | ||
@@ -11,4 +11,4 @@ "repository": { | ||
".": "./src/index", | ||
"./processors": "./src/processors", | ||
"./plugins": "./src/plugins", | ||
"./processors": "./src/processors/index", | ||
"./plugins": "./src/plugins/index", | ||
"./translation.schema": "./src/translation.schema", | ||
@@ -23,6 +23,6 @@ "./ts.schema": "./src/ts.schema" | ||
"processors": [ | ||
"./src/processors.ts" | ||
"./src/processors/index.ts" | ||
], | ||
"plugins": [ | ||
"./src/plugins.ts" | ||
"./src/plugins/index.ts" | ||
], | ||
@@ -35,3 +35,3 @@ "translation.schema": [ | ||
"types": "src/index", | ||
"main": "dist/es/index", | ||
"main": "src/index", | ||
"type": "module", | ||
@@ -42,5 +42,6 @@ "scripts": { | ||
"build:next": "tsc --outDir dist/esnext --module esnext", | ||
"build": "npm run build:es && npm run build:next", | ||
"dev": "tsc --noEmit -w", | ||
"commit-build": "(git diff --quiet && git diff --staged --quiet) || (git commit -am \"Update dist\")", | ||
"preversion": "npm t && npm run build && npm run commit-build", | ||
"preversion": "npm run build && npm run commit-build", | ||
"prerelease": "npm version prerelease --preid=rc", | ||
@@ -47,0 +48,0 @@ "pre-patch": "npm version prerelease --preid=rc", |
@@ -36,8 +36,36 @@ # intl-schematic (WIP) | ||
Comprehensive documentation is in progress. | ||
See a simplified example below and don't be afraid to take a look into the sources to find out more. | ||
### Define a translation document | ||
### Define a function that returns a translation document | ||
```js | ||
const en = { | ||
"hello": "Hello, World!", | ||
"hello-name": name => `Hello, ${name}!` | ||
}; | ||
``` | ||
### Define functions that return a translation document and a locale | ||
```js | ||
const getDocument = () => en; | ||
const getLocale = () => new Intl.Locale('en') | ||
``` | ||
### Create a translator function (`t()`) | ||
```js | ||
import { createTranslator } from 'intl-schematic'; | ||
const t = createTranslator(getDocument, getLocale); | ||
``` | ||
### Use a translator function | ||
```js | ||
console.log(t('hello')); // `Hello, World!` | ||
console.log(t('hello-name', ['Bob'])); // `Hello, Bob!` | ||
``` |
188
src/index.ts
import type { LocaleInputParameter, LocaleKey, LocaleOptionsParameter, Translation, TranslationProxy } from './ts.schema'; | ||
import type { InputObject, ParameterObject, ParametrizedTranslationRecord } from './translation.schema'; | ||
import type { Processor, Processors, defaultProcessors } from './processors'; | ||
import { ResolveMissingKeyPlugin, callPlugins } from './plugins'; | ||
import type { Plugin } from './plugins'; | ||
import type { Processors, defaultProcessors } from './processors'; | ||
import { callPlugins } from './plugins/core'; | ||
import type { Plugin } from './plugins/core'; | ||
export * from './ts.schema.d'; | ||
const isProcessedKey = (k: object): k is ParametrizedTranslationRecord => 'processor' in k && typeof k['processor'] === 'object'; | ||
// TODO: decouple processor architecture from plugins | ||
interface Options<P extends Processors, Locale extends Translation> { | ||
processors?: P; | ||
plugins?: Plugin<Locale>[]; | ||
plugins?: Plugin<Locale, P>[]; | ||
} | ||
/** | ||
* 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: { | ||
@@ -28,3 +34,2 @@ <Locale extends Translation>( | ||
): TranslationProxy<Locale, P>; | ||
} = <Locale extends Translation>( | ||
@@ -37,23 +42,6 @@ getLocaleDocument: () => Locale | undefined, | ||
processors = {} as Processors, | ||
plugins = [ResolveMissingKeyPlugin], | ||
plugins = [], | ||
} = options; | ||
const plugin = callPlugins(plugins); | ||
const localizedProcessorsByLocale: Record<string, Record<string, ReturnType<Processor>>> = {}; | ||
function getLocalizedProcessors() { | ||
const localeId = currentLocaleId(); | ||
if (!localeId) { | ||
return {}; | ||
} | ||
return Object.keys(processors).reduce((obj, key) => ({ | ||
...obj, | ||
[key]: processors[key as keyof Processors](localeId), | ||
}), {} as Record<string, ReturnType<Processor>>); | ||
} | ||
return function translate<K extends LocaleKey<Locale>>( | ||
const translate = function <K extends LocaleKey<Locale>>( | ||
key: K, | ||
@@ -65,67 +53,22 @@ input?: LocaleInputParameter<Locale, LocaleKey<Locale>, Processors>, | ||
const call = ( | ||
hook: keyof Omit<Plugin<Locale>, 'name'>, | ||
value?: string, | ||
const callHook = ( | ||
hook: keyof Omit<Plugin<Locale, Processors>, 'name'>, | ||
value?: unknown, | ||
processor?: string, | ||
_input: typeof input = input, | ||
) => plugin(hook, key, _input, value, processor, doc) ?? key; | ||
) => callPluginsHook(hook, value, _input, parameter, currentLocaleId, key, doc, processor) ?? key; | ||
if (!doc) { | ||
return call('docNotFound'); | ||
return callHook('docNotFound'); | ||
} | ||
const localizedProcessors = ( | ||
localizedProcessorsByLocale[String(currentLocaleId()?.baseName)] ??= getLocalizedProcessors() | ||
); | ||
const currentKey = doc[key]; | ||
if (typeof currentKey === 'undefined') { | ||
return call('keyNotFound'); | ||
return callHook('keyNotFound'); | ||
} | ||
// Process an array record (["Some text", "translation-key"]) | ||
// TODO: move into a plugin | ||
if (Array.isArray(currentKey)) { | ||
const result = currentKey.reduce((arr, refK) => { | ||
if (typeof refK !== 'string') { | ||
const refParamK = Object.keys(refK)[0] as K; | ||
if (refParamK.startsWith('input:')) { | ||
const key = refParamK.replace('input:', ''); | ||
const value = (input as Record<string, typeof input>)?.[key]; | ||
return [ | ||
...arr, | ||
// TOOD: add a way to get a stringifier for a processors input | ||
String(value) | ||
]; | ||
} | ||
if (refK.__ignore) { | ||
return arr; | ||
} | ||
return [...arr, translate( | ||
refParamK, | ||
(input as Record<string, typeof input>)?.[refParamK], | ||
(parameter as Record<string, typeof parameter>)?.[refParamK] | ||
)]; | ||
} | ||
if (!refK.startsWith('input:')) { | ||
return [...arr, translate( | ||
refK as K, | ||
(input as Record<string, typeof input>)?.[refK], | ||
(parameter as Record<string, typeof parameter>)?.[refK] | ||
)]; | ||
} | ||
const _input = input as Record<string, typeof input>; | ||
const inputKey = refK.replace('input:', ''); | ||
return [...arr, _input[inputKey] as string]; | ||
}, [] as string[]).join(' '); | ||
return call('keyProcessed', result); | ||
// Process a plain-string | ||
if (typeof currentKey !== 'object' && typeof currentKey !== 'function') { | ||
return currentKey ? callHook('keyProcessed', currentKey) : callHook('keyNotFound'); | ||
} | ||
@@ -136,83 +79,14 @@ | ||
if (typeof currentKey === 'function') { | ||
const inputContainsArgs = typeof input === 'object' && input && 'args' in input && Array.isArray(input.args); | ||
return call('keyProcessed', currentKey(...(inputContainsArgs ? input.args as [] : []))); | ||
return callHook('keyProcessed', currentKey(...(Array.isArray(input) ? input : []))); | ||
} | ||
// Process a plain-string | ||
if (typeof currentKey !== 'object') { | ||
return currentKey ? call('keyFound', currentKey) : call('keyNotFound'); | ||
} | ||
return callHook('keyFound', currentKey); | ||
} as TranslationProxy<Locale, Processors>; | ||
// Process an object record that doesn't specify a processor | ||
// TODO: move into a plugin | ||
if (!isProcessedKey(currentKey)) { | ||
return call( | ||
'keyProcessed', | ||
Object.keys(currentKey).map((refKey) => { | ||
const inputForKey = typeof input === 'object' && input ? input[refKey as keyof typeof input] : {}; | ||
const translated = translate( | ||
refKey as LocaleKey<Locale>, | ||
( | ||
typeof input === 'object' && input | ||
? mergeInputs( | ||
currentKey[refKey], | ||
inputForKey ?? null | ||
) | ||
: currentKey[refKey] | ||
) as LocaleInputParameter<Locale, LocaleKey<Locale>, Processors> | ||
); | ||
const callPluginsHook = callPlugins(translate, plugins); | ||
return translated; | ||
}).join(' ').replace(/\s+/, ' ') | ||
); | ||
} | ||
// Initialize plugins | ||
callPluginsHook('initPlugin', processors, undefined, undefined, currentLocaleId, '', undefined, undefined); | ||
// Process a parametrized record using a processor: | ||
// TODO: move into a plugin | ||
const processorName = Object.keys(currentKey.processor)[0]; | ||
const processor = localizedProcessors[processorName]; | ||
if (!processor) { | ||
return call('processorNotFound', key, processorName); | ||
} | ||
// Delete undefined keys to make defaults bypass them in the spread later | ||
const mergedInput = mergeInputs( | ||
currentKey.input, | ||
input as InputObject | ||
); | ||
const mergedParameter = { | ||
...currentKey.parameter, | ||
...parameter as ParameterObject, | ||
}; | ||
const getProcessedResult = processor(mergedParameter, key, doc); | ||
const result = getProcessedResult(mergedInput, mergedParameter); | ||
return result | ||
? call('keyProcessed', result, processorName) | ||
: (call('keyNotProcessed', result, processorName) ?? call('keyNotFound', result, processorName)); | ||
} as TranslationProxy<Locale, Processors>; | ||
return translate; | ||
}; | ||
function mergeInputs( | ||
baseInput: InputObject, | ||
input: InputObject, | ||
) { | ||
if (typeof input === 'object' && input != null) { | ||
for (const prop in input) | ||
if (input[prop] == null) { | ||
delete input[prop]; | ||
} | ||
} | ||
const mergedInput = typeof baseInput === 'object' && typeof input === 'object' | ||
? { ...baseInput, ...input } | ||
: (input ?? baseInput); | ||
return mergedInput; | ||
} |
{ | ||
"extends": "tsconfig/base" | ||
"extends": "tsconfig/base", | ||
"include": ["./src"] | ||
} |
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
99685
71
1936
71
1