@betomorrow/i18n-typegen
Advanced tools
Comparing version 1.1.0 to 2.0.0
@@ -38,2 +38,23 @@ "use strict"; | ||
}, | ||
rules: [ | ||
{ | ||
"//": "Add pluralization placeholders", | ||
condition: { keyEndsWith: ["zero", "one", "other"] }, | ||
transformer: { | ||
addPlaceholder: { name: "count", type: ["number"] }, | ||
removeLastPart: true, | ||
}, | ||
}, | ||
{ | ||
"//": "Add interpolation values for matched placeholders", | ||
condition: { placeholderPattern: { prefix: "{{", suffix: "}}" } }, | ||
transformer: { | ||
addMatchedPlaceholder: { type: ["string", "number"] }, | ||
}, | ||
}, | ||
], | ||
extra: { | ||
prettierIgnore: true, | ||
eslintDisablePrettier: false, | ||
}, | ||
}; | ||
@@ -40,0 +61,0 @@ console.log(`Initialize config file ${configurationFilename}\n`); |
@@ -62,6 +62,7 @@ "use strict"; | ||
const wordings = (0, wording_loader_1.loadWordings)(configuration); | ||
const templateData = (0, generate_template_data_1.generateTemplateData)(wordings); | ||
const generatedType = mustache_1.default.render(template, Object.assign({ keys: templateData }, utilityChar)); | ||
const templateData = (0, generate_template_data_1.generateTemplateData)(wordings, configuration); | ||
console.log(JSON.stringify(template, null, 2)); | ||
const generatedType = mustache_1.default.render(template, Object.assign(Object.assign({ keys: templateData }, utilityChar), configuration.extra)); | ||
writeFile(generatedType, configuration.output.path); | ||
} | ||
exports.generateType = generateType; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.generateTemplateData = void 0; | ||
const find_interpolation_1 = require("./find-interpolation"); | ||
const is_enumerable_1 = require("./is-enumerable"); | ||
const defaultConfiguration = { | ||
detectPlurial: true, | ||
detectInterpolation: true, | ||
}; | ||
function generateTemplateData(translations, overwriteConfiguration = {}) { | ||
const config = Object.assign(Object.assign({}, defaultConfiguration), overwriteConfiguration); | ||
const entries = new Map(); | ||
const key_1 = require("../rules/key"); | ||
const translation_1 = require("../rules/translation"); | ||
function createEntry(item) { | ||
return { | ||
key: item.key, | ||
translation: item.translation, | ||
interpolations: new Map(), | ||
}; | ||
} | ||
function generateTemplateData(translations, configuration) { | ||
const entries = []; | ||
Object.entries(translations).forEach(([key, translation]) => { | ||
const entry = processTranslation(key, translation, entries, config); | ||
entries.set(entry.key, entry); | ||
const entry = processTranslation(createEntry({ key, translation }), configuration); | ||
entries.push(entry); | ||
}); | ||
return mapToTemplateData(entries); | ||
return toTemplateData(entries); | ||
} | ||
exports.generateTemplateData = generateTemplateData; | ||
function mapToTemplateData(entries) { | ||
return Array.from(entries.values()).map((it) => ({ | ||
key: it.key, | ||
interpolations: interpolationsMapToTemplate(it.interpolations), | ||
})); | ||
} | ||
function interpolationsMapToTemplate(interpolations) { | ||
const interpolationArray = Array.from(interpolations.entries()).map(([name, type]) => ({ name, type })); | ||
if (interpolationArray.length > 0) | ||
interpolationArray[interpolationArray.length - 1].last = true; | ||
return interpolationArray; | ||
} | ||
function processTranslation(key, translation, entries, configuration) { | ||
const { detectPlurial, detectInterpolation } = configuration; | ||
const interpolationsNames = (0, find_interpolation_1.findInterpolations)(translation); | ||
const interpolations = detectInterpolation | ||
? new Map(interpolationsNames.map((name) => [name, "string"])) | ||
: new Map(); | ||
if (detectPlurial && (0, is_enumerable_1.isEnumerable)(key)) { | ||
const shrunkKey = removeLastPart(key); | ||
interpolations.set("count", "number"); | ||
if (entries.has(shrunkKey)) { | ||
// If the entry already exists, merge interpolations | ||
const existingEntry = entries.get(shrunkKey); | ||
return { | ||
key: shrunkKey, | ||
interpolations: mergeInterpolations(existingEntry.interpolations, interpolations), | ||
}; | ||
function toTemplateData(entries) { | ||
var _a, _b; | ||
const entryMap = new Map(); | ||
for (const entry of entries) { | ||
// Retrieve or initialize the interpolations for the current entry key | ||
let interpolations = (_b = (_a = entryMap.get(entry.key)) === null || _a === void 0 ? void 0 : _a.interpolations) !== null && _b !== void 0 ? _b : []; | ||
// Add new interpolations from the entry | ||
for (const [name, types] of entry.interpolations) { | ||
const existingInterpolation = interpolations.find((interpol) => interpol.name === name); | ||
if (existingInterpolation) { | ||
// Merge types if the interpolation already exists | ||
existingInterpolation.type = mergeUniqueTypes(existingInterpolation.type, types); | ||
} | ||
else { | ||
// Add new interpolation | ||
interpolations.push({ | ||
name, | ||
type: types.map((t) => ({ value: t })), | ||
}); | ||
} | ||
} | ||
else { | ||
return { key: shrunkKey, interpolations }; | ||
// Update the entry map | ||
entryMap.set(entry.key, { key: entry.key, interpolations }); | ||
} | ||
// Set the `last` property | ||
for (const entry of entryMap.values()) { | ||
const lastInterpolationIndex = entry.interpolations.length - 1; | ||
if (lastInterpolationIndex >= 0) { | ||
entry.interpolations[lastInterpolationIndex].last = true; | ||
} | ||
for (const interpolation of entry.interpolations) { | ||
const lastTypeIndex = interpolation.type.length - 1; | ||
if (lastTypeIndex >= 0) { | ||
interpolation.type[lastTypeIndex].last = true; | ||
} | ||
} | ||
} | ||
return { key, interpolations }; | ||
return Array.from(entryMap.values()); | ||
} | ||
const removeLastPart = (key, delimiter = ".") => key.split(delimiter).slice(0, -1).join(delimiter); | ||
function mergeInterpolations(existingInterpolations, newInterpolations) { | ||
const mergedInterpolations = new Map([...existingInterpolations]); | ||
newInterpolations.forEach((type, name) => { | ||
if (!mergedInterpolations.has(name)) { | ||
mergedInterpolations.set(name, type); | ||
function mergeUniqueTypes(existingTypes, newTypes) { | ||
const existingTypeValues = new Set(existingTypes.map((t) => t.value)); | ||
for (const newType of newTypes) { | ||
if (!existingTypeValues.has(newType)) { | ||
existingTypes.push({ value: newType }); | ||
} | ||
} | ||
return existingTypes; | ||
} | ||
function processTranslation(entry, configuration) { | ||
let result = entry; | ||
// Apply key rules first | ||
const sortedRules = configuration.rules.sort((a, b) => { | ||
if ((0, key_1.isKeyRule)(a) && !(0, key_1.isKeyRule)(b)) { | ||
return -1; | ||
} | ||
if ((0, key_1.isKeyRule)(b) && !(0, key_1.isKeyRule)(a)) { | ||
return 1; | ||
} | ||
return 0; | ||
}); | ||
return mergedInterpolations; | ||
for (const rule of sortedRules) { | ||
if ((0, key_1.isKeyRule)(rule)) { | ||
result = (0, key_1.applyKeyEndsWithRule)(rule, result); | ||
} | ||
if ((0, translation_1.isTranslationRule)(rule)) { | ||
result = (0, translation_1.applyTranslationMatchRule)(rule, result); | ||
} | ||
} | ||
return result; | ||
} |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const generate_template_data_1 = require("./generate-template-data"); | ||
const mockConfig = { input: {}, output: {}, rules: [] }; | ||
describe("generateType", () => { | ||
@@ -11,7 +12,15 @@ it("should handle pluralization correctly", () => { | ||
}; | ||
const result = (0, generate_template_data_1.generateTemplateData)(translations, { detectPlurial: true }); | ||
const result = (0, generate_template_data_1.generateTemplateData)(translations, Object.assign(Object.assign({}, mockConfig), { rules: [ | ||
{ | ||
condition: { keyEndsWith: ["one", "other", "zero"] }, | ||
transformer: { | ||
addPlaceholder: { name: "count", type: ["number"] }, | ||
removeLastPart: true, | ||
}, | ||
}, | ||
] })); | ||
expect(result).toHaveLength(1); | ||
expect(result[0]).toEqual({ | ||
expect(result[0]).toMatchObject({ | ||
key: "day", | ||
interpolations: [{ name: "count", type: "number", last: true }], | ||
interpolations: [{ name: "count", type: [{ value: "number" }] }], | ||
}); | ||
@@ -25,11 +34,11 @@ }); | ||
}; | ||
const result = (0, generate_template_data_1.generateTemplateData)(translations, { detectPlurial: false }); | ||
const result = (0, generate_template_data_1.generateTemplateData)(translations, mockConfig); | ||
expect(result).toHaveLength(3); | ||
expect(result[0]).toEqual({ | ||
expect(result[0]).toMatchObject({ | ||
key: "day.one", | ||
interpolations: [], | ||
}); | ||
expect(result[1]).toEqual({ | ||
expect(result[1]).toMatchObject({ | ||
key: "day.other", | ||
interpolations: [{ name: "count", type: "string", last: true }], | ||
interpolations: [], | ||
}); | ||
@@ -41,9 +50,20 @@ }); | ||
}; | ||
const result = (0, generate_template_data_1.generateTemplateData)(translations); | ||
const result = (0, generate_template_data_1.generateTemplateData)(translations, Object.assign(Object.assign({}, mockConfig), { rules: [ | ||
{ | ||
condition: { placeholderPattern: { prefix: "{{", suffix: "}}" } }, | ||
transformer: { addMatchedPlaceholder: { type: ["string", "number"] } }, | ||
}, | ||
] })); | ||
expect(result).toHaveLength(1); | ||
expect(result[0]).toEqual({ | ||
expect(result[0]).toMatchObject({ | ||
key: "greeting", | ||
interpolations: [ | ||
{ name: "firstName", type: "string" }, | ||
{ name: "familyName", type: "string", last: true }, | ||
{ | ||
name: "firstName", | ||
type: [{ value: "string" }, { value: "number" }], | ||
}, | ||
{ | ||
name: "familyName", | ||
type: [{ value: "string" }, { value: "number" }], | ||
}, | ||
], | ||
@@ -58,13 +78,49 @@ }); | ||
}; | ||
const result = (0, generate_template_data_1.generateTemplateData)(translations); | ||
const rules = [ | ||
{ | ||
condition: { placeholderPattern: { prefix: "{{", suffix: "}}" } }, | ||
transformer: { addMatchedPlaceholder: { type: ["string"] } }, | ||
}, | ||
{ | ||
condition: { keyEndsWith: ["one", "other", "zero"] }, | ||
transformer: { addPlaceholder: { name: "count", type: ["number"] }, removeLastPart: true }, | ||
}, | ||
]; | ||
const result = (0, generate_template_data_1.generateTemplateData)(translations, Object.assign(Object.assign({}, mockConfig), { rules })); | ||
expect(result).toHaveLength(1); | ||
expect(result[0]).toEqual({ | ||
expect(result[0]).toMatchObject({ | ||
key: "day", | ||
interpolations: [ | ||
{ name: "mood", type: "string" }, | ||
{ name: "count", type: "number" }, | ||
{ name: "moods", type: "string", last: true }, | ||
{ name: "count", type: [{ value: "number" }, { value: "string" }] }, | ||
{ name: "mood", type: [{ value: "string" }] }, | ||
{ name: "moods", type: [{ value: "string" }] }, | ||
], | ||
}); | ||
}); | ||
it("should handle ", () => { | ||
const translations = { | ||
"greeting.man": "Hello Mr {{name}}!", | ||
"greeting.woman": "Hello Ms {{name}}!", | ||
"greeting.neutral": "Hello {{name}}!", | ||
}; | ||
const rules = [ | ||
{ | ||
condition: { placeholderPattern: { prefix: "{{", suffix: "}}" } }, | ||
transformer: { addMatchedPlaceholder: { type: ["string"] } }, | ||
}, | ||
{ | ||
condition: { keyEndsWith: ["man", "woman", "neutral"] }, | ||
transformer: { addPlaceholder: { name: "gender", type: ["string"] }, removeLastPart: true }, | ||
}, | ||
]; | ||
const result = (0, generate_template_data_1.generateTemplateData)(translations, Object.assign(Object.assign({}, mockConfig), { rules })); | ||
expect(result).toHaveLength(1); | ||
expect(result[0]).toMatchObject({ | ||
key: "greeting", | ||
interpolations: [ | ||
{ name: "gender", type: [{ value: "string" }] }, | ||
{ name: "name", type: [{ value: "string" }] }, | ||
], | ||
}); | ||
}); | ||
}); |
{ | ||
"name": "@betomorrow/i18n-typegen", | ||
"version": "1.1.0", | ||
"version": "2.0.0", | ||
"description": "Generate TS type for your translations keys and interpolation values", | ||
@@ -10,2 +10,3 @@ "main": "dist/index.js", | ||
"test": "jest", | ||
"snapshot": "node test/test.js", | ||
"build": "tsc -p . && cp ./src/templates/translations.mustache ./dist/templates/translations.mustache" | ||
@@ -33,2 +34,3 @@ }, | ||
"devDependencies": { | ||
"@release-it/conventional-changelog": "^8.0.1", | ||
"@types/fs-extra": "^11.0.4", | ||
@@ -38,3 +40,5 @@ "@types/jest": "^29.5.11", | ||
"@types/node": "^20.10.4", | ||
"diff": "^5.2.0", | ||
"jest": "^29.7.0", | ||
"release-it": "^17.6.0", | ||
"ts-jest": "^29.1.1", | ||
@@ -51,3 +55,51 @@ "typescript": "^5.3.3" | ||
"bin/" | ||
] | ||
], | ||
"prettier": { | ||
"arrowParens": "always", | ||
"bracketSpacing": true, | ||
"bracketSameLine": true, | ||
"printWidth": 100, | ||
"semi": true, | ||
"singleQuote": false, | ||
"tabWidth": 2, | ||
"trailingComma": "es5", | ||
"useTabs": false | ||
}, | ||
"release-it": { | ||
"git": { | ||
"commitMessage": "chore: release ${version}", | ||
"tagName": "v${version}" | ||
}, | ||
"npm": { | ||
"publish": true | ||
}, | ||
"github": { | ||
"release": true | ||
}, | ||
"plugins": { | ||
"@release-it/conventional-changelog": { | ||
"preset": { | ||
"name": "conventionalcommits", | ||
"types": [ | ||
{ | ||
"type": "feat", | ||
"section": "✨ Features" | ||
}, | ||
{ | ||
"type": "fix", | ||
"section": "🐛 Bug Fixes" | ||
}, | ||
{ | ||
"type": "chore(deps)", | ||
"section": "🛠️ Dependency Upgrades" | ||
}, | ||
{ | ||
"type": "docs", | ||
"section": "📚 Documentation" | ||
} | ||
] | ||
} | ||
} | ||
} | ||
} | ||
} |
@@ -73,7 +73,28 @@ # i18n-typegen | ||
"input": { | ||
"format": "flatten", | ||
"path": "./i18n/en.json" | ||
"format": "nested", | ||
"path": "./input/default.json" | ||
}, | ||
"output": { | ||
"path": "./i18n/translations.d.ts" | ||
"path": "./output/default.d.ts" | ||
}, | ||
"rules": [ | ||
{ | ||
"//": "Add pluralization placeholders", | ||
"condition": { "keyEndsWith": ["zero", "one", "other"] }, | ||
"transformer": { | ||
"addPlaceholder": { "name": "count", "type": ["number", "string"] }, | ||
"removeLastPart": true | ||
} | ||
}, | ||
{ | ||
"//": "Add interpolation values for matched placeholders", | ||
"condition": { "placeholderPattern": { "prefix": "{{", "suffix": "}}" } }, | ||
"transformer": { | ||
"addMatchedPlaceholder": { "type": ["string", "number"] } | ||
} | ||
} | ||
], | ||
"extra": { | ||
"prettierIgnore": true, | ||
"eslintDisablePrettier": false | ||
} | ||
@@ -86,2 +107,5 @@ } | ||
- nested scoped dictionnaries: `{ home: { header: { greeting: "hello" } } }` | ||
- extra de-opt generated files from prettier or eslint prettier rule to comply with specific configurations. | ||
- `prettierIgnore` add `// prettier-ignore` in generated file. | ||
- `eslintDisablePrettier` add `/* eslint-disable prettier/prettier */` in generated file. | ||
@@ -108,6 +132,3 @@ ### Recommended Toolbox | ||
*/ | ||
unsafeTranslate: ( | ||
key: string, | ||
interpolations?: Record<string, unknown> | ||
) => string; | ||
unsafeTranslate: (key: string, interpolations?: Record<string, unknown>) => string; | ||
}; | ||
@@ -114,0 +135,0 @@ |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
36276
22
683
159
10