Skip to content

Commit

Permalink
feat(config): Generator is open to configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
fdrault committed Aug 5, 2024
1 parent 2e38208 commit aa88fd3
Show file tree
Hide file tree
Showing 10 changed files with 337 additions and 105 deletions.
13 changes: 12 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,16 @@
"files": [
"dist/",
"bin/"
]
],
"prettier": {
"arrowParens": "always",
"bracketSpacing": true,
"bracketSameLine": true,
"printWidth": 100,
"semi": true,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5",
"useTabs": false
}
}
17 changes: 17 additions & 0 deletions src/command/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,23 @@ export function init() {
output: {
path: "./i18n/translations.d.ts",
},
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"] },
},
},
],
};
console.log(`Initialize config file ${configurationFilename}\n`);
writeDefaultConfiguration(defaultConfiguration, configurationFilename);
Expand Down
6 changes: 6 additions & 0 deletions src/config/config-loader.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import * as fs from "fs";
import { KeyEndsWithRule } from "../rules/key";
import { TranslationMatchRule } from "../rules/translation";

export function readConfigFile(configPath: string): Configuration {
try {
Expand All @@ -15,6 +17,9 @@ Run "npm run i18n-typegen init" to generate one\n'

type InputFormat = "flatten" | "nested";

export type Rule = (TranslationMatchRule | KeyEndsWithRule) & {
"//"?: string;
};
export interface Configuration {
input: {
format: InputFormat;
Expand All @@ -23,4 +28,5 @@ export interface Configuration {
output: {
path: string;
};
rules: Rule[];
}
37 changes: 37 additions & 0 deletions src/rules/key.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Rule } from "../config/config-loader";
import { TranslationEntry } from "../translation/generate-template-data";
import {
AddPlaceholderTransformer,
RemoveKeyPartTransformer,
applyAddPlaceholderTransformer,
applyRemoveLastPartTransformer,
} from "./transformer";

type KeyEndsWithCondition = { keyEndsWith: string[] };

export type KeyEndsWithRule = {
condition: KeyEndsWithCondition;
transformer: AddPlaceholderTransformer | RemoveKeyPartTransformer;
};

export function isKeyRule(rule: Rule): rule is KeyEndsWithRule {
return "keyEndsWith" in rule.condition;
}

export function applyKeyEndsWithRule(
rule: KeyEndsWithRule,
entry: TranslationEntry
): TranslationEntry {
const { key } = entry;
let result = entry;
const match = rule.condition.keyEndsWith.some((endingPart) => key.endsWith(`.${endingPart}`));
if (match) {
if ("removeLastPart" in rule.transformer) {
result = applyRemoveLastPartTransformer(rule.transformer, result);
}
if ("addPlaceholder" in rule.transformer) {
result = applyAddPlaceholderTransformer(rule.transformer, result);
}
}
return result;
}
53 changes: 53 additions & 0 deletions src/rules/transformer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { TranslationEntry } from "../translation/generate-template-data";

export type AddPlaceholderTransformer = {
addPlaceholder: {
name: string;
type: string[];
};
};
export type RemoveKeyPartTransformer = { removeLastPart: true };
export type AddMatchedPlaceholderTransformer = {
addMatchedPlaceholder: {
type: string[];
};
};

export function applyAddPlaceholderTransformer(
transformer: AddPlaceholderTransformer,
entry: TranslationEntry
): TranslationEntry {
const previousType =
entry.interpolations.get(transformer.addPlaceholder.name) ?? [];
entry.interpolations.set(
transformer.addPlaceholder.name,
previousType.concat(transformer.addPlaceholder.type)
);
return entry;
}

export function applyAddMatchedPlaceholderTransformer(
transformer: AddMatchedPlaceholderTransformer,
placeholder: string,
entry: TranslationEntry
): TranslationEntry {
const previousType = entry.interpolations.get(placeholder) ?? [];
entry.interpolations.set(
placeholder,
previousType.concat(transformer.addMatchedPlaceholder.type)
);
return entry;
}

export function applyRemoveLastPartTransformer(
transformer: RemoveKeyPartTransformer,
entry: TranslationEntry
): TranslationEntry {
if (transformer.removeLastPart) {
return { ...entry, key: removeLastPart(entry.key) };
}
return entry;
}

const removeLastPart = (key: string, delimiter = ".") =>
key.split(delimiter).slice(0, -1).join(delimiter);
54 changes: 54 additions & 0 deletions src/rules/translation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Rule } from "../config/config-loader";
import { TranslationEntry } from "../translation/generate-template-data";
import {
AddMatchedPlaceholderTransformer,
AddPlaceholderTransformer,
applyAddMatchedPlaceholderTransformer,
applyAddPlaceholderTransformer,
} from "./transformer";

type TranslationMatchCondition = {
placeholderPattern: {
prefix: string;
suffix: string;
};
};

export type TranslationMatchRule = {
condition: TranslationMatchCondition;
transformer: AddMatchedPlaceholderTransformer | AddPlaceholderTransformer;
};

export function isTranslationRule(rule: Rule): rule is TranslationMatchRule {
return "placeholderPattern" in rule.condition;
}

export function applyTranslationMatchRule(
rule: TranslationMatchRule,
entry: TranslationEntry
): TranslationEntry {
let result = entry;
const { prefix, suffix } = rule.condition.placeholderPattern;
const regexp = new RegExp(`${prefix}(.*?)${suffix}`, "g");

const matches: string[] = [];
let match;
while ((match = regexp.exec(entry.translation)) !== null) {
matches.push(match[1]);
}

if ("addMatchedPlaceholder" in rule.transformer) {
const transformer = rule.transformer;
matches.forEach((placeholder) => {
result = applyAddMatchedPlaceholderTransformer(
transformer,
placeholder,
result
);
});
}
if ("addPlaceholder" in rule.transformer) {
result = applyAddPlaceholderTransformer(rule.transformer, result);
}
return result;
}
2 changes: 1 addition & 1 deletion src/templates/generate-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export function generateType(configuration: Configuration) {
const template = openTemplate();

const wordings = loadWordings(configuration);
const templateData = generateTemplateData(wordings);
const templateData = generateTemplateData(wordings, configuration);

const generatedType = Mustache.render(template, {
keys: templateData,
Expand Down
9 changes: 7 additions & 2 deletions src/templates/template-type.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
export interface InterpolationTemplateData {
name: string;
type: "string" | "number";
type: InterpolationTypeTemplateData[];
last?: boolean;
}

export interface WordingEntryTemplateData {
export interface InterpolationTypeTemplateData {
value: string;
last?: boolean;
}

export interface TranslationEntryTemplateData {
key: string;
interpolations: InterpolationTemplateData[];
}
60 changes: 49 additions & 11 deletions src/translation/generate-template-data.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { generateTemplateData } from "./generate-template-data";

const mockConfig = { input: {} as any, output: {} as any, rules: [] };
describe("generateType", () => {
it("should handle pluralization correctly", () => {
const translations = {
Expand All @@ -8,13 +9,24 @@ describe("generateType", () => {
"day.zero": "0 day",
};

const result = generateTemplateData(translations, { detectPlurial: true });
const result = generateTemplateData(translations, {
...mockConfig,
rules: [
{
condition: { keyEndsWith: ["one", "other", "zero"] },
transformer: {
addPlaceholder: { name: "count", type: ["number"] },
removeLastPart: true,
},
},
],
});

expect(result).toHaveLength(1);

expect(result[0]).toEqual({
key: "day",
interpolations: [{ name: "count", type: "number", last: true }],
interpolations: [{ name: "count", type: [{ value: "number", last: true }], last: true }],
});
});

Expand All @@ -25,7 +37,7 @@ describe("generateType", () => {
"day.zero": "0 day",
};

const result = generateTemplateData(translations, { detectPlurial: false });
const result = generateTemplateData(translations, mockConfig);

expect(result).toHaveLength(3);

Expand All @@ -35,7 +47,7 @@ describe("generateType", () => {
});
expect(result[1]).toEqual({
key: "day.other",
interpolations: [{ name: "count", type: "string", last: true }],
interpolations: [],
});
});

Expand All @@ -44,15 +56,30 @@ describe("generateType", () => {
greeting: "Hello {{firstName}} {{familyName}}",
};

const result = generateTemplateData(translations);
const result = generateTemplateData(translations, {
...mockConfig,
rules: [
{
condition: { placeholderPattern: { prefix: "{{", suffix: "}}" } },
transformer: { addMatchedPlaceholder: { type: ["string", "number"] } },
},
],
});

expect(result).toHaveLength(1);

expect(result[0]).toEqual({
key: "greeting",
interpolations: [
{ name: "firstName", type: "string" },
{ name: "familyName", type: "string", last: true },
{
name: "firstName",
type: [{ value: "string" }, { value: "number", last: true }],
},
{
name: "familyName",
type: [{ value: "string" }, { value: "number", last: true }],
last: true,
},
],
});
});
Expand All @@ -64,16 +91,27 @@ describe("generateType", () => {
"day.zero": "0 {{mood}} day",
};

const result = 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 = generateTemplateData(translations, { ...mockConfig, rules });

expect(result).toHaveLength(1);

expect(result[0]).toEqual({
key: "day",
interpolations: [
{ name: "mood", type: "string" },
{ name: "count", type: "number" },
{ name: "moods", type: "string", last: true },
{ name: "count", type: [{ value: "number" }, { value: "string", last: true }] },
{ name: "mood", type: [{ value: "string", last: true }] },
{ name: "moods", type: [{ value: "string", last: true }], last: true },
],
});
});
Expand Down
Loading

0 comments on commit aa88fd3

Please sign in to comment.