@vocab/core
Advanced tools
Comparing version 1.0.4 to 1.1.0
# @vocab/core | ||
## 1.1.0 | ||
### Minor Changes | ||
- [`87333d7`](https://github.com/seek-oss/vocab/commit/87333d79c4a883b07d7d8f2c272b16e2243c49bd) [#80](https://github.com/seek-oss/vocab/pull/80) Thanks [@askoufis](https://github.com/askoufis)! - Enable the creation of generated languages via the `generatedLanguages` config. | ||
See [the docs] for more information and examples. | ||
[the docs]: https://github.com/seek-oss/vocab#generated-languages | ||
### Patch Changes | ||
- Updated dependencies [[`87333d7`](https://github.com/seek-oss/vocab/commit/87333d79c4a883b07d7d8f2c272b16e2243c49bd)]: | ||
- @vocab/types@1.1.0 | ||
## 1.0.4 | ||
@@ -4,0 +18,0 @@ |
@@ -13,2 +13,4 @@ 'use strict'; | ||
var glob = require('fast-glob'); | ||
var IntlMessageFormat = require('intl-messageformat'); | ||
var printer = require('@formatjs/icu-messageformat-parser/printer'); | ||
var findUp = require('find-up'); | ||
@@ -25,2 +27,3 @@ var Validator = require('fastest-validator'); | ||
var glob__default = /*#__PURE__*/_interopDefault(glob); | ||
var IntlMessageFormat__default = /*#__PURE__*/_interopDefault(IntlMessageFormat); | ||
var findUp__default = /*#__PURE__*/_interopDefault(findUp); | ||
@@ -113,2 +116,67 @@ var Validator__default = /*#__PURE__*/_interopDefault(Validator); | ||
function generateLanguageFromTranslations({ | ||
baseTranslations, | ||
generator | ||
}) { | ||
if (!generator.transformElement && !generator.transformMessage) { | ||
return baseTranslations; | ||
} | ||
const translationKeys = Object.keys(baseTranslations); | ||
const generatedTranslations = {}; | ||
for (const translationKey of translationKeys) { | ||
const translation = baseTranslations[translationKey]; | ||
let transformedMessage = translation.message; | ||
if (generator.transformElement) { | ||
const messageAst = new IntlMessageFormat__default['default'](translation.message).getAst(); | ||
const transformedAst = messageAst.map(transformMessageFormatElement(generator.transformElement)); | ||
transformedMessage = printer.printAST(transformedAst); | ||
} | ||
if (generator.transformMessage) { | ||
transformedMessage = generator.transformMessage(transformedMessage); | ||
} | ||
generatedTranslations[translationKey] = { | ||
message: transformedMessage | ||
}; | ||
} | ||
return generatedTranslations; | ||
} | ||
function transformMessageFormatElement(transformElement) { | ||
return messageFormatElement => { | ||
const transformedMessageFormatElement = { ...messageFormatElement | ||
}; | ||
switch (transformedMessageFormatElement.type) { | ||
case icuMessageformatParser.TYPE.literal: | ||
const transformedValue = transformElement(transformedMessageFormatElement.value); | ||
transformedMessageFormatElement.value = transformedValue; | ||
break; | ||
case icuMessageformatParser.TYPE.select: | ||
case icuMessageformatParser.TYPE.plural: | ||
const transformedOptions = { ...transformedMessageFormatElement.options | ||
}; | ||
for (const key of Object.keys(transformedOptions)) { | ||
transformedOptions[key].value = transformedOptions[key].value.map(transformMessageFormatElement(transformElement)); | ||
} | ||
break; | ||
case icuMessageformatParser.TYPE.tag: | ||
const transformedChildren = transformedMessageFormatElement.children.map(transformMessageFormatElement(transformElement)); | ||
transformedMessageFormatElement.children = transformedChildren; | ||
break; | ||
} | ||
return transformedMessageFormatElement; | ||
}; | ||
} | ||
function getUniqueKey(key, namespace) { | ||
@@ -242,3 +310,3 @@ return `${key}.${namespace}`; | ||
if (typeof translation === 'string') { | ||
printValidationError(`Found string for a translation "${translationKey}" in ${filePath}. Translation must be an object of the format {mesage: string}.`); | ||
printValidationError(`Found string for a translation "${translationKey}" in ${filePath}. Translation must be an object of the format {message: string}.`); | ||
continue; | ||
@@ -248,3 +316,3 @@ } | ||
if (!translation) { | ||
printValidationError(`Found empty translation "${translationKey}" in ${filePath}. Translation must be an object of the format {mesage: string}.`); | ||
printValidationError(`Found empty translation "${translationKey}" in ${filePath}. Translation must be an object of the format {message: string}.`); | ||
continue; | ||
@@ -254,3 +322,3 @@ } | ||
if (!translation.message || typeof translation.message !== 'string') { | ||
printValidationError(`No message found for translation "${translationKey}" in ${filePath}. Translation must be an object of the format {mesage: string}.`); | ||
printValidationError(`No message found for translation "${translationKey}" in ${filePath}. Translation must be an object of the format {message: string}.`); | ||
continue; | ||
@@ -347,2 +415,15 @@ } | ||
for (const generatedLanguage of userConfig.generatedLanguages || []) { | ||
const { | ||
name: generatedLanguageName, | ||
generator | ||
} = generatedLanguage; | ||
const baseLanguage = generatedLanguage.extends || userConfig.devLanguage; | ||
const baseTranslations = languageSet[baseLanguage]; | ||
languageSet[generatedLanguageName] = generateLanguageFromTranslations({ | ||
baseTranslations, | ||
generator | ||
}); | ||
} | ||
return { | ||
@@ -719,2 +800,31 @@ filePath, | ||
}, | ||
generatedLanguages: { | ||
type: 'array', | ||
items: { | ||
type: 'object', | ||
props: { | ||
name: { | ||
type: 'string' | ||
}, | ||
extends: { | ||
type: 'string', | ||
optional: true | ||
}, | ||
generator: { | ||
type: 'object', | ||
props: { | ||
transformElement: { | ||
type: 'function', | ||
optional: true | ||
}, | ||
transformMessage: { | ||
type: 'function', | ||
optional: true | ||
} | ||
} | ||
} | ||
} | ||
}, | ||
optional: true | ||
}, | ||
translationsDirectorySuffix: { | ||
@@ -757,9 +867,8 @@ type: 'string', | ||
}).join(' \n')); | ||
} // Dev Language should exist in languages | ||
} | ||
const languageStrings = c.languages.map(v => v.name); // Dev Language should exist in languages | ||
const languageStrings = c.languages.map(v => v.name); | ||
if (!languageStrings.includes(c.devLanguage)) { | ||
throw new ValidationError('InvalidDevLanguage', `InvalidDevLanguage: The dev language "${chalk__default['default'].bold.cyan(c.devLanguage)}" was not found in languages ${languageStrings.join(', ')}.`); | ||
throw new ValidationError('InvalidDevLanguage', `The dev language "${chalk__default['default'].bold.cyan(c.devLanguage)}" was not found in languages ${languageStrings.join(', ')}.`); | ||
} | ||
@@ -782,2 +891,22 @@ | ||
const foundGeneratedLanguages = []; | ||
for (const generatedLang of c.generatedLanguages || []) { | ||
// Generated languages must only exist once | ||
if (foundGeneratedLanguages.includes(generatedLang.name)) { | ||
throw new ValidationError('DuplicateGeneratedLanguage', `The generated language "${chalk__default['default'].bold.cyan(generatedLang.name)}" was defined multiple times.`); | ||
} | ||
foundGeneratedLanguages.push(generatedLang.name); // Generated language names must not conflict with language names | ||
if (languageStrings.includes(generatedLang.name)) { | ||
throw new ValidationError('InvalidGeneratedLanguage', `The generated language "${chalk__default['default'].bold.cyan(generatedLang.name)}" is already defined as a language.`); | ||
} // Any extends must be in languages | ||
if (generatedLang.extends && !languageStrings.includes(generatedLang.extends)) { | ||
throw new ValidationError('InvalidExtends', `The generated language "${chalk__default['default'].bold.cyan(generatedLang.name)}"'s extends of ${chalk__default['default'].bold.cyan(generatedLang.extends)} was not found in languages ${languageStrings.join(', ')}.`); | ||
} | ||
} | ||
trace('Configuration file is valid'); | ||
@@ -784,0 +913,0 @@ return true; |
@@ -13,2 +13,4 @@ 'use strict'; | ||
var glob = require('fast-glob'); | ||
var IntlMessageFormat = require('intl-messageformat'); | ||
var printer = require('@formatjs/icu-messageformat-parser/printer'); | ||
var findUp = require('find-up'); | ||
@@ -25,2 +27,3 @@ var Validator = require('fastest-validator'); | ||
var glob__default = /*#__PURE__*/_interopDefault(glob); | ||
var IntlMessageFormat__default = /*#__PURE__*/_interopDefault(IntlMessageFormat); | ||
var findUp__default = /*#__PURE__*/_interopDefault(findUp); | ||
@@ -113,2 +116,67 @@ var Validator__default = /*#__PURE__*/_interopDefault(Validator); | ||
function generateLanguageFromTranslations({ | ||
baseTranslations, | ||
generator | ||
}) { | ||
if (!generator.transformElement && !generator.transformMessage) { | ||
return baseTranslations; | ||
} | ||
const translationKeys = Object.keys(baseTranslations); | ||
const generatedTranslations = {}; | ||
for (const translationKey of translationKeys) { | ||
const translation = baseTranslations[translationKey]; | ||
let transformedMessage = translation.message; | ||
if (generator.transformElement) { | ||
const messageAst = new IntlMessageFormat__default['default'](translation.message).getAst(); | ||
const transformedAst = messageAst.map(transformMessageFormatElement(generator.transformElement)); | ||
transformedMessage = printer.printAST(transformedAst); | ||
} | ||
if (generator.transformMessage) { | ||
transformedMessage = generator.transformMessage(transformedMessage); | ||
} | ||
generatedTranslations[translationKey] = { | ||
message: transformedMessage | ||
}; | ||
} | ||
return generatedTranslations; | ||
} | ||
function transformMessageFormatElement(transformElement) { | ||
return messageFormatElement => { | ||
const transformedMessageFormatElement = { ...messageFormatElement | ||
}; | ||
switch (transformedMessageFormatElement.type) { | ||
case icuMessageformatParser.TYPE.literal: | ||
const transformedValue = transformElement(transformedMessageFormatElement.value); | ||
transformedMessageFormatElement.value = transformedValue; | ||
break; | ||
case icuMessageformatParser.TYPE.select: | ||
case icuMessageformatParser.TYPE.plural: | ||
const transformedOptions = { ...transformedMessageFormatElement.options | ||
}; | ||
for (const key of Object.keys(transformedOptions)) { | ||
transformedOptions[key].value = transformedOptions[key].value.map(transformMessageFormatElement(transformElement)); | ||
} | ||
break; | ||
case icuMessageformatParser.TYPE.tag: | ||
const transformedChildren = transformedMessageFormatElement.children.map(transformMessageFormatElement(transformElement)); | ||
transformedMessageFormatElement.children = transformedChildren; | ||
break; | ||
} | ||
return transformedMessageFormatElement; | ||
}; | ||
} | ||
function getUniqueKey(key, namespace) { | ||
@@ -242,3 +310,3 @@ return `${key}.${namespace}`; | ||
if (typeof translation === 'string') { | ||
printValidationError(`Found string for a translation "${translationKey}" in ${filePath}. Translation must be an object of the format {mesage: string}.`); | ||
printValidationError(`Found string for a translation "${translationKey}" in ${filePath}. Translation must be an object of the format {message: string}.`); | ||
continue; | ||
@@ -248,3 +316,3 @@ } | ||
if (!translation) { | ||
printValidationError(`Found empty translation "${translationKey}" in ${filePath}. Translation must be an object of the format {mesage: string}.`); | ||
printValidationError(`Found empty translation "${translationKey}" in ${filePath}. Translation must be an object of the format {message: string}.`); | ||
continue; | ||
@@ -254,3 +322,3 @@ } | ||
if (!translation.message || typeof translation.message !== 'string') { | ||
printValidationError(`No message found for translation "${translationKey}" in ${filePath}. Translation must be an object of the format {mesage: string}.`); | ||
printValidationError(`No message found for translation "${translationKey}" in ${filePath}. Translation must be an object of the format {message: string}.`); | ||
continue; | ||
@@ -347,2 +415,15 @@ } | ||
for (const generatedLanguage of userConfig.generatedLanguages || []) { | ||
const { | ||
name: generatedLanguageName, | ||
generator | ||
} = generatedLanguage; | ||
const baseLanguage = generatedLanguage.extends || userConfig.devLanguage; | ||
const baseTranslations = languageSet[baseLanguage]; | ||
languageSet[generatedLanguageName] = generateLanguageFromTranslations({ | ||
baseTranslations, | ||
generator | ||
}); | ||
} | ||
return { | ||
@@ -719,2 +800,31 @@ filePath, | ||
}, | ||
generatedLanguages: { | ||
type: 'array', | ||
items: { | ||
type: 'object', | ||
props: { | ||
name: { | ||
type: 'string' | ||
}, | ||
extends: { | ||
type: 'string', | ||
optional: true | ||
}, | ||
generator: { | ||
type: 'object', | ||
props: { | ||
transformElement: { | ||
type: 'function', | ||
optional: true | ||
}, | ||
transformMessage: { | ||
type: 'function', | ||
optional: true | ||
} | ||
} | ||
} | ||
} | ||
}, | ||
optional: true | ||
}, | ||
translationsDirectorySuffix: { | ||
@@ -757,9 +867,8 @@ type: 'string', | ||
}).join(' \n')); | ||
} // Dev Language should exist in languages | ||
} | ||
const languageStrings = c.languages.map(v => v.name); // Dev Language should exist in languages | ||
const languageStrings = c.languages.map(v => v.name); | ||
if (!languageStrings.includes(c.devLanguage)) { | ||
throw new ValidationError('InvalidDevLanguage', `InvalidDevLanguage: The dev language "${chalk__default['default'].bold.cyan(c.devLanguage)}" was not found in languages ${languageStrings.join(', ')}.`); | ||
throw new ValidationError('InvalidDevLanguage', `The dev language "${chalk__default['default'].bold.cyan(c.devLanguage)}" was not found in languages ${languageStrings.join(', ')}.`); | ||
} | ||
@@ -782,2 +891,22 @@ | ||
const foundGeneratedLanguages = []; | ||
for (const generatedLang of c.generatedLanguages || []) { | ||
// Generated languages must only exist once | ||
if (foundGeneratedLanguages.includes(generatedLang.name)) { | ||
throw new ValidationError('DuplicateGeneratedLanguage', `The generated language "${chalk__default['default'].bold.cyan(generatedLang.name)}" was defined multiple times.`); | ||
} | ||
foundGeneratedLanguages.push(generatedLang.name); // Generated language names must not conflict with language names | ||
if (languageStrings.includes(generatedLang.name)) { | ||
throw new ValidationError('InvalidGeneratedLanguage', `The generated language "${chalk__default['default'].bold.cyan(generatedLang.name)}" is already defined as a language.`); | ||
} // Any extends must be in languages | ||
if (generatedLang.extends && !languageStrings.includes(generatedLang.extends)) { | ||
throw new ValidationError('InvalidExtends', `The generated language "${chalk__default['default'].bold.cyan(generatedLang.name)}"'s extends of ${chalk__default['default'].bold.cyan(generatedLang.extends)} was not found in languages ${languageStrings.join(', ')}.`); | ||
} | ||
} | ||
trace('Configuration file is valid'); | ||
@@ -784,0 +913,0 @@ return true; |
import { existsSync, promises } from 'fs'; | ||
import path from 'path'; | ||
import { parse, isSelectElement, isTagElement, isArgumentElement, isNumberElement, isPluralElement, isDateElement, isTimeElement } from '@formatjs/icu-messageformat-parser'; | ||
import { TYPE, parse, isSelectElement, isTagElement, isArgumentElement, isNumberElement, isPluralElement, isDateElement, isTimeElement } from '@formatjs/icu-messageformat-parser'; | ||
import prettier from 'prettier'; | ||
@@ -9,2 +9,4 @@ import chokidar from 'chokidar'; | ||
import glob from 'fast-glob'; | ||
import IntlMessageFormat from 'intl-messageformat'; | ||
import { printAST } from '@formatjs/icu-messageformat-parser/printer'; | ||
import findUp from 'find-up'; | ||
@@ -97,2 +99,67 @@ import Validator from 'fastest-validator'; | ||
function generateLanguageFromTranslations({ | ||
baseTranslations, | ||
generator | ||
}) { | ||
if (!generator.transformElement && !generator.transformMessage) { | ||
return baseTranslations; | ||
} | ||
const translationKeys = Object.keys(baseTranslations); | ||
const generatedTranslations = {}; | ||
for (const translationKey of translationKeys) { | ||
const translation = baseTranslations[translationKey]; | ||
let transformedMessage = translation.message; | ||
if (generator.transformElement) { | ||
const messageAst = new IntlMessageFormat(translation.message).getAst(); | ||
const transformedAst = messageAst.map(transformMessageFormatElement(generator.transformElement)); | ||
transformedMessage = printAST(transformedAst); | ||
} | ||
if (generator.transformMessage) { | ||
transformedMessage = generator.transformMessage(transformedMessage); | ||
} | ||
generatedTranslations[translationKey] = { | ||
message: transformedMessage | ||
}; | ||
} | ||
return generatedTranslations; | ||
} | ||
function transformMessageFormatElement(transformElement) { | ||
return messageFormatElement => { | ||
const transformedMessageFormatElement = { ...messageFormatElement | ||
}; | ||
switch (transformedMessageFormatElement.type) { | ||
case TYPE.literal: | ||
const transformedValue = transformElement(transformedMessageFormatElement.value); | ||
transformedMessageFormatElement.value = transformedValue; | ||
break; | ||
case TYPE.select: | ||
case TYPE.plural: | ||
const transformedOptions = { ...transformedMessageFormatElement.options | ||
}; | ||
for (const key of Object.keys(transformedOptions)) { | ||
transformedOptions[key].value = transformedOptions[key].value.map(transformMessageFormatElement(transformElement)); | ||
} | ||
break; | ||
case TYPE.tag: | ||
const transformedChildren = transformedMessageFormatElement.children.map(transformMessageFormatElement(transformElement)); | ||
transformedMessageFormatElement.children = transformedChildren; | ||
break; | ||
} | ||
return transformedMessageFormatElement; | ||
}; | ||
} | ||
function getUniqueKey(key, namespace) { | ||
@@ -226,3 +293,3 @@ return `${key}.${namespace}`; | ||
if (typeof translation === 'string') { | ||
printValidationError(`Found string for a translation "${translationKey}" in ${filePath}. Translation must be an object of the format {mesage: string}.`); | ||
printValidationError(`Found string for a translation "${translationKey}" in ${filePath}. Translation must be an object of the format {message: string}.`); | ||
continue; | ||
@@ -232,3 +299,3 @@ } | ||
if (!translation) { | ||
printValidationError(`Found empty translation "${translationKey}" in ${filePath}. Translation must be an object of the format {mesage: string}.`); | ||
printValidationError(`Found empty translation "${translationKey}" in ${filePath}. Translation must be an object of the format {message: string}.`); | ||
continue; | ||
@@ -238,3 +305,3 @@ } | ||
if (!translation.message || typeof translation.message !== 'string') { | ||
printValidationError(`No message found for translation "${translationKey}" in ${filePath}. Translation must be an object of the format {mesage: string}.`); | ||
printValidationError(`No message found for translation "${translationKey}" in ${filePath}. Translation must be an object of the format {message: string}.`); | ||
continue; | ||
@@ -331,2 +398,15 @@ } | ||
for (const generatedLanguage of userConfig.generatedLanguages || []) { | ||
const { | ||
name: generatedLanguageName, | ||
generator | ||
} = generatedLanguage; | ||
const baseLanguage = generatedLanguage.extends || userConfig.devLanguage; | ||
const baseTranslations = languageSet[baseLanguage]; | ||
languageSet[generatedLanguageName] = generateLanguageFromTranslations({ | ||
baseTranslations, | ||
generator | ||
}); | ||
} | ||
return { | ||
@@ -703,2 +783,31 @@ filePath, | ||
}, | ||
generatedLanguages: { | ||
type: 'array', | ||
items: { | ||
type: 'object', | ||
props: { | ||
name: { | ||
type: 'string' | ||
}, | ||
extends: { | ||
type: 'string', | ||
optional: true | ||
}, | ||
generator: { | ||
type: 'object', | ||
props: { | ||
transformElement: { | ||
type: 'function', | ||
optional: true | ||
}, | ||
transformMessage: { | ||
type: 'function', | ||
optional: true | ||
} | ||
} | ||
} | ||
} | ||
}, | ||
optional: true | ||
}, | ||
translationsDirectorySuffix: { | ||
@@ -741,9 +850,8 @@ type: 'string', | ||
}).join(' \n')); | ||
} // Dev Language should exist in languages | ||
} | ||
const languageStrings = c.languages.map(v => v.name); // Dev Language should exist in languages | ||
const languageStrings = c.languages.map(v => v.name); | ||
if (!languageStrings.includes(c.devLanguage)) { | ||
throw new ValidationError('InvalidDevLanguage', `InvalidDevLanguage: The dev language "${chalk.bold.cyan(c.devLanguage)}" was not found in languages ${languageStrings.join(', ')}.`); | ||
throw new ValidationError('InvalidDevLanguage', `The dev language "${chalk.bold.cyan(c.devLanguage)}" was not found in languages ${languageStrings.join(', ')}.`); | ||
} | ||
@@ -766,2 +874,22 @@ | ||
const foundGeneratedLanguages = []; | ||
for (const generatedLang of c.generatedLanguages || []) { | ||
// Generated languages must only exist once | ||
if (foundGeneratedLanguages.includes(generatedLang.name)) { | ||
throw new ValidationError('DuplicateGeneratedLanguage', `The generated language "${chalk.bold.cyan(generatedLang.name)}" was defined multiple times.`); | ||
} | ||
foundGeneratedLanguages.push(generatedLang.name); // Generated language names must not conflict with language names | ||
if (languageStrings.includes(generatedLang.name)) { | ||
throw new ValidationError('InvalidGeneratedLanguage', `The generated language "${chalk.bold.cyan(generatedLang.name)}" is already defined as a language.`); | ||
} // Any extends must be in languages | ||
if (generatedLang.extends && !languageStrings.includes(generatedLang.extends)) { | ||
throw new ValidationError('InvalidExtends', `The generated language "${chalk.bold.cyan(generatedLang.name)}"'s extends of ${chalk.bold.cyan(generatedLang.extends)} was not found in languages ${languageStrings.join(', ')}.`); | ||
} | ||
} | ||
trace('Configuration file is valid'); | ||
@@ -768,0 +896,0 @@ return true; |
{ | ||
"name": "@vocab/core", | ||
"version": "1.0.4", | ||
"version": "1.1.0", | ||
"main": "dist/vocab-core.cjs.js", | ||
@@ -16,5 +16,11 @@ "module": "dist/vocab-core.esm.js", | ||
}, | ||
"files": [ | ||
"dist", | ||
"runtime", | ||
"icu-handler", | ||
"translation-file" | ||
], | ||
"dependencies": { | ||
"@formatjs/icu-messageformat-parser": "^2.0.10", | ||
"@vocab/types": "^1.0.1", | ||
"@vocab/types": "^1.1.0", | ||
"chalk": "^4.1.0", | ||
@@ -21,0 +27,0 @@ "chokidar": "^3.4.3", |
149
README.md
@@ -187,2 +187,10 @@ # Vocab | ||
```js | ||
function capitalize(element) { | ||
return element.toUpperCase(); | ||
} | ||
function pad(message) { | ||
return '[' + message + ']'; | ||
} | ||
module.exports = { | ||
@@ -197,6 +205,20 @@ devLanguage: 'en', | ||
/** | ||
* An array of languages to generate based off translations for existing languages | ||
* Default: [] | ||
*/ | ||
generatedLanguages: [ | ||
{ | ||
name: 'generatedLangauge', | ||
extends: 'en', | ||
generator: { | ||
transformElement: capitalize, | ||
transformMessage: pad | ||
} | ||
} | ||
], | ||
/** | ||
* The root directory to compile and validate translations | ||
* Default: Current working directory | ||
*/ | ||
projectRoot: './example/'; | ||
projectRoot: './example/', | ||
/** | ||
@@ -214,2 +236,127 @@ * A custom suffix to name vocab translation directories | ||
## Generated languages | ||
Vocab supports the creation of generated languages via the `generatedLanguages` config. | ||
Generated languages are created by running a message `generator` over every translation message in an existing translation. | ||
A `generator` may contain a `transformElement` function, a `transformMessage` function, or both. | ||
Both of these functions accept a single string parameter and return a string. | ||
`transformElement` is applied to string literal values contained within `MessageFormatElement`s. | ||
A `MessageFormatElement` is an object representing a node in the AST of a compiled translation message. | ||
Simply put, any text that would end up being translated by a translator, i.e. anything that is not part of the [ICU Message syntax], will be passed to `transformElement`. | ||
An example of a use case for this function would be adding [diacritics] to every letter in order to stress your UI from a vertical line-height perspective. | ||
`transformMessage` receives the entire translation message _after_ `transformElement` has been applied to its individual elements. | ||
An example of a use case for this function would be adding padding text to the start/end of your messages in order to easily identify which text in your app has not been extracted into a `translations.json` file. | ||
By default, a generated language's messages will be based off the `devLanguage`'s messages, but this can be overridden by providing an `extends` value that references another language. | ||
**vocab.config.js** | ||
```js | ||
function capitalize(message) { | ||
return message.toUpperCase(); | ||
} | ||
function pad(message) { | ||
return '[' + message + ']'; | ||
} | ||
module.exports = { | ||
devLanguage: 'en', | ||
languages: [{ name: 'en' }, { name: 'fr' }], | ||
generatedLanguages: [ | ||
{ | ||
name: 'generatedLanguage', | ||
extends: 'en', | ||
generator: { | ||
transformElement: capitalize, | ||
transformMessage: pad | ||
} | ||
} | ||
] | ||
}; | ||
``` | ||
Generated languages are consumed the same way as regular languages. | ||
Any Vocab API that accepts a `language` parameter will work with a generated language as well as a regular language. | ||
**App.tsx** | ||
```tsx | ||
const App = () => ( | ||
<VocabProvider language="generatedLanguage"> | ||
... | ||
</VocabProvider> | ||
); | ||
``` | ||
[icu message syntax]: https://formatjs.io/docs/intl-messageformat/#message-syntax | ||
[diacritics]: https://en.wikipedia.org/wiki/Diacritic | ||
## Pseudo-localization | ||
The `@vocab/pseudo-localize` package exports low-level functions that can be used for pseudo-localization of translation messages. | ||
```ts | ||
import { | ||
extendVowels, | ||
padString, | ||
pseudoLocalize, | ||
substituteCharacters | ||
} from '@vocab/pseudo-localize'; | ||
const message = 'Hello'; | ||
// [Hello] | ||
const paddedMessage = padString(message); | ||
// Ḩẽƚƚö | ||
const substitutedMessage = substituteCharacters(message); | ||
// Heelloo | ||
const extendedMessage = extendVowels(message); | ||
// Extend the message and then substitute characters | ||
// Ḩẽẽƚƚöö | ||
const pseudoLocalizedMessage = pseudoLocalize(message); | ||
``` | ||
Pseudo-localization is a transformation that can be applied to a translation message. | ||
Vocab's implementation of this transformation contains the following elements: | ||
- _Start and end markers (`padString`):_ All strings are encapsulated in `[` and `]`. If a developer doesn’t see these characters they know the string has been clipped by an inflexible UI element. | ||
- _Transformation of ASCII characters to extended character equivalents (`substituteCharacters`):_ Stresses the UI from a vertical line-height perspective, tests font and encoding support, and weeds out strings that haven’t been externalized correctly (they will not have the pseudo-localization applied to them). | ||
- _Padding text (`extendVowels`):_ Simulates translation-induced expansion. Vocab's implementation of this involves repeating vowels (and `y`) to simulate a 40% expansion in the message's length. | ||
This Netflix technology [blog post][blog post] inspired Vocab's implementation of this | ||
functionality. | ||
### Generating a pseudo-localized language using Vocab | ||
Vocab can generate a pseudo-localized language via the [`generatedLanguages` config][generated languages config], either via the webpack plugin or your `vocab.config.js` file. | ||
`@vocab/pseudo-localize` exports a `generator` that can be used directly in your config. | ||
**vocab.config.js** | ||
```js | ||
const { generator } = require('@vocab/pseudo-localize'); | ||
module.exports = { | ||
devLanguage: 'en', | ||
languages: [{ name: 'en' }, { name: 'fr' }], | ||
generatedLanguages: [ | ||
{ | ||
name: 'pseudo', | ||
extends: 'en', | ||
generator | ||
} | ||
] | ||
}; | ||
``` | ||
[blog post]: https://netflixtechblog.com/pseudo-localization-netflix-12fff76fbcbe | ||
[generated languages config]: #generated-languages | ||
## Use without React | ||
@@ -216,0 +363,0 @@ |
448
132262
38
2692
Updated@vocab/types@^1.1.0