Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
- summary: |
Support translated API specs in docs publishes by registering locale-specific API definitions and wiring translated docs payloads and endpoint navigation titles to those API IDs.
type: feat
Original file line number Diff line number Diff line change
Expand Up @@ -2381,6 +2381,18 @@ function parseNavigationItemOverlays(items: unknown[]): docsYml.NavigationItemOv
continue;
}

// An API reference item: { api: "Title", layout: [...] }
if (typeof obj.api === "string") {
const apiOverlay: docsYml.NavigationItemOverlay.Api = {
type: "api",
title: obj.api,
slug: typeof obj.slug === "string" ? obj.slug : undefined,
layout: Array.isArray(obj.layout) ? parseNavigationItemOverlays(obj.layout) : undefined
};
result.push(apiOverlay);
continue;
}

// A page item: { page: "Title", path: "..." }
if (typeof obj.page === "string") {
const pageOverlay: docsYml.NavigationItemOverlay.Page = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,7 @@ export interface AnnouncementOverlay {
export type NavigationItemOverlay =
| NavigationItemOverlay.Page
| NavigationItemOverlay.Section
| NavigationItemOverlay.Api
| NavigationItemOverlay.Tab
| NavigationItemOverlay.Variant;

Expand All @@ -608,6 +609,12 @@ export declare namespace NavigationItemOverlay {
slug: string | undefined;
contents: NavigationItemOverlay[] | undefined;
}
export interface Api {
type: "api";
title: string | undefined;
slug: string | undefined;
layout: NavigationItemOverlay[] | undefined;
}
export interface Tab {
type: "tab";
tabId: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,83 @@ describe("applyTranslatedNavigationOverlays", () => {
expect(sectionChildren[0]?.title).toBe("概要");
});

it("applies api reference and api package title overrides from navigation overlay", () => {
const root = {
type: "root",
child: {
type: "unversioned",
child: {
type: "tabbed",
children: [
{
type: "tab",
title: "API Reference",
slug: "docs/api-reference",
child: {
type: "sidebarRoot",
children: [
{
type: "apiReference",
title: "Plant Store API",
slug: "docs/api-reference/plant-store-api",
children: [
{
type: "apiPackage",
title: "Plants",
slug: "docs/api-reference/plant-store-api/plants",
children: [
{
type: "endpoint",
title: "Create plant",
slug: "docs/api-reference/plant-store-api/plants/create-plant"
}
]
}
]
}
]
}
}
]
}
}
};
const overlay: docsYml.TranslationNavigationOverlay = {
...emptyOverlay(),
tabs: {
"api-reference": { displayName: "API 参考", slug: undefined }
},
navigation: [
{
type: "tab",
tabId: "API Reference",
layout: [
{
type: "api",
title: "植物商店 API",
slug: "plant-store-api",
layout: [{ type: "section", title: "植物", slug: "plants", contents: undefined }]
}
],
variants: undefined
}
]
};

const result = applyTranslatedNavigationOverlays(asRoot(root), overlay);
const unversioned = (result as unknown as Record<string, unknown>).child as Record<string, unknown>;
const tabbed = unversioned.child as Record<string, unknown>;
const tab = (tabbed.children as Array<Record<string, unknown>>)[0] as Record<string, unknown>;
expect(tab.title).toBe("API 参考");

const sidebarRoot = tab.child as Record<string, unknown>;
const apiReference = (sidebarRoot.children as Array<Record<string, unknown>>)[0] as Record<string, unknown>;
expect(apiReference.title).toBe("植物商店 API");

const apiPackage = (apiReference.children as Array<Record<string, unknown>>)[0] as Record<string, unknown>;
expect(apiPackage.title).toBe("植物");
});

it("matches tabs by overlay slug when tab ID differs from nav slug", () => {
const root = {
type: "root",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,10 @@ function applyTabOverlayToNode(node: Record<string, unknown>, overlay: docsYml.T
for (const [tabId, tabOverlay] of Object.entries(overlay.tabs)) {
// Match by tab ID (YAML key) against nav tree slug segment, or
// by the overlay's explicit slug field against the slug segment
const isMatch = tabId === tabSlug || (tabOverlay.slug != null && tabOverlay.slug === tabSlug);
const isMatch =
tabId === tabSlug ||
slugifyNavigationKey(tabId) === tabSlug ||
(tabOverlay.slug != null && tabOverlay.slug === tabSlug);
if (isMatch && tabOverlay.displayName != null) {
node["title"] = tabOverlay.displayName;
break;
Expand Down Expand Up @@ -309,7 +312,7 @@ function findTabNavOverlay(
continue;
}
// Match by tab ID against slug
if (tabSlug != null && navItem.tabId === tabSlug) {
if (tabSlug != null && (navItem.tabId === tabSlug || slugifyNavigationKey(navItem.tabId) === tabSlug)) {
return navItem;
}
// Match by overlay tab's explicit slug field against the nav tree slug
Expand Down Expand Up @@ -345,6 +348,7 @@ function applySidebarChildOverlays(
navOverlays: docsYml.NavigationItemOverlay[],
overlay: docsYml.TranslationNavigationOverlay
): unknown[] {
let apiIdx = 0;
let sectionIdx = 0;
let pageIdx = 0;

Expand All @@ -356,6 +360,29 @@ function applySidebarChildOverlays(

const childType = childObj["type"] as string | undefined;

if (childType === "apiReference") {
const apiOverlays = navOverlays.filter(
(item): item is docsYml.NavigationItemOverlay.Api => item.type === "api"
);
const matched = matchApiOverlay(childObj, apiOverlays, apiIdx);
apiIdx++;

if (matched != null) {
const walked = walkAndApply(child, overlay) as Record<string, unknown>;
if (matched.title != null) {
walked["title"] = matched.title;
}
if (matched.layout != null) {
const childArray = walked["children"] as unknown[] | undefined;
if (childArray != null) {
walked["children"] = applySidebarChildOverlays(childArray, matched.layout, overlay);
}
}
return walked;
}
return walkAndApply(child, overlay);
}

if (childType === "section") {
const sectionOverlays = navOverlays.filter(
(item): item is docsYml.NavigationItemOverlay.Section => item.type === "section"
Expand All @@ -380,6 +407,29 @@ function applySidebarChildOverlays(
return walkAndApply(child, overlay);
}

if (childType === "apiPackage") {
const sectionOverlays = navOverlays.filter(
(item): item is docsYml.NavigationItemOverlay.Section => item.type === "section"
);
const matched = matchSectionOverlay(childObj, sectionOverlays, sectionIdx);
sectionIdx++;

if (matched != null) {
const walked = walkAndApply(child, overlay) as Record<string, unknown>;
if (matched.title != null) {
walked["title"] = matched.title;
}
if (matched.contents != null) {
const childArray = walked["children"] as unknown[] | undefined;
if (childArray != null) {
walked["children"] = applySidebarChildOverlays(childArray, matched.contents, overlay);
}
}
return walked;
}
return walkAndApply(child, overlay);
}

if (childType === "page" || childType === "landingPage") {
const pageOverlays = navOverlays.filter(
(item): item is docsYml.NavigationItemOverlay.Page => item.type === "page"
Expand All @@ -399,6 +449,26 @@ function applySidebarChildOverlays(
});
}

function matchApiOverlay(
apiReference: Record<string, unknown>,
overlays: docsYml.NavigationItemOverlay.Api[],
positionIndex: number
): docsYml.NavigationItemOverlay.Api | undefined {
const apiSlug = extractLastSlugSegment(apiReference["slug"] as string | undefined);

for (const o of overlays) {
if (o.slug != null && o.slug === apiSlug) {
return o;
}
}

const noSlugOverlays = overlays.filter((o) => o.slug == null);
if (positionIndex < noSlugOverlays.length) {
return noSlugOverlays[positionIndex];
}
return undefined;
}

function matchSectionOverlay(
section: Record<string, unknown>,
overlays: docsYml.NavigationItemOverlay.Section[],
Expand Down Expand Up @@ -453,6 +523,14 @@ function extractLastSlugSegment(slug: string | undefined): string | undefined {
return parts[parts.length - 1];
}

function slugifyNavigationKey(value: string): string {
return value
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
}

/**
* Applies variant overlays to tab variant children.
* Matches variants by slug first, then falls back to positional matching.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"@fern-api/configuration": "workspace:*",
"@fern-api/core": "workspace:*",
"@fern-api/core-utils": "workspace:*",
"@fern-api/docs-markdown-utils": "workspace:*",
"@fern-api/docs-resolver": "workspace:*",
"@fern-api/fai-sdk": "catalog:",
"@fern-api/fdr-sdk": "1.2.0-1d73d2e141",
Expand Down
Loading
Loading