Skip to content

Commit b6c22ad

Browse files
committed
fix(translate): add fallback for empty translatedTitle in toggle _category_.json and add tests
When translatedTitle is empty or whitespace-only, fall back to the English source title (or "Untitled") so _category_.json label/customProps.title are never blank. Adds unit tests for the fallback and a locale-output regression test that asserts all translated _category_.json files have non-empty labels. Closes #158, closes #159
1 parent 737d928 commit b6c22ad

File tree

3 files changed

+172
-2
lines changed

3 files changed

+172
-2
lines changed

scripts/notion-translate/index.test.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1392,6 +1392,110 @@ describe("notion-translate index", () => {
13921392
});
13931393
});
13941394

1395+
describe("toggle _category_.json fallback behavior", () => {
1396+
const mockConfig = {
1397+
language: "pt-BR",
1398+
notionLangCode: "Portuguese",
1399+
outputDir: "/test/output",
1400+
};
1401+
1402+
it("falls back to English title when translatedTitle is empty string", async () => {
1403+
const { saveTranslatedContentToDisk } = await import("./index");
1404+
1405+
const togglePage = createMockNotionPage({
1406+
id: "toggle-abc123",
1407+
title: "English Toggle Title",
1408+
elementType: "Toggle",
1409+
});
1410+
1411+
await saveTranslatedContentToDisk(togglePage, "", "", mockConfig);
1412+
1413+
const writeCall = mockWriteFile.mock.calls.find((call: string[]) =>
1414+
call[0].endsWith("_category_.json")
1415+
);
1416+
expect(writeCall).toBeDefined();
1417+
const written = JSON.parse(writeCall[1] as string);
1418+
expect(written.label).toBe("English Toggle Title");
1419+
expect(written.customProps.title).toBe("English Toggle Title");
1420+
});
1421+
1422+
it("falls back to English title when translatedTitle is whitespace-only", async () => {
1423+
const { saveTranslatedContentToDisk } = await import("./index");
1424+
1425+
const togglePage = createMockNotionPage({
1426+
id: "toggle-def456",
1427+
title: "English Toggle Title",
1428+
elementType: "Toggle",
1429+
});
1430+
1431+
await saveTranslatedContentToDisk(togglePage, "", " ", mockConfig);
1432+
1433+
const writeCall = mockWriteFile.mock.calls.find((call: string[]) =>
1434+
call[0].endsWith("_category_.json")
1435+
);
1436+
expect(writeCall).toBeDefined();
1437+
const written = JSON.parse(writeCall[1] as string);
1438+
expect(written.label).toBe("English Toggle Title");
1439+
expect(written.customProps.title).toBe("English Toggle Title");
1440+
});
1441+
1442+
it("uses translatedTitle when it is non-empty", async () => {
1443+
const { saveTranslatedContentToDisk } = await import("./index");
1444+
1445+
const togglePage = createMockNotionPage({
1446+
id: "toggle-ghi789",
1447+
title: "English Toggle Title",
1448+
elementType: "Toggle",
1449+
});
1450+
1451+
await saveTranslatedContentToDisk(
1452+
togglePage,
1453+
"",
1454+
"Título Traduzido",
1455+
mockConfig
1456+
);
1457+
1458+
const writeCall = mockWriteFile.mock.calls.find((call: string[]) =>
1459+
call[0].endsWith("_category_.json")
1460+
);
1461+
expect(writeCall).toBeDefined();
1462+
const written = JSON.parse(writeCall[1] as string);
1463+
expect(written.label).toBe("Título Traduzido");
1464+
expect(written.customProps.title).toBe("Título Traduzido");
1465+
});
1466+
});
1467+
1468+
describe("non-toggle page output regression", () => {
1469+
it("writes .mdx file and does not produce _category_.json for page-type content", async () => {
1470+
const { saveTranslatedContentToDisk } = await import("./index");
1471+
1472+
const regularPage = createMockNotionPage({
1473+
id: "page-regular123",
1474+
title: "Regular Page",
1475+
elementType: "Page",
1476+
});
1477+
1478+
const filePath = await saveTranslatedContentToDisk(
1479+
regularPage,
1480+
"# Translated Content",
1481+
"Translated Title",
1482+
{
1483+
language: "pt-BR",
1484+
notionLangCode: "Portuguese",
1485+
outputDir: "/test/output",
1486+
}
1487+
);
1488+
1489+
expect(filePath).toMatch(/\.md$/);
1490+
expect(filePath).not.toContain("_category_.json");
1491+
1492+
const categoryCall = mockWriteFile.mock.calls.find((call: string[]) =>
1493+
call[0].endsWith("_category_.json")
1494+
);
1495+
expect(categoryCall).toBeUndefined();
1496+
});
1497+
});
1498+
13951499
describe("missing parent relation handling", () => {
13961500
it("gracefully skips pages without Parent item relation and reports as non-critical failure", async () => {
13971501
// Create a page WITHOUT parent relation

scripts/notion-translate/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -681,8 +681,9 @@ export async function saveTranslatedContentToDisk(
681681
await fs.mkdir(sectionPath, { recursive: true });
682682

683683
// Create _category_.json file
684+
const effectiveTitle = translatedTitle?.trim() || title || "Untitled";
684685
const categoryContent = {
685-
label: translatedTitle,
686+
label: effectiveTitle,
686687
position:
687688
(
688689
englishPage.properties[NOTION_PROPERTIES.ORDER] as
@@ -695,7 +696,7 @@ export async function saveTranslatedContentToDisk(
695696
type: "generated-index",
696697
},
697698
customProps: {
698-
title: translatedTitle,
699+
title: effectiveTitle,
699700
},
700701
};
701702

scripts/verify-locale-output.test.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,71 @@ describe("Locale Output Verification", () => {
279279
).toBeLessThanOrEqual(maxAllowedDiff);
280280
});
281281

282+
it("has non-empty label in translated toggle _category_.json files", async () => {
283+
const locales = ["es", "pt"];
284+
for (const locale of locales) {
285+
const localeDocsDir = path.join(
286+
i18nDir,
287+
locale,
288+
"docusaurus-plugin-content-docs",
289+
"current"
290+
);
291+
let categoryFiles: string[];
292+
try {
293+
// Recursively find all _category_.json files under the locale docs dir
294+
const findCategoryFiles = async (dir: string): Promise<string[]> => {
295+
const results: string[] = [];
296+
let entries;
297+
try {
298+
entries = await fs.readdir(dir, { withFileTypes: true });
299+
} catch {
300+
return results;
301+
}
302+
for (const entry of entries) {
303+
const fullPath = path.join(dir, entry.name);
304+
if (entry.isDirectory()) {
305+
results.push(...(await findCategoryFiles(fullPath)));
306+
} else if (entry.name === "_category_.json") {
307+
results.push(fullPath);
308+
}
309+
}
310+
return results;
311+
};
312+
categoryFiles = await findCategoryFiles(localeDocsDir);
313+
} catch (error) {
314+
if (
315+
error instanceof Error &&
316+
"code" in error &&
317+
(error as NodeJS.ErrnoException).code === "ENOENT"
318+
) {
319+
console.log(
320+
`${locale} locale docs directory not found - content branch may not have toggle pages`
321+
);
322+
continue;
323+
}
324+
throw error;
325+
}
326+
327+
if (categoryFiles.length === 0) {
328+
console.log(
329+
`No _category_.json files found for locale ${locale} - may not have toggle pages`
330+
);
331+
continue;
332+
}
333+
334+
for (const filePath of categoryFiles) {
335+
const content = await fs.readFile(filePath, "utf8");
336+
const category = JSON.parse(content);
337+
expect(
338+
category.label,
339+
`_category_.json at ${filePath} has empty label`
340+
).toBeTruthy();
341+
expect(typeof category.label).toBe("string");
342+
expect(category.label.trim().length).toBeGreaterThan(0);
343+
}
344+
}
345+
});
346+
282347
it("does not have English locale directory (en/)", async () => {
283348
const enDir = path.join(i18nDir, "en");
284349

0 commit comments

Comments
 (0)