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

@vocab/core

Package Overview
Dependencies
Maintainers
5
Versions
40
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@vocab/core - npm Package Compare versions

Comparing version 1.2.0 to 1.2.1

LICENSE

0

dist/declarations/src/compile.d.ts

@@ -0,0 +0,0 @@ import { LoadedTranslation, UserConfig } from '@vocab/types';

@@ -0,0 +0,0 @@ import { UserConfig } from '@vocab/types';

@@ -0,0 +0,0 @@ import { MessageGenerator, TranslationsByKey } from '@vocab/types';

import { ParsedICUMessages, TranslationMessagesByKey } from '@vocab/types';
export declare const getParsedICUMessages: (m: TranslationMessagesByKey, locale: string) => ParsedICUMessages<any>;

@@ -0,0 +0,0 @@ export { compile, watch } from './compile';

@@ -0,0 +0,0 @@ import type { TranslationsByKey, UserConfig, LoadedTranslation, LanguageTarget } from '@vocab/types';

import debug from 'debug';
export declare const trace: debug.Debugger;
export declare const log: (...params: unknown[]) => void;
import { TranslationModule, TranslationMessagesByKey } from '@vocab/types';
export { createTranslationFile } from './translation-file';
export declare const createLanguage: (module: TranslationMessagesByKey) => TranslationModule<any>;
import { TranslationModuleByLanguage, LanguageName, ParsedFormatFnByKey, TranslationFile } from '@vocab/types';
export declare function createTranslationFile<Language extends LanguageName, FormatFnByKey extends ParsedFormatFnByKey>(translationsByLanguage: TranslationModuleByLanguage<Language, FormatFnByKey>): TranslationFile<Language, FormatFnByKey>;

2

dist/declarations/src/utils.d.ts
import type { LanguageName, LanguageTarget, TranslationsByKey, TranslationMessagesByKey, UserConfig } from '@vocab/types';
export declare const defaultTranslationDirSuffix = ".vocab";
export declare const devTranslationFileName = "translations.json";
export declare type Fallback = 'none' | 'valid' | 'all';
export type Fallback = 'none' | 'valid' | 'all';
export declare function isDevLanguageFile(filePath: string): boolean;

@@ -6,0 +6,0 @@ export declare function isAltLanguageFile(filePath: string): boolean;

import { UserConfig, LoadedTranslation, LanguageName } from '@vocab/types';
export declare function findMissingKeys(loadedTranslation: LoadedTranslation, devLanguageName: LanguageName, altLanguages: Array<LanguageName>): readonly [boolean, Record<string, string[]>];
export declare function validate(config: UserConfig): Promise<boolean>;

@@ -0,0 +0,0 @@ export declare class ValidationError extends Error {

@@ -30,3 +30,3 @@ 'use strict';

const trace = debug__default['default'](`vocab:core`);
const trace = debug__default["default"](`vocab:core`);

@@ -78,4 +78,4 @@ const defaultTranslationDirSuffix = '.vocab';

function getDevLanguageFileFromTsFile(tsFilePath) {
const directory = path__default['default'].dirname(tsFilePath);
const result = path__default['default'].normalize(path__default['default'].join(directory, devTranslationFileName));
const directory = path__default["default"].dirname(tsFilePath);
const result = path__default["default"].normalize(path__default["default"].join(directory, devTranslationFileName));
trace(`Returning dev language path ${result} for path ${tsFilePath}`);

@@ -85,4 +85,4 @@ return result;

function getDevLanguageFileFromAltLanguageFile(altLanguageFilePath) {
const directory = path__default['default'].dirname(altLanguageFilePath);
const result = path__default['default'].normalize(path__default['default'].join(directory, devTranslationFileName));
const directory = path__default["default"].dirname(altLanguageFilePath);
const result = path__default["default"].normalize(path__default["default"].join(directory, devTranslationFileName));
trace(`Returning dev language path ${result} for path ${altLanguageFilePath}`);

@@ -92,4 +92,4 @@ return result;

function getTSFileFromDevLanguageFile(devLanguageFilePath) {
const directory = path__default['default'].dirname(devLanguageFilePath);
const result = path__default['default'].normalize(path__default['default'].join(directory, 'index.ts'));
const directory = path__default["default"].dirname(devLanguageFilePath);
const result = path__default["default"].normalize(path__default["default"].join(directory, 'index.ts'));
trace(`Returning TS path ${result} for path ${devLanguageFilePath}`);

@@ -99,6 +99,6 @@ return result;

function getAltLanguageFilePath(devLanguageFilePath, language) {
const directory = path__default['default'].dirname(devLanguageFilePath);
const result = path__default['default'].normalize(path__default['default'].join(directory, `${language}.translations.json`));
const directory = path__default["default"].dirname(devLanguageFilePath);
const result = path__default["default"].normalize(path__default["default"].join(directory, `${language}.translations.json`));
trace(`Returning alt language path ${result} for path ${devLanguageFilePath}`);
return path__default['default'].normalize(result);
return path__default["default"].normalize(result);
}

@@ -108,7 +108,5 @@ function mapValues(obj, func) {

const keys = Object.keys(obj);
for (const key of keys) {
newObj[key] = func(obj[key]);
}
return newObj;

@@ -127,20 +125,15 @@ }

}
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 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] = {

@@ -150,11 +143,9 @@ message: transformedMessage

}
return generatedTranslations;
}
function transformMessageFormatElement(transformElement) {
return messageFormatElement => {
const transformedMessageFormatElement = { ...messageFormatElement
const transformedMessageFormatElement = {
...messageFormatElement
};
switch (transformedMessageFormatElement.type) {

@@ -165,14 +156,11 @@ case icuMessageformatParser.TYPE.literal:

break;
case icuMessageformatParser.TYPE.select:
case icuMessageformatParser.TYPE.plural:
const transformedOptions = { ...transformedMessageFormatElement.options
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:

@@ -183,3 +171,2 @@ const transformedChildren = transformedMessageFormatElement.children.map(transformMessageFormatElement(transformElement));

}
return transformedMessageFormatElement;

@@ -199,3 +186,2 @@ };

const newLanguage = {};
for (const key of keys) {

@@ -209,6 +195,4 @@ if (translation[key]) {

}
return newLanguage;
}
function getLanguageFallbacks({

@@ -218,3 +202,2 @@ languages

const languageFallbackMap = new Map();
for (const lang of languages) {

@@ -225,6 +208,4 @@ if (lang.extends) {

}
return languageFallbackMap;
}
function getLanguageHierarchy({

@@ -237,7 +218,5 @@ languages

});
for (const lang of languages) {
const langHierarchy = [];
let currLang = lang.extends;
while (currLang) {

@@ -247,6 +226,4 @@ langHierarchy.push(currLang);

}
hierarchyMap.set(lang.name, langHierarchy);
}
return hierarchyMap;

@@ -263,12 +240,8 @@ }

}).get(languageName);
if (!languageHierarchy) {
throw new Error(`Missing language hierarchy for ${languageName}`);
}
const fallbackLanguageOrder = [languageName];
if (fallbacks !== 'none') {
fallbackLanguageOrder.unshift(...languageHierarchy.reverse());
if (fallbacks === 'all' && fallbackLanguageOrder[0] !== devLanguage) {

@@ -278,23 +251,17 @@ fallbackLanguageOrder.unshift(devLanguage);

}
return fallbackLanguageOrder;
}
function getNamespaceByFilePath(relativePath, {
translationsDirectorySuffix = defaultTranslationDirSuffix
}) {
let namespace = path__default['default'].dirname(relativePath).replace(/^src\//, '').replace(/\//g, '_');
let namespace = path__default["default"].dirname(relativePath).replace(/^src\//, '').replace(/\//g, '_');
if (namespace.endsWith(translationsDirectorySuffix)) {
namespace = namespace.slice(0, -translationsDirectorySuffix.length);
}
return namespace;
}
function printValidationError(...params) {
// eslint-disable-next-line no-console
console.error(chalk__default['default'].red('Error loading translation:'), ...params);
console.error(chalk__default["default"].red('Error loading translation:'), ...params);
}
function getTranslationsFromFile(translationFileContents, {

@@ -308,3 +275,2 @@ isAltLanguage,

}
const {

@@ -315,19 +281,15 @@ $namespace,

} = translationFileContents;
if (isAltLanguage && $namespace) {
printValidationError(`Found $namespace in alt language file in ${filePath}. $namespace is only used in the dev language and will be ignored.`);
}
if (!isAltLanguage && $namespace && typeof $namespace !== 'string') {
printValidationError(`Found non-string $namespace in language file in ${filePath}. $namespace must be a string.`);
}
if (isAltLanguage && _meta !== null && _meta !== void 0 && _meta.tags) {
printValidationError(`Found _meta.tags in alt language file in ${filePath}. _meta.tags is only used in the dev language and will be ignored.`);
} // Never return tags if we're fetching translations for an alt language
}
// Never return tags if we're fetching translations for an alt language
const includeTags = !isAltLanguage && withTags;
const validKeys = {};
for (const [translationKey, {

@@ -341,3 +303,2 @@ tags,

}
if (!translation) {

@@ -347,3 +308,2 @@ printValidationError(`Found empty translation "${translationKey}" in ${filePath}. Translation must be an object of the format {message: string}.`);

}
if (!translation.message || typeof translation.message !== 'string') {

@@ -353,8 +313,7 @@ printValidationError(`No message found for translation "${translationKey}" in ${filePath}. Translation must be an object of the format {message: string}.`);

}
validKeys[translationKey] = { ...translation,
validKeys[translationKey] = {
...translation,
tags: includeTags ? tags : undefined
};
}
const metadata = {

@@ -369,3 +328,2 @@ tags: includeTags ? _meta === null || _meta === void 0 ? void 0 : _meta.tags : undefined

}
function loadAltLanguageFile({

@@ -388,3 +346,2 @@ filePath,

trace(`Loading alt language file with precedence: ${fallbackLanguageOrder.slice().reverse().join(' -> ')}`);
for (const fallbackLanguage of fallbackLanguageOrder) {

@@ -395,5 +352,3 @@ if (fallbackLanguage !== devLanguage) {

delete require.cache[altFilePath];
const translationFile = require(altFilePath);
const {

@@ -417,6 +372,4 @@ keys: fallbackLanguageTranslation

}
return altLanguageTranslation;
}
function stripTagsFromTranslations(translations) {

@@ -428,3 +381,2 @@ return Object.fromEntries(Object.entries(translations).map(([key, {

}
function loadTranslation({

@@ -438,6 +390,4 @@ filePath,

delete require.cache[filePath];
const translationContent = require(filePath);
const relativePath = path__default['default'].relative(userConfig.projectRoot || process.cwd(), filePath);
const relativePath = path__default["default"].relative(userConfig.projectRoot || process.cwd(), filePath);
const {

@@ -457,3 +407,2 @@ $namespace,

const altLanguages = getAltLanguages(userConfig);
for (const languageName of altLanguages) {

@@ -467,3 +416,2 @@ languageSet[languageName] = loadAltLanguageFile({

}
for (const generatedLanguage of userConfig.generatedLanguages || []) {

@@ -481,3 +429,2 @@ const {

}
return {

@@ -501,3 +448,3 @@ filePath,

} = config;
const translationFiles = await glob__default['default'](getDevTranslationFileGlob(config), {
const translationFiles = await glob__default["default"](getDevTranslationFileGlob(config), {
ignore: includeNodeModules ? ignore : [...ignore, '**/node_modules/**'],

@@ -514,7 +461,5 @@ absolute: true,

const keys = new Set();
for (const loadedTranslation of result) {
for (const key of loadedTranslation.keys) {
const uniqueKey = getUniqueKey(key, loadedTranslation.namespace);
if (keys.has(uniqueKey)) {

@@ -524,7 +469,5 @@ trace(`Duplicate keys found`);

}
keys.add(uniqueKey);
}
}
return result;

@@ -534,5 +477,3 @@ }

const encodeWithinSingleQuotes = v => v.replace(/'/g, "\\'");
const encodeBackslash = v => v.replace(/\\/g, '\\\\');
function extractHasTags(ast) {

@@ -544,11 +485,8 @@ return ast.some(element => {

}
return icuMessageformatParser.isTagElement(element);
});
}
function extractParamTypes(ast) {
let params = {};
let imports = new Set();
for (const element of ast) {

@@ -561,2 +499,11 @@ if (icuMessageformatParser.isArgumentElement(element)) {

params[element.value] = 'number';
const children = Object.values(element.options).map(o => o.value);
for (const child of children) {
const [subParams, subImports] = extractParamTypes(child);
imports = new Set([...imports, ...subImports]);
params = {
...params,
...subParams
};
}
} else if (icuMessageformatParser.isDateElement(element) || icuMessageformatParser.isTimeElement(element)) {

@@ -569,3 +516,4 @@ params[element.value] = 'Date | number';

imports = new Set([...imports, ...subImports]);
params = { ...params,
params = {
...params,
...subParams

@@ -576,7 +524,7 @@ };

const children = Object.values(element.options).map(o => o.value);
for (const child of children) {
const [subParams, subImports] = extractParamTypes(child);
imports = new Set([...imports, ...subImports]);
params = { ...params,
params = {
...params,
...subParams

@@ -587,9 +535,6 @@ };

}
return [params, imports];
}
function serialiseObjectToType(v) {
let result = '';
for (const [key, value] of Object.entries(v)) {

@@ -602,12 +547,8 @@ if (value && typeof value === 'object') {

}
return `{ ${result} }`;
}
const banner = `// This file is automatically generated by Vocab.\n// To make changes update translation.json files directly.`;
function serialiseTranslationRuntime(value, imports, loadedTranslation) {
trace('Serialising translations:', loadedTranslation);
const translationsType = {};
for (const [key, {

@@ -619,3 +560,2 @@ params,

let translationFunctionString = `() => ${message}`;
if (Object.keys(params).length > 0) {

@@ -626,6 +566,4 @@ const formatGeneric = hasTags ? '<T = string>' : '';

}
translationsType[encodeBackslash(key)] = translationFunctionString;
}
const content = Object.entries(loadedTranslation.languages).map(([languageName, translations]) => `'${encodeWithinSingleQuotes(languageName)}': createLanguage(${JSON.stringify(getTranslationMessages(translations))})`).join(',');

@@ -642,3 +580,2 @@ const languagesUnionAsString = Object.keys(loadedTranslation.languages).map(l => `'${l}'`).join(' | ');

}
async function generateRuntime(loadedTranslation) {

@@ -652,3 +589,2 @@ const {

let imports = new Set();
for (const key of loadedTranslation.keys) {

@@ -658,3 +594,2 @@ let params = {};

let hasTags = false;
for (const translatedLanguage of Object.values(loadedLanguages)) {

@@ -666,3 +601,4 @@ if (translatedLanguage[key]) {

imports = new Set([...imports, ...parsedImports]);
params = { ...params,
params = {
...params,
...parsedParams

@@ -673,3 +609,2 @@ };

}
const returnType = hasTags ? 'NonNullable<ReactNode>' : 'string';

@@ -683,6 +618,6 @@ translationTypes.set(key, {

}
const prettierConfig = await prettier__default['default'].resolveConfig(filePath);
const prettierConfig = await prettier__default["default"].resolveConfig(filePath);
const serializedTranslationType = serialiseTranslationRuntime(translationTypes, imports, loadedTranslation);
const declaration = prettier__default['default'].format(serializedTranslationType, { ...prettierConfig,
const declaration = prettier__default["default"].format(serializedTranslationType, {
...prettierConfig,
parser: 'typescript'

@@ -696,3 +631,3 @@ });

const cwd = config.projectRoot || process.cwd();
const watcher = chokidar__default['default'].watch([getDevTranslationFileGlob(config), getAltTranslationFileGlob(config), getTranslationFolderGlob(config)], {
const watcher = chokidar__default["default"].watch([getDevTranslationFileGlob(config), getAltTranslationFileGlob(config), getTranslationFolderGlob(config)], {
cwd,

@@ -702,13 +637,10 @@ ignored: config.ignore ? [...config.ignore, '**/node_modules/**'] : ['**/node_modules/**'],

});
const onTranslationChange = async relativePath => {
trace(`Detected change for file ${relativePath}`);
let targetFile;
if (isDevLanguageFile(relativePath)) {
targetFile = path__default['default'].resolve(cwd, relativePath);
targetFile = path__default["default"].resolve(cwd, relativePath);
} else if (isAltLanguageFile(relativePath)) {
targetFile = getDevLanguageFileFromAltLanguageFile(path__default['default'].resolve(cwd, relativePath));
targetFile = getDevLanguageFileFromAltLanguageFile(path__default["default"].resolve(cwd, relativePath));
}
if (targetFile) {

@@ -723,4 +655,4 @@ try {

// eslint-disable-next-line no-console
console.log('Failed to generate types for', relativePath); // eslint-disable-next-line no-console
console.log('Failed to generate types for', relativePath);
// eslint-disable-next-line no-console
console.error(e);

@@ -730,6 +662,4 @@ }

};
const onNewDirectory = async relativePath => {
trace('Detected new directory', relativePath);
if (!isTranslationDirectory(relativePath, config)) {

@@ -739,5 +669,3 @@ trace('Ignoring non-translation directory:', relativePath);

}
const newFilePath = path__default['default'].join(relativePath, devTranslationFileName);
const newFilePath = path__default["default"].join(relativePath, devTranslationFileName);
if (!fs.existsSync(newFilePath)) {

@@ -750,3 +678,2 @@ await fs.promises.writeFile(newFilePath, JSON.stringify({}, null, 2));

};
watcher.on('addDir', onNewDirectory);

@@ -763,7 +690,5 @@ watcher.on('add', onTranslationChange).on('change', onTranslationChange);

}, config);
for (const loadedTranslation of translations) {
await generateRuntime(loadedTranslation);
}
if (shouldWatch) {

@@ -774,6 +699,4 @@ trace('Listening for changes to files...');

}
async function writeIfChanged(filepath, contents) {
let hasChanged = true;
try {

@@ -784,5 +707,5 @@ const existingContents = await fs.promises.readFile(filepath, {

hasChanged = existingContents !== contents;
} catch (e) {// ignore error, likely a file doesn't exist error so we want to write anyway
} catch (e) {
// ignore error, likely a file doesn't exist error so we want to write anyway
}
if (hasChanged) {

@@ -798,20 +721,14 @@ await fs.promises.writeFile(filepath, contents, {

const devLanguage = loadedTranslation.languages[devLanguageName];
if (!devLanguage) {
throw new Error(`Failed to load dev language: ${loadedTranslation.filePath}`);
}
const result = {};
let valid = true;
const requiredKeys = Object.keys(devLanguage);
if (requiredKeys.length > 0) {
for (const altLanguageName of altLanguages) {
var _loadedTranslation$la;
const altLanguage = (_loadedTranslation$la = loadedTranslation.languages[altLanguageName]) !== null && _loadedTranslation$la !== void 0 ? _loadedTranslation$la : {};
for (const key of requiredKeys) {
var _altLanguage$key;
if (typeof ((_altLanguage$key = altLanguage[key]) === null || _altLanguage$key === void 0 ? void 0 : _altLanguage$key.message) !== 'string') {

@@ -821,3 +738,2 @@ if (!result[altLanguageName]) {

}
result[altLanguageName].push(key);

@@ -829,3 +745,2 @@ valid = false;

}
return [valid, result];

@@ -839,17 +754,13 @@ }

let valid = true;
for (const loadedTranslation of allTranslations) {
const [translationValid, result] = findMissingKeys(loadedTranslation, config.devLanguage, getAltLanguages(config));
if (!translationValid) {
valid = false;
console.log(chalk__default['default'].red`Incomplete translations: "${chalk__default['default'].bold(loadedTranslation.relativePath)}"`);
console.log(chalk__default["default"].red`Incomplete translations: "${chalk__default["default"].bold(loadedTranslation.relativePath)}"`);
for (const lang of Object.keys(result)) {
const missingKeys = result[lang];
console.log(chalk__default['default'].yellow(lang), '->', missingKeys.map(v => `"${v}"`).join(', '));
console.log(chalk__default["default"].yellow(lang), '->', missingKeys.map(v => `"${v}"`).join(', '));
}
}
}
return valid;

@@ -864,6 +775,5 @@ }

}
}
const validator = new Validator__default['default']();
const validator = new Validator__default["default"]();
const schema = {

@@ -933,73 +843,61 @@ $$strict: true,

const checkConfigFile = validator.compile(schema);
const splitMap = (message, callback) => message.split(' ,').map(v => callback(v)).join(' ,');
function validateConfig(c) {
trace('Validating configuration file'); // Note: checkConfigFile mutates the config file by applying defaults
trace('Validating configuration file');
// Note: checkConfigFile mutates the config file by applying defaults
const isValid = checkConfigFile(c);
if (isValid !== true) {
throw new ValidationError('InvalidStructure', (Array.isArray(isValid) ? isValid : []).map(v => {
if (v.type === 'objectStrict') {
return `Invalid key(s) ${splitMap(v.actual, m => `"${chalk__default['default'].cyan(m)}"`)}. Expected one of ${splitMap(v.expected, chalk__default['default'].green)}`;
return `Invalid key(s) ${splitMap(v.actual, m => `"${chalk__default["default"].cyan(m)}"`)}. Expected one of ${splitMap(v.expected, chalk__default["default"].green)}`;
}
if (v.field) {
var _v$message;
return (_v$message = v.message) === null || _v$message === void 0 ? void 0 : _v$message.replace(v.field, chalk__default['default'].cyan(v.field));
return (_v$message = v.message) === null || _v$message === void 0 ? void 0 : _v$message.replace(v.field, chalk__default["default"].cyan(v.field));
}
return v.message;
}).join(' \n'));
}
const languageStrings = c.languages.map(v => v.name);
const languageStrings = c.languages.map(v => v.name); // Dev Language should exist in languages
// Dev Language should exist in languages
if (!languageStrings.includes(c.devLanguage)) {
throw new ValidationError('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(', ')}.`);
}
const foundLanguages = [];
for (const lang of c.languages) {
// Languages must only exist once
if (foundLanguages.includes(lang.name)) {
throw new ValidationError('DuplicateLanguage', `The language "${chalk__default['default'].bold.cyan(lang.name)}" was defined multiple times.`);
throw new ValidationError('DuplicateLanguage', `The language "${chalk__default["default"].bold.cyan(lang.name)}" was defined multiple times.`);
}
foundLanguages.push(lang.name);
foundLanguages.push(lang.name); // Any extends must be in languages
// Any extends must be in languages
if (lang.extends && !languageStrings.includes(lang.extends)) {
throw new ValidationError('InvalidExtends', `The language "${chalk__default['default'].bold.cyan(lang.name)}"'s extends of ${chalk__default['default'].bold.cyan(lang.extends)} was not found in languages ${languageStrings.join(', ')}.`);
throw new ValidationError('InvalidExtends', `The language "${chalk__default["default"].bold.cyan(lang.name)}"'s extends of ${chalk__default["default"].bold.cyan(lang.extends)} was not found in languages ${languageStrings.join(', ')}.`);
}
}
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.`);
throw new ValidationError('DuplicateGeneratedLanguage', `The generated language "${chalk__default["default"].bold.cyan(generatedLang.name)}" was defined multiple times.`);
}
foundGeneratedLanguages.push(generatedLang.name);
foundGeneratedLanguages.push(generatedLang.name); // Generated language names must not conflict with language names
// 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
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(', ')}.`);
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');
return true;
}
function createConfig(configFilePath) {
const cwd = path__default['default'].dirname(configFilePath);
const cwd = path__default["default"].dirname(configFilePath);
return {

@@ -1010,6 +908,4 @@ projectRoot: cwd,

}
async function resolveConfig(customConfigFilePath) {
const configFilePath = customConfigFilePath ? path__default['default'].resolve(customConfigFilePath) : await findUp__default['default']('vocab.config.js');
const configFilePath = customConfigFilePath ? path__default["default"].resolve(customConfigFilePath) : await findUp__default["default"]('vocab.config.js');
if (configFilePath) {

@@ -1019,3 +915,2 @@ trace(`Resolved configuration file to ${configFilePath}`);

}
trace('No configuration file found');

@@ -1025,4 +920,3 @@ return null;

function resolveConfigSync(customConfigFilePath) {
const configFilePath = customConfigFilePath ? path__default['default'].resolve(customConfigFilePath) : findUp__default['default'].sync('vocab.config.js');
const configFilePath = customConfigFilePath ? path__default["default"].resolve(customConfigFilePath) : findUp__default["default"].sync('vocab.config.js');
if (configFilePath) {

@@ -1032,3 +926,2 @@ trace(`Resolved configuration file to ${configFilePath}`);

}
trace('No configuration file found');

@@ -1035,0 +928,0 @@ return null;

@@ -30,3 +30,3 @@ 'use strict';

const trace = debug__default['default'](`vocab:core`);
const trace = debug__default["default"](`vocab:core`);

@@ -78,4 +78,4 @@ const defaultTranslationDirSuffix = '.vocab';

function getDevLanguageFileFromTsFile(tsFilePath) {
const directory = path__default['default'].dirname(tsFilePath);
const result = path__default['default'].normalize(path__default['default'].join(directory, devTranslationFileName));
const directory = path__default["default"].dirname(tsFilePath);
const result = path__default["default"].normalize(path__default["default"].join(directory, devTranslationFileName));
trace(`Returning dev language path ${result} for path ${tsFilePath}`);

@@ -85,4 +85,4 @@ return result;

function getDevLanguageFileFromAltLanguageFile(altLanguageFilePath) {
const directory = path__default['default'].dirname(altLanguageFilePath);
const result = path__default['default'].normalize(path__default['default'].join(directory, devTranslationFileName));
const directory = path__default["default"].dirname(altLanguageFilePath);
const result = path__default["default"].normalize(path__default["default"].join(directory, devTranslationFileName));
trace(`Returning dev language path ${result} for path ${altLanguageFilePath}`);

@@ -92,4 +92,4 @@ return result;

function getTSFileFromDevLanguageFile(devLanguageFilePath) {
const directory = path__default['default'].dirname(devLanguageFilePath);
const result = path__default['default'].normalize(path__default['default'].join(directory, 'index.ts'));
const directory = path__default["default"].dirname(devLanguageFilePath);
const result = path__default["default"].normalize(path__default["default"].join(directory, 'index.ts'));
trace(`Returning TS path ${result} for path ${devLanguageFilePath}`);

@@ -99,6 +99,6 @@ return result;

function getAltLanguageFilePath(devLanguageFilePath, language) {
const directory = path__default['default'].dirname(devLanguageFilePath);
const result = path__default['default'].normalize(path__default['default'].join(directory, `${language}.translations.json`));
const directory = path__default["default"].dirname(devLanguageFilePath);
const result = path__default["default"].normalize(path__default["default"].join(directory, `${language}.translations.json`));
trace(`Returning alt language path ${result} for path ${devLanguageFilePath}`);
return path__default['default'].normalize(result);
return path__default["default"].normalize(result);
}

@@ -108,7 +108,5 @@ function mapValues(obj, func) {

const keys = Object.keys(obj);
for (const key of keys) {
newObj[key] = func(obj[key]);
}
return newObj;

@@ -127,20 +125,15 @@ }

}
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 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] = {

@@ -150,11 +143,9 @@ message: transformedMessage

}
return generatedTranslations;
}
function transformMessageFormatElement(transformElement) {
return messageFormatElement => {
const transformedMessageFormatElement = { ...messageFormatElement
const transformedMessageFormatElement = {
...messageFormatElement
};
switch (transformedMessageFormatElement.type) {

@@ -165,14 +156,11 @@ case icuMessageformatParser.TYPE.literal:

break;
case icuMessageformatParser.TYPE.select:
case icuMessageformatParser.TYPE.plural:
const transformedOptions = { ...transformedMessageFormatElement.options
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:

@@ -183,3 +171,2 @@ const transformedChildren = transformedMessageFormatElement.children.map(transformMessageFormatElement(transformElement));

}
return transformedMessageFormatElement;

@@ -199,3 +186,2 @@ };

const newLanguage = {};
for (const key of keys) {

@@ -209,6 +195,4 @@ if (translation[key]) {

}
return newLanguage;
}
function getLanguageFallbacks({

@@ -218,3 +202,2 @@ languages

const languageFallbackMap = new Map();
for (const lang of languages) {

@@ -225,6 +208,4 @@ if (lang.extends) {

}
return languageFallbackMap;
}
function getLanguageHierarchy({

@@ -237,7 +218,5 @@ languages

});
for (const lang of languages) {
const langHierarchy = [];
let currLang = lang.extends;
while (currLang) {

@@ -247,6 +226,4 @@ langHierarchy.push(currLang);

}
hierarchyMap.set(lang.name, langHierarchy);
}
return hierarchyMap;

@@ -263,12 +240,8 @@ }

}).get(languageName);
if (!languageHierarchy) {
throw new Error(`Missing language hierarchy for ${languageName}`);
}
const fallbackLanguageOrder = [languageName];
if (fallbacks !== 'none') {
fallbackLanguageOrder.unshift(...languageHierarchy.reverse());
if (fallbacks === 'all' && fallbackLanguageOrder[0] !== devLanguage) {

@@ -278,23 +251,17 @@ fallbackLanguageOrder.unshift(devLanguage);

}
return fallbackLanguageOrder;
}
function getNamespaceByFilePath(relativePath, {
translationsDirectorySuffix = defaultTranslationDirSuffix
}) {
let namespace = path__default['default'].dirname(relativePath).replace(/^src\//, '').replace(/\//g, '_');
let namespace = path__default["default"].dirname(relativePath).replace(/^src\//, '').replace(/\//g, '_');
if (namespace.endsWith(translationsDirectorySuffix)) {
namespace = namespace.slice(0, -translationsDirectorySuffix.length);
}
return namespace;
}
function printValidationError(...params) {
// eslint-disable-next-line no-console
console.error(chalk__default['default'].red('Error loading translation:'), ...params);
console.error(chalk__default["default"].red('Error loading translation:'), ...params);
}
function getTranslationsFromFile(translationFileContents, {

@@ -308,3 +275,2 @@ isAltLanguage,

}
const {

@@ -315,19 +281,15 @@ $namespace,

} = translationFileContents;
if (isAltLanguage && $namespace) {
printValidationError(`Found $namespace in alt language file in ${filePath}. $namespace is only used in the dev language and will be ignored.`);
}
if (!isAltLanguage && $namespace && typeof $namespace !== 'string') {
printValidationError(`Found non-string $namespace in language file in ${filePath}. $namespace must be a string.`);
}
if (isAltLanguage && _meta !== null && _meta !== void 0 && _meta.tags) {
printValidationError(`Found _meta.tags in alt language file in ${filePath}. _meta.tags is only used in the dev language and will be ignored.`);
} // Never return tags if we're fetching translations for an alt language
}
// Never return tags if we're fetching translations for an alt language
const includeTags = !isAltLanguage && withTags;
const validKeys = {};
for (const [translationKey, {

@@ -341,3 +303,2 @@ tags,

}
if (!translation) {

@@ -347,3 +308,2 @@ printValidationError(`Found empty translation "${translationKey}" in ${filePath}. Translation must be an object of the format {message: string}.`);

}
if (!translation.message || typeof translation.message !== 'string') {

@@ -353,8 +313,7 @@ printValidationError(`No message found for translation "${translationKey}" in ${filePath}. Translation must be an object of the format {message: string}.`);

}
validKeys[translationKey] = { ...translation,
validKeys[translationKey] = {
...translation,
tags: includeTags ? tags : undefined
};
}
const metadata = {

@@ -369,3 +328,2 @@ tags: includeTags ? _meta === null || _meta === void 0 ? void 0 : _meta.tags : undefined

}
function loadAltLanguageFile({

@@ -388,3 +346,2 @@ filePath,

trace(`Loading alt language file with precedence: ${fallbackLanguageOrder.slice().reverse().join(' -> ')}`);
for (const fallbackLanguage of fallbackLanguageOrder) {

@@ -395,5 +352,3 @@ if (fallbackLanguage !== devLanguage) {

delete require.cache[altFilePath];
const translationFile = require(altFilePath);
const {

@@ -417,6 +372,4 @@ keys: fallbackLanguageTranslation

}
return altLanguageTranslation;
}
function stripTagsFromTranslations(translations) {

@@ -428,3 +381,2 @@ return Object.fromEntries(Object.entries(translations).map(([key, {

}
function loadTranslation({

@@ -438,6 +390,4 @@ filePath,

delete require.cache[filePath];
const translationContent = require(filePath);
const relativePath = path__default['default'].relative(userConfig.projectRoot || process.cwd(), filePath);
const relativePath = path__default["default"].relative(userConfig.projectRoot || process.cwd(), filePath);
const {

@@ -457,3 +407,2 @@ $namespace,

const altLanguages = getAltLanguages(userConfig);
for (const languageName of altLanguages) {

@@ -467,3 +416,2 @@ languageSet[languageName] = loadAltLanguageFile({

}
for (const generatedLanguage of userConfig.generatedLanguages || []) {

@@ -481,3 +429,2 @@ const {

}
return {

@@ -501,3 +448,3 @@ filePath,

} = config;
const translationFiles = await glob__default['default'](getDevTranslationFileGlob(config), {
const translationFiles = await glob__default["default"](getDevTranslationFileGlob(config), {
ignore: includeNodeModules ? ignore : [...ignore, '**/node_modules/**'],

@@ -514,7 +461,5 @@ absolute: true,

const keys = new Set();
for (const loadedTranslation of result) {
for (const key of loadedTranslation.keys) {
const uniqueKey = getUniqueKey(key, loadedTranslation.namespace);
if (keys.has(uniqueKey)) {

@@ -524,7 +469,5 @@ trace(`Duplicate keys found`);

}
keys.add(uniqueKey);
}
}
return result;

@@ -534,5 +477,3 @@ }

const encodeWithinSingleQuotes = v => v.replace(/'/g, "\\'");
const encodeBackslash = v => v.replace(/\\/g, '\\\\');
function extractHasTags(ast) {

@@ -544,11 +485,8 @@ return ast.some(element => {

}
return icuMessageformatParser.isTagElement(element);
});
}
function extractParamTypes(ast) {
let params = {};
let imports = new Set();
for (const element of ast) {

@@ -561,2 +499,11 @@ if (icuMessageformatParser.isArgumentElement(element)) {

params[element.value] = 'number';
const children = Object.values(element.options).map(o => o.value);
for (const child of children) {
const [subParams, subImports] = extractParamTypes(child);
imports = new Set([...imports, ...subImports]);
params = {
...params,
...subParams
};
}
} else if (icuMessageformatParser.isDateElement(element) || icuMessageformatParser.isTimeElement(element)) {

@@ -569,3 +516,4 @@ params[element.value] = 'Date | number';

imports = new Set([...imports, ...subImports]);
params = { ...params,
params = {
...params,
...subParams

@@ -576,7 +524,7 @@ };

const children = Object.values(element.options).map(o => o.value);
for (const child of children) {
const [subParams, subImports] = extractParamTypes(child);
imports = new Set([...imports, ...subImports]);
params = { ...params,
params = {
...params,
...subParams

@@ -587,9 +535,6 @@ };

}
return [params, imports];
}
function serialiseObjectToType(v) {
let result = '';
for (const [key, value] of Object.entries(v)) {

@@ -602,12 +547,8 @@ if (value && typeof value === 'object') {

}
return `{ ${result} }`;
}
const banner = `// This file is automatically generated by Vocab.\n// To make changes update translation.json files directly.`;
function serialiseTranslationRuntime(value, imports, loadedTranslation) {
trace('Serialising translations:', loadedTranslation);
const translationsType = {};
for (const [key, {

@@ -619,3 +560,2 @@ params,

let translationFunctionString = `() => ${message}`;
if (Object.keys(params).length > 0) {

@@ -626,6 +566,4 @@ const formatGeneric = hasTags ? '<T = string>' : '';

}
translationsType[encodeBackslash(key)] = translationFunctionString;
}
const content = Object.entries(loadedTranslation.languages).map(([languageName, translations]) => `'${encodeWithinSingleQuotes(languageName)}': createLanguage(${JSON.stringify(getTranslationMessages(translations))})`).join(',');

@@ -642,3 +580,2 @@ const languagesUnionAsString = Object.keys(loadedTranslation.languages).map(l => `'${l}'`).join(' | ');

}
async function generateRuntime(loadedTranslation) {

@@ -652,3 +589,2 @@ const {

let imports = new Set();
for (const key of loadedTranslation.keys) {

@@ -658,3 +594,2 @@ let params = {};

let hasTags = false;
for (const translatedLanguage of Object.values(loadedLanguages)) {

@@ -666,3 +601,4 @@ if (translatedLanguage[key]) {

imports = new Set([...imports, ...parsedImports]);
params = { ...params,
params = {
...params,
...parsedParams

@@ -673,3 +609,2 @@ };

}
const returnType = hasTags ? 'NonNullable<ReactNode>' : 'string';

@@ -683,6 +618,6 @@ translationTypes.set(key, {

}
const prettierConfig = await prettier__default['default'].resolveConfig(filePath);
const prettierConfig = await prettier__default["default"].resolveConfig(filePath);
const serializedTranslationType = serialiseTranslationRuntime(translationTypes, imports, loadedTranslation);
const declaration = prettier__default['default'].format(serializedTranslationType, { ...prettierConfig,
const declaration = prettier__default["default"].format(serializedTranslationType, {
...prettierConfig,
parser: 'typescript'

@@ -696,3 +631,3 @@ });

const cwd = config.projectRoot || process.cwd();
const watcher = chokidar__default['default'].watch([getDevTranslationFileGlob(config), getAltTranslationFileGlob(config), getTranslationFolderGlob(config)], {
const watcher = chokidar__default["default"].watch([getDevTranslationFileGlob(config), getAltTranslationFileGlob(config), getTranslationFolderGlob(config)], {
cwd,

@@ -702,13 +637,10 @@ ignored: config.ignore ? [...config.ignore, '**/node_modules/**'] : ['**/node_modules/**'],

});
const onTranslationChange = async relativePath => {
trace(`Detected change for file ${relativePath}`);
let targetFile;
if (isDevLanguageFile(relativePath)) {
targetFile = path__default['default'].resolve(cwd, relativePath);
targetFile = path__default["default"].resolve(cwd, relativePath);
} else if (isAltLanguageFile(relativePath)) {
targetFile = getDevLanguageFileFromAltLanguageFile(path__default['default'].resolve(cwd, relativePath));
targetFile = getDevLanguageFileFromAltLanguageFile(path__default["default"].resolve(cwd, relativePath));
}
if (targetFile) {

@@ -723,4 +655,4 @@ try {

// eslint-disable-next-line no-console
console.log('Failed to generate types for', relativePath); // eslint-disable-next-line no-console
console.log('Failed to generate types for', relativePath);
// eslint-disable-next-line no-console
console.error(e);

@@ -730,6 +662,4 @@ }

};
const onNewDirectory = async relativePath => {
trace('Detected new directory', relativePath);
if (!isTranslationDirectory(relativePath, config)) {

@@ -739,5 +669,3 @@ trace('Ignoring non-translation directory:', relativePath);

}
const newFilePath = path__default['default'].join(relativePath, devTranslationFileName);
const newFilePath = path__default["default"].join(relativePath, devTranslationFileName);
if (!fs.existsSync(newFilePath)) {

@@ -750,3 +678,2 @@ await fs.promises.writeFile(newFilePath, JSON.stringify({}, null, 2));

};
watcher.on('addDir', onNewDirectory);

@@ -763,7 +690,5 @@ watcher.on('add', onTranslationChange).on('change', onTranslationChange);

}, config);
for (const loadedTranslation of translations) {
await generateRuntime(loadedTranslation);
}
if (shouldWatch) {

@@ -774,6 +699,4 @@ trace('Listening for changes to files...');

}
async function writeIfChanged(filepath, contents) {
let hasChanged = true;
try {

@@ -784,5 +707,5 @@ const existingContents = await fs.promises.readFile(filepath, {

hasChanged = existingContents !== contents;
} catch (e) {// ignore error, likely a file doesn't exist error so we want to write anyway
} catch (e) {
// ignore error, likely a file doesn't exist error so we want to write anyway
}
if (hasChanged) {

@@ -798,20 +721,14 @@ await fs.promises.writeFile(filepath, contents, {

const devLanguage = loadedTranslation.languages[devLanguageName];
if (!devLanguage) {
throw new Error(`Failed to load dev language: ${loadedTranslation.filePath}`);
}
const result = {};
let valid = true;
const requiredKeys = Object.keys(devLanguage);
if (requiredKeys.length > 0) {
for (const altLanguageName of altLanguages) {
var _loadedTranslation$la;
const altLanguage = (_loadedTranslation$la = loadedTranslation.languages[altLanguageName]) !== null && _loadedTranslation$la !== void 0 ? _loadedTranslation$la : {};
for (const key of requiredKeys) {
var _altLanguage$key;
if (typeof ((_altLanguage$key = altLanguage[key]) === null || _altLanguage$key === void 0 ? void 0 : _altLanguage$key.message) !== 'string') {

@@ -821,3 +738,2 @@ if (!result[altLanguageName]) {

}
result[altLanguageName].push(key);

@@ -829,3 +745,2 @@ valid = false;

}
return [valid, result];

@@ -839,17 +754,13 @@ }

let valid = true;
for (const loadedTranslation of allTranslations) {
const [translationValid, result] = findMissingKeys(loadedTranslation, config.devLanguage, getAltLanguages(config));
if (!translationValid) {
valid = false;
console.log(chalk__default['default'].red`Incomplete translations: "${chalk__default['default'].bold(loadedTranslation.relativePath)}"`);
console.log(chalk__default["default"].red`Incomplete translations: "${chalk__default["default"].bold(loadedTranslation.relativePath)}"`);
for (const lang of Object.keys(result)) {
const missingKeys = result[lang];
console.log(chalk__default['default'].yellow(lang), '->', missingKeys.map(v => `"${v}"`).join(', '));
console.log(chalk__default["default"].yellow(lang), '->', missingKeys.map(v => `"${v}"`).join(', '));
}
}
}
return valid;

@@ -864,6 +775,5 @@ }

}
}
const validator = new Validator__default['default']();
const validator = new Validator__default["default"]();
const schema = {

@@ -933,73 +843,61 @@ $$strict: true,

const checkConfigFile = validator.compile(schema);
const splitMap = (message, callback) => message.split(' ,').map(v => callback(v)).join(' ,');
function validateConfig(c) {
trace('Validating configuration file'); // Note: checkConfigFile mutates the config file by applying defaults
trace('Validating configuration file');
// Note: checkConfigFile mutates the config file by applying defaults
const isValid = checkConfigFile(c);
if (isValid !== true) {
throw new ValidationError('InvalidStructure', (Array.isArray(isValid) ? isValid : []).map(v => {
if (v.type === 'objectStrict') {
return `Invalid key(s) ${splitMap(v.actual, m => `"${chalk__default['default'].cyan(m)}"`)}. Expected one of ${splitMap(v.expected, chalk__default['default'].green)}`;
return `Invalid key(s) ${splitMap(v.actual, m => `"${chalk__default["default"].cyan(m)}"`)}. Expected one of ${splitMap(v.expected, chalk__default["default"].green)}`;
}
if (v.field) {
var _v$message;
return (_v$message = v.message) === null || _v$message === void 0 ? void 0 : _v$message.replace(v.field, chalk__default['default'].cyan(v.field));
return (_v$message = v.message) === null || _v$message === void 0 ? void 0 : _v$message.replace(v.field, chalk__default["default"].cyan(v.field));
}
return v.message;
}).join(' \n'));
}
const languageStrings = c.languages.map(v => v.name);
const languageStrings = c.languages.map(v => v.name); // Dev Language should exist in languages
// Dev Language should exist in languages
if (!languageStrings.includes(c.devLanguage)) {
throw new ValidationError('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(', ')}.`);
}
const foundLanguages = [];
for (const lang of c.languages) {
// Languages must only exist once
if (foundLanguages.includes(lang.name)) {
throw new ValidationError('DuplicateLanguage', `The language "${chalk__default['default'].bold.cyan(lang.name)}" was defined multiple times.`);
throw new ValidationError('DuplicateLanguage', `The language "${chalk__default["default"].bold.cyan(lang.name)}" was defined multiple times.`);
}
foundLanguages.push(lang.name);
foundLanguages.push(lang.name); // Any extends must be in languages
// Any extends must be in languages
if (lang.extends && !languageStrings.includes(lang.extends)) {
throw new ValidationError('InvalidExtends', `The language "${chalk__default['default'].bold.cyan(lang.name)}"'s extends of ${chalk__default['default'].bold.cyan(lang.extends)} was not found in languages ${languageStrings.join(', ')}.`);
throw new ValidationError('InvalidExtends', `The language "${chalk__default["default"].bold.cyan(lang.name)}"'s extends of ${chalk__default["default"].bold.cyan(lang.extends)} was not found in languages ${languageStrings.join(', ')}.`);
}
}
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.`);
throw new ValidationError('DuplicateGeneratedLanguage', `The generated language "${chalk__default["default"].bold.cyan(generatedLang.name)}" was defined multiple times.`);
}
foundGeneratedLanguages.push(generatedLang.name);
foundGeneratedLanguages.push(generatedLang.name); // Generated language names must not conflict with language names
// 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
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(', ')}.`);
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');
return true;
}
function createConfig(configFilePath) {
const cwd = path__default['default'].dirname(configFilePath);
const cwd = path__default["default"].dirname(configFilePath);
return {

@@ -1010,6 +908,4 @@ projectRoot: cwd,

}
async function resolveConfig(customConfigFilePath) {
const configFilePath = customConfigFilePath ? path__default['default'].resolve(customConfigFilePath) : await findUp__default['default']('vocab.config.js');
const configFilePath = customConfigFilePath ? path__default["default"].resolve(customConfigFilePath) : await findUp__default["default"]('vocab.config.js');
if (configFilePath) {

@@ -1019,3 +915,2 @@ trace(`Resolved configuration file to ${configFilePath}`);

}
trace('No configuration file found');

@@ -1025,4 +920,3 @@ return null;

function resolveConfigSync(customConfigFilePath) {
const configFilePath = customConfigFilePath ? path__default['default'].resolve(customConfigFilePath) : findUp__default['default'].sync('vocab.config.js');
const configFilePath = customConfigFilePath ? path__default["default"].resolve(customConfigFilePath) : findUp__default["default"].sync('vocab.config.js');
if (configFilePath) {

@@ -1032,3 +926,2 @@ trace(`Resolved configuration file to ${configFilePath}`);

}
trace('No configuration file found');

@@ -1035,0 +928,0 @@ return null;

@@ -87,7 +87,5 @@ import { existsSync, promises } from 'fs';

const keys = Object.keys(obj);
for (const key of keys) {
newObj[key] = func(obj[key]);
}
return newObj;

@@ -106,10 +104,7 @@ }

}
const translationKeys = Object.keys(baseTranslations);
const generatedTranslations = {};
for (const translationKey of translationKeys) {
const translation = baseTranslations[translationKey];
let transformedMessage = translation.message;
if (generator.transformElement) {

@@ -120,7 +115,5 @@ const messageAst = new IntlMessageFormat(translation.message).getAst();

}
if (generator.transformMessage) {
transformedMessage = generator.transformMessage(transformedMessage);
}
generatedTranslations[translationKey] = {

@@ -130,11 +123,9 @@ message: transformedMessage

}
return generatedTranslations;
}
function transformMessageFormatElement(transformElement) {
return messageFormatElement => {
const transformedMessageFormatElement = { ...messageFormatElement
const transformedMessageFormatElement = {
...messageFormatElement
};
switch (transformedMessageFormatElement.type) {

@@ -145,14 +136,11 @@ case TYPE.literal:

break;
case TYPE.select:
case TYPE.plural:
const transformedOptions = { ...transformedMessageFormatElement.options
const transformedOptions = {
...transformedMessageFormatElement.options
};
for (const key of Object.keys(transformedOptions)) {
transformedOptions[key].value = transformedOptions[key].value.map(transformMessageFormatElement(transformElement));
}
break;
case TYPE.tag:

@@ -163,3 +151,2 @@ const transformedChildren = transformedMessageFormatElement.children.map(transformMessageFormatElement(transformElement));

}
return transformedMessageFormatElement;

@@ -179,3 +166,2 @@ };

const newLanguage = {};
for (const key of keys) {

@@ -189,6 +175,4 @@ if (translation[key]) {

}
return newLanguage;
}
function getLanguageFallbacks({

@@ -198,3 +182,2 @@ languages

const languageFallbackMap = new Map();
for (const lang of languages) {

@@ -205,6 +188,4 @@ if (lang.extends) {

}
return languageFallbackMap;
}
function getLanguageHierarchy({

@@ -217,7 +198,5 @@ languages

});
for (const lang of languages) {
const langHierarchy = [];
let currLang = lang.extends;
while (currLang) {

@@ -227,6 +206,4 @@ langHierarchy.push(currLang);

}
hierarchyMap.set(lang.name, langHierarchy);
}
return hierarchyMap;

@@ -243,12 +220,8 @@ }

}).get(languageName);
if (!languageHierarchy) {
throw new Error(`Missing language hierarchy for ${languageName}`);
}
const fallbackLanguageOrder = [languageName];
if (fallbacks !== 'none') {
fallbackLanguageOrder.unshift(...languageHierarchy.reverse());
if (fallbacks === 'all' && fallbackLanguageOrder[0] !== devLanguage) {

@@ -258,6 +231,4 @@ fallbackLanguageOrder.unshift(devLanguage);

}
return fallbackLanguageOrder;
}
function getNamespaceByFilePath(relativePath, {

@@ -267,10 +238,7 @@ translationsDirectorySuffix = defaultTranslationDirSuffix

let namespace = path.dirname(relativePath).replace(/^src\//, '').replace(/\//g, '_');
if (namespace.endsWith(translationsDirectorySuffix)) {
namespace = namespace.slice(0, -translationsDirectorySuffix.length);
}
return namespace;
}
function printValidationError(...params) {

@@ -280,3 +248,2 @@ // eslint-disable-next-line no-console

}
function getTranslationsFromFile(translationFileContents, {

@@ -290,3 +257,2 @@ isAltLanguage,

}
const {

@@ -297,19 +263,15 @@ $namespace,

} = translationFileContents;
if (isAltLanguage && $namespace) {
printValidationError(`Found $namespace in alt language file in ${filePath}. $namespace is only used in the dev language and will be ignored.`);
}
if (!isAltLanguage && $namespace && typeof $namespace !== 'string') {
printValidationError(`Found non-string $namespace in language file in ${filePath}. $namespace must be a string.`);
}
if (isAltLanguage && _meta !== null && _meta !== void 0 && _meta.tags) {
printValidationError(`Found _meta.tags in alt language file in ${filePath}. _meta.tags is only used in the dev language and will be ignored.`);
} // Never return tags if we're fetching translations for an alt language
}
// Never return tags if we're fetching translations for an alt language
const includeTags = !isAltLanguage && withTags;
const validKeys = {};
for (const [translationKey, {

@@ -323,3 +285,2 @@ tags,

}
if (!translation) {

@@ -329,3 +290,2 @@ printValidationError(`Found empty translation "${translationKey}" in ${filePath}. Translation must be an object of the format {message: string}.`);

}
if (!translation.message || typeof translation.message !== 'string') {

@@ -335,8 +295,7 @@ printValidationError(`No message found for translation "${translationKey}" in ${filePath}. Translation must be an object of the format {message: string}.`);

}
validKeys[translationKey] = { ...translation,
validKeys[translationKey] = {
...translation,
tags: includeTags ? tags : undefined
};
}
const metadata = {

@@ -351,3 +310,2 @@ tags: includeTags ? _meta === null || _meta === void 0 ? void 0 : _meta.tags : undefined

}
function loadAltLanguageFile({

@@ -370,3 +328,2 @@ filePath,

trace(`Loading alt language file with precedence: ${fallbackLanguageOrder.slice().reverse().join(' -> ')}`);
for (const fallbackLanguage of fallbackLanguageOrder) {

@@ -377,5 +334,3 @@ if (fallbackLanguage !== devLanguage) {

delete require.cache[altFilePath];
const translationFile = require(altFilePath);
const {

@@ -399,6 +354,4 @@ keys: fallbackLanguageTranslation

}
return altLanguageTranslation;
}
function stripTagsFromTranslations(translations) {

@@ -410,3 +363,2 @@ return Object.fromEntries(Object.entries(translations).map(([key, {

}
function loadTranslation({

@@ -420,5 +372,3 @@ filePath,

delete require.cache[filePath];
const translationContent = require(filePath);
const relativePath = path.relative(userConfig.projectRoot || process.cwd(), filePath);

@@ -439,3 +389,2 @@ const {

const altLanguages = getAltLanguages(userConfig);
for (const languageName of altLanguages) {

@@ -449,3 +398,2 @@ languageSet[languageName] = loadAltLanguageFile({

}
for (const generatedLanguage of userConfig.generatedLanguages || []) {

@@ -463,3 +411,2 @@ const {

}
return {

@@ -495,7 +442,5 @@ filePath,

const keys = new Set();
for (const loadedTranslation of result) {
for (const key of loadedTranslation.keys) {
const uniqueKey = getUniqueKey(key, loadedTranslation.namespace);
if (keys.has(uniqueKey)) {

@@ -505,7 +450,5 @@ trace(`Duplicate keys found`);

}
keys.add(uniqueKey);
}
}
return result;

@@ -515,5 +458,3 @@ }

const encodeWithinSingleQuotes = v => v.replace(/'/g, "\\'");
const encodeBackslash = v => v.replace(/\\/g, '\\\\');
function extractHasTags(ast) {

@@ -525,11 +466,8 @@ return ast.some(element => {

}
return isTagElement(element);
});
}
function extractParamTypes(ast) {
let params = {};
let imports = new Set();
for (const element of ast) {

@@ -542,2 +480,11 @@ if (isArgumentElement(element)) {

params[element.value] = 'number';
const children = Object.values(element.options).map(o => o.value);
for (const child of children) {
const [subParams, subImports] = extractParamTypes(child);
imports = new Set([...imports, ...subImports]);
params = {
...params,
...subParams
};
}
} else if (isDateElement(element) || isTimeElement(element)) {

@@ -550,3 +497,4 @@ params[element.value] = 'Date | number';

imports = new Set([...imports, ...subImports]);
params = { ...params,
params = {
...params,
...subParams

@@ -557,7 +505,7 @@ };

const children = Object.values(element.options).map(o => o.value);
for (const child of children) {
const [subParams, subImports] = extractParamTypes(child);
imports = new Set([...imports, ...subImports]);
params = { ...params,
params = {
...params,
...subParams

@@ -568,9 +516,6 @@ };

}
return [params, imports];
}
function serialiseObjectToType(v) {
let result = '';
for (const [key, value] of Object.entries(v)) {

@@ -583,12 +528,8 @@ if (value && typeof value === 'object') {

}
return `{ ${result} }`;
}
const banner = `// This file is automatically generated by Vocab.\n// To make changes update translation.json files directly.`;
function serialiseTranslationRuntime(value, imports, loadedTranslation) {
trace('Serialising translations:', loadedTranslation);
const translationsType = {};
for (const [key, {

@@ -600,3 +541,2 @@ params,

let translationFunctionString = `() => ${message}`;
if (Object.keys(params).length > 0) {

@@ -607,6 +547,4 @@ const formatGeneric = hasTags ? '<T = string>' : '';

}
translationsType[encodeBackslash(key)] = translationFunctionString;
}
const content = Object.entries(loadedTranslation.languages).map(([languageName, translations]) => `'${encodeWithinSingleQuotes(languageName)}': createLanguage(${JSON.stringify(getTranslationMessages(translations))})`).join(',');

@@ -623,3 +561,2 @@ const languagesUnionAsString = Object.keys(loadedTranslation.languages).map(l => `'${l}'`).join(' | ');

}
async function generateRuntime(loadedTranslation) {

@@ -633,3 +570,2 @@ const {

let imports = new Set();
for (const key of loadedTranslation.keys) {

@@ -639,3 +575,2 @@ let params = {};

let hasTags = false;
for (const translatedLanguage of Object.values(loadedLanguages)) {

@@ -647,3 +582,4 @@ if (translatedLanguage[key]) {

imports = new Set([...imports, ...parsedImports]);
params = { ...params,
params = {
...params,
...parsedParams

@@ -654,3 +590,2 @@ };

}
const returnType = hasTags ? 'NonNullable<ReactNode>' : 'string';

@@ -664,6 +599,6 @@ translationTypes.set(key, {

}
const prettierConfig = await prettier.resolveConfig(filePath);
const serializedTranslationType = serialiseTranslationRuntime(translationTypes, imports, loadedTranslation);
const declaration = prettier.format(serializedTranslationType, { ...prettierConfig,
const declaration = prettier.format(serializedTranslationType, {
...prettierConfig,
parser: 'typescript'

@@ -682,7 +617,5 @@ });

});
const onTranslationChange = async relativePath => {
trace(`Detected change for file ${relativePath}`);
let targetFile;
if (isDevLanguageFile(relativePath)) {

@@ -693,3 +626,2 @@ targetFile = path.resolve(cwd, relativePath);

}
if (targetFile) {

@@ -704,4 +636,4 @@ try {

// eslint-disable-next-line no-console
console.log('Failed to generate types for', relativePath); // eslint-disable-next-line no-console
console.log('Failed to generate types for', relativePath);
// eslint-disable-next-line no-console
console.error(e);

@@ -711,6 +643,4 @@ }

};
const onNewDirectory = async relativePath => {
trace('Detected new directory', relativePath);
if (!isTranslationDirectory(relativePath, config)) {

@@ -720,5 +650,3 @@ trace('Ignoring non-translation directory:', relativePath);

}
const newFilePath = path.join(relativePath, devTranslationFileName);
if (!existsSync(newFilePath)) {

@@ -731,3 +659,2 @@ await promises.writeFile(newFilePath, JSON.stringify({}, null, 2));

};
watcher.on('addDir', onNewDirectory);

@@ -744,7 +671,5 @@ watcher.on('add', onTranslationChange).on('change', onTranslationChange);

}, config);
for (const loadedTranslation of translations) {
await generateRuntime(loadedTranslation);
}
if (shouldWatch) {

@@ -755,6 +680,4 @@ trace('Listening for changes to files...');

}
async function writeIfChanged(filepath, contents) {
let hasChanged = true;
try {

@@ -765,5 +688,5 @@ const existingContents = await promises.readFile(filepath, {

hasChanged = existingContents !== contents;
} catch (e) {// ignore error, likely a file doesn't exist error so we want to write anyway
} catch (e) {
// ignore error, likely a file doesn't exist error so we want to write anyway
}
if (hasChanged) {

@@ -779,20 +702,14 @@ await promises.writeFile(filepath, contents, {

const devLanguage = loadedTranslation.languages[devLanguageName];
if (!devLanguage) {
throw new Error(`Failed to load dev language: ${loadedTranslation.filePath}`);
}
const result = {};
let valid = true;
const requiredKeys = Object.keys(devLanguage);
if (requiredKeys.length > 0) {
for (const altLanguageName of altLanguages) {
var _loadedTranslation$la;
const altLanguage = (_loadedTranslation$la = loadedTranslation.languages[altLanguageName]) !== null && _loadedTranslation$la !== void 0 ? _loadedTranslation$la : {};
for (const key of requiredKeys) {
var _altLanguage$key;
if (typeof ((_altLanguage$key = altLanguage[key]) === null || _altLanguage$key === void 0 ? void 0 : _altLanguage$key.message) !== 'string') {

@@ -802,3 +719,2 @@ if (!result[altLanguageName]) {

}
result[altLanguageName].push(key);

@@ -810,3 +726,2 @@ valid = false;

}
return [valid, result];

@@ -820,10 +735,7 @@ }

let valid = true;
for (const loadedTranslation of allTranslations) {
const [translationValid, result] = findMissingKeys(loadedTranslation, config.devLanguage, getAltLanguages(config));
if (!translationValid) {
valid = false;
console.log(chalk.red`Incomplete translations: "${chalk.bold(loadedTranslation.relativePath)}"`);
for (const lang of Object.keys(result)) {

@@ -835,3 +747,2 @@ const missingKeys = result[lang];

}
return valid;

@@ -846,3 +757,2 @@ }

}
}

@@ -915,10 +825,7 @@

const checkConfigFile = validator.compile(schema);
const splitMap = (message, callback) => message.split(' ,').map(v => callback(v)).join(' ,');
function validateConfig(c) {
trace('Validating configuration file'); // Note: checkConfigFile mutates the config file by applying defaults
trace('Validating configuration file');
// Note: checkConfigFile mutates the config file by applying defaults
const isValid = checkConfigFile(c);
if (isValid !== true) {

@@ -929,21 +836,16 @@ throw new ValidationError('InvalidStructure', (Array.isArray(isValid) ? isValid : []).map(v => {

}
if (v.field) {
var _v$message;
return (_v$message = v.message) === null || _v$message === void 0 ? void 0 : _v$message.replace(v.field, chalk.cyan(v.field));
}
return v.message;
}).join(' \n'));
}
const languageStrings = c.languages.map(v => v.name);
const languageStrings = c.languages.map(v => v.name); // Dev Language should exist in languages
// Dev Language should exist in languages
if (!languageStrings.includes(c.devLanguage)) {
throw new ValidationError('InvalidDevLanguage', `The dev language "${chalk.bold.cyan(c.devLanguage)}" was not found in languages ${languageStrings.join(', ')}.`);
}
const foundLanguages = [];
for (const lang of c.languages) {

@@ -954,5 +856,5 @@ // Languages must only exist once

}
foundLanguages.push(lang.name);
foundLanguages.push(lang.name); // Any extends must be in languages
// Any extends must be in languages
if (lang.extends && !languageStrings.includes(lang.extends)) {

@@ -962,5 +864,3 @@ throw new ValidationError('InvalidExtends', `The language "${chalk.bold.cyan(lang.name)}"'s extends of ${chalk.bold.cyan(lang.extends)} was not found in languages ${languageStrings.join(', ')}.`);

}
const foundGeneratedLanguages = [];
for (const generatedLang of c.generatedLanguages || []) {

@@ -971,10 +871,10 @@ // Generated languages must only exist once

}
foundGeneratedLanguages.push(generatedLang.name);
foundGeneratedLanguages.push(generatedLang.name); // Generated language names must not conflict with language names
// 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
}
// Any extends must be in languages
if (generatedLang.extends && !languageStrings.includes(generatedLang.extends)) {

@@ -984,7 +884,5 @@ 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');
return true;
}
function createConfig(configFilePath) {

@@ -997,6 +895,4 @@ const cwd = path.dirname(configFilePath);

}
async function resolveConfig(customConfigFilePath) {
const configFilePath = customConfigFilePath ? path.resolve(customConfigFilePath) : await findUp('vocab.config.js');
if (configFilePath) {

@@ -1006,3 +902,2 @@ trace(`Resolved configuration file to ${configFilePath}`);

}
trace('No configuration file found');

@@ -1013,3 +908,2 @@ return null;

const configFilePath = customConfigFilePath ? path.resolve(customConfigFilePath) : findUp.sync('vocab.config.js');
if (configFilePath) {

@@ -1019,3 +913,2 @@ trace(`Resolved configuration file to ${configFilePath}`);

}
trace('No configuration file found');

@@ -1022,0 +915,0 @@ return null;

@@ -14,11 +14,8 @@ 'use strict';

const moduleCachedResult = moduleCache.get(m);
if (moduleCachedResult && moduleCachedResult[locale]) {
return moduleCachedResult[locale];
}
const parsedICUMessages = {};
for (const translation of Object.keys(m)) {
const intlMessageFormat = new IntlMessageFormat__default['default'](m[translation], locale);
const intlMessageFormat = new IntlMessageFormat__default["default"](m[translation], locale);
parsedICUMessages[translation] = {

@@ -28,4 +25,4 @@ format: params => intlMessageFormat.format(params)

}
moduleCache.set(m, { ...moduleCachedResult,
moduleCache.set(m, {
...moduleCachedResult,
[locale]: parsedICUMessages

@@ -32,0 +29,0 @@ });

@@ -14,11 +14,8 @@ 'use strict';

const moduleCachedResult = moduleCache.get(m);
if (moduleCachedResult && moduleCachedResult[locale]) {
return moduleCachedResult[locale];
}
const parsedICUMessages = {};
for (const translation of Object.keys(m)) {
const intlMessageFormat = new IntlMessageFormat__default['default'](m[translation], locale);
const intlMessageFormat = new IntlMessageFormat__default["default"](m[translation], locale);
parsedICUMessages[translation] = {

@@ -28,4 +25,4 @@ format: params => intlMessageFormat.format(params)

}
moduleCache.set(m, { ...moduleCachedResult,
moduleCache.set(m, {
...moduleCachedResult,
[locale]: parsedICUMessages

@@ -32,0 +29,0 @@ });

@@ -6,9 +6,6 @@ import IntlMessageFormat from 'intl-messageformat';

const moduleCachedResult = moduleCache.get(m);
if (moduleCachedResult && moduleCachedResult[locale]) {
return moduleCachedResult[locale];
}
const parsedICUMessages = {};
for (const translation of Object.keys(m)) {

@@ -20,4 +17,4 @@ const intlMessageFormat = new IntlMessageFormat(m[translation], locale);

}
moduleCache.set(m, { ...moduleCachedResult,
moduleCache.set(m, {
...moduleCachedResult,
[locale]: parsedICUMessages

@@ -24,0 +21,0 @@ });

{
"name": "@vocab/core",
"version": "1.2.0",
"version": "1.2.1",
"main": "dist/vocab-core.cjs.js",

@@ -57,2 +57,2 @@ "module": "dist/vocab-core.esm.js",

}
}
}

@@ -8,10 +8,7 @@ 'use strict';

const translationModule = translationsByLanguage[language];
if (!translationModule) {
throw new Error(`Attempted to retrieve translations for unknown language "${language}"`);
}
return translationModule;
}
return {

@@ -22,3 +19,2 @@ getLoadedMessages(language, locale) {

},
getMessages(language, locale) {

@@ -28,11 +24,8 @@ const translationModule = getByLanguage(language);

const result = translationModule.getValue(locale || language);
if (!result) {
throw new Error(`Unable to find translations for ${language} after attempting to load. Module may have failed to load or an internal error may have occurred.`);
}
return result;
});
},
load(language) {

@@ -42,3 +35,2 @@ const translationModule = getByLanguage(language);

}
};

@@ -45,0 +37,0 @@ }

@@ -8,10 +8,7 @@ 'use strict';

const translationModule = translationsByLanguage[language];
if (!translationModule) {
throw new Error(`Attempted to retrieve translations for unknown language "${language}"`);
}
return translationModule;
}
return {

@@ -22,3 +19,2 @@ getLoadedMessages(language, locale) {

},
getMessages(language, locale) {

@@ -28,11 +24,8 @@ const translationModule = getByLanguage(language);

const result = translationModule.getValue(locale || language);
if (!result) {
throw new Error(`Unable to find translations for ${language} after attempting to load. Module may have failed to load or an internal error may have occurred.`);
}
return result;
});
},
load(language) {

@@ -42,3 +35,2 @@ const translationModule = getByLanguage(language);

}
};

@@ -45,0 +37,0 @@ }

function createTranslationFile(translationsByLanguage) {
function getByLanguage(language) {
const translationModule = translationsByLanguage[language];
if (!translationModule) {
throw new Error(`Attempted to retrieve translations for unknown language "${language}"`);
}
return translationModule;
}
return {

@@ -17,3 +14,2 @@ getLoadedMessages(language, locale) {

},
getMessages(language, locale) {

@@ -23,11 +19,8 @@ const translationModule = getByLanguage(language);

const result = translationModule.getValue(locale || language);
if (!result) {
throw new Error(`Unable to find translations for ${language} after attempting to load. Module may have failed to load or an internal error may have occurred.`);
}
return result;
});
},
load(language) {

@@ -37,3 +30,2 @@ const translationModule = getByLanguage(language);

}
};

@@ -40,0 +32,0 @@ }

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