Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,27 @@ MAX_IMAGE_RETRIES=3
# TEST_MODE=true

# OpenAI API Configuration
# Required for translation features (notion-translate scripts)
OPENAI_API_KEY=your_openai_api_key_here

# OpenAI model to use for translations
# Default: gpt-5-mini
# Valid values: any OpenAI-compatible model name
OPENAI_MODEL=gpt-5-mini

# Optional: Use alternative OpenAI-compatible APIs (like Deepseek)
# OPENAI_BASE_URL=https://api.deepseek.com
# OPENAI_MODEL=deepseek-chat

# API Server Configuration
# Host and port for the Notion Jobs API server
# Default: API_HOST=localhost, API_PORT=3001
API_HOST=localhost
API_PORT=3001

# API Key Authentication
# Set one or more API_KEY_* variables to enable authentication on protected endpoints.
# When no API_KEY_* variables are set, the API server is fully open (no auth required).
# Format: API_KEY_<name>=<value> (minimum 16 characters)
# Example: API_KEY_GITHUB_ACTIONS=your-secret-key-min-16-chars
# API_KEY_GITHUB_ACTIONS=your-secret-key-min-16-chars
104 changes: 104 additions & 0 deletions scripts/notion-translate/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1392,6 +1392,110 @@ describe("notion-translate index", () => {
});
});

describe("toggle _category_.json fallback behavior", () => {
const mockConfig = {
language: "pt-BR",
notionLangCode: "Portuguese",
outputDir: "/test/output",
};

it("falls back to English title when translatedTitle is empty string", async () => {
const { saveTranslatedContentToDisk } = await import("./index");

const togglePage = createMockNotionPage({
id: "toggle-abc123",
title: "English Toggle Title",
elementType: "Toggle",
});

await saveTranslatedContentToDisk(togglePage, "", "", mockConfig);

const writeCall = mockWriteFile.mock.calls.find((call: string[]) =>
call[0].endsWith("_category_.json")
);
expect(writeCall).toBeDefined();
const written = JSON.parse(writeCall[1] as string);
expect(written.label).toBe("English Toggle Title");
expect(written.customProps.title).toBe("English Toggle Title");
});

it("falls back to English title when translatedTitle is whitespace-only", async () => {
const { saveTranslatedContentToDisk } = await import("./index");

const togglePage = createMockNotionPage({
id: "toggle-def456",
title: "English Toggle Title",
elementType: "Toggle",
});

await saveTranslatedContentToDisk(togglePage, "", " ", mockConfig);

const writeCall = mockWriteFile.mock.calls.find((call: string[]) =>
call[0].endsWith("_category_.json")
);
expect(writeCall).toBeDefined();
const written = JSON.parse(writeCall[1] as string);
expect(written.label).toBe("English Toggle Title");
expect(written.customProps.title).toBe("English Toggle Title");
});

it("uses translatedTitle when it is non-empty", async () => {
const { saveTranslatedContentToDisk } = await import("./index");

const togglePage = createMockNotionPage({
id: "toggle-ghi789",
title: "English Toggle Title",
elementType: "Toggle",
});

await saveTranslatedContentToDisk(
togglePage,
"",
"Título Traduzido",
mockConfig
);

const writeCall = mockWriteFile.mock.calls.find((call: string[]) =>
call[0].endsWith("_category_.json")
);
expect(writeCall).toBeDefined();
const written = JSON.parse(writeCall[1] as string);
expect(written.label).toBe("Título Traduzido");
expect(written.customProps.title).toBe("Título Traduzido");
});
});

describe("non-toggle page output regression", () => {
it("writes .mdx file and does not produce _category_.json for page-type content", async () => {
const { saveTranslatedContentToDisk } = await import("./index");

const regularPage = createMockNotionPage({
id: "page-regular123",
title: "Regular Page",
elementType: "Page",
});

const filePath = await saveTranslatedContentToDisk(
regularPage,
"# Translated Content",
"Translated Title",
{
language: "pt-BR",
notionLangCode: "Portuguese",
outputDir: "/test/output",
}
);

expect(filePath).toMatch(/\.md$/);
expect(filePath).not.toContain("_category_.json");

const categoryCall = mockWriteFile.mock.calls.find((call: string[]) =>
call[0].endsWith("_category_.json")
);
expect(categoryCall).toBeUndefined();
});
});

describe("missing parent relation handling", () => {
it("gracefully skips pages without Parent item relation and reports as non-critical failure", async () => {
// Create a page WITHOUT parent relation
Expand Down
5 changes: 3 additions & 2 deletions scripts/notion-translate/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -681,8 +681,9 @@ export async function saveTranslatedContentToDisk(
await fs.mkdir(sectionPath, { recursive: true });

// Create _category_.json file
const effectiveTitle = translatedTitle?.trim() || title || "untitled";
const categoryContent = {
label: translatedTitle,
label: effectiveTitle,
position:
(
englishPage.properties[NOTION_PROPERTIES.ORDER] as
Expand All @@ -695,7 +696,7 @@ export async function saveTranslatedContentToDisk(
type: "generated-index",
},
customProps: {
title: translatedTitle,
title: effectiveTitle,
},
};

Expand Down
65 changes: 65 additions & 0 deletions scripts/verify-locale-output.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,71 @@ describe("Locale Output Verification", () => {
).toBeLessThanOrEqual(maxAllowedDiff);
});

it("has non-empty label in translated toggle _category_.json files", async () => {
const locales = ["es", "pt"];
for (const locale of locales) {
const localeDocsDir = path.join(
i18nDir,
locale,
"docusaurus-plugin-content-docs",
"current"
);
let categoryFiles: string[];
try {
// Recursively find all _category_.json files under the locale docs dir
const findCategoryFiles = async (dir: string): Promise<string[]> => {
const results: string[] = [];
let entries;
try {
entries = await fs.readdir(dir, { withFileTypes: true });
} catch {
return results;
}
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
results.push(...(await findCategoryFiles(fullPath)));
} else if (entry.name === "_category_.json") {
results.push(fullPath);
}
}
return results;
};
categoryFiles = await findCategoryFiles(localeDocsDir);
} catch (error) {
if (
error instanceof Error &&
"code" in error &&
(error as NodeJS.ErrnoException).code === "ENOENT"
) {
console.log(
`${locale} locale docs directory not found - content branch may not have toggle pages`
);
continue;
}
throw error;
}

if (categoryFiles.length === 0) {
console.log(
`No _category_.json files found for locale ${locale} - may not have toggle pages`
);
continue;
}

for (const filePath of categoryFiles) {
const content = await fs.readFile(filePath, "utf8");
const category = JSON.parse(content);
expect(
category.label,
`_category_.json at ${filePath} has empty label`
).toBeTruthy();
expect(typeof category.label).toBe("string");
expect(category.label.trim().length).toBeGreaterThan(0);
}
}
});

it("does not have English locale directory (en/)", async () => {
const enDir = path.join(i18nDir, "en");

Expand Down
Loading