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,5 @@
- summary: |
Support registering translated API definitions alongside translated docs pages.
Users can place translated API definition JSON files in `translations/<lang>/apis/<api-name>.json`
and they will be included in the translation registration request to FDR.
type: feat
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,14 @@ export async function parseDocsConfiguration({
context
});

const translationLocales =
rawDocsConfiguration.translations?.map((t) => docsYml.DocsYmlSchemas.normalizeTranslationConfig(t).lang) ?? [];
const translationApiDefinitionsPromise = loadTranslationApiDefinitions({
locales: translationLocales,
defaultLocale,
absolutePathToFernFolder,
context
});
const [
navigation,
pages,
Expand All @@ -182,7 +190,8 @@ export async function parseDocsConfiguration({
llmsTxtFile,
llmsFullTxtFile,
translationPages,
translationNavigationOverlays
translationNavigationOverlays,
translationApiDefinitions
] = await Promise.all([
convertedNavigationPromise,
pagesPromise,
Expand All @@ -194,7 +203,8 @@ export async function parseDocsConfiguration({
llmsTxtFilePromise,
llmsFullTxtFilePromise,
translationPagesPromise,
translationNavigationOverlaysPromise
translationNavigationOverlaysPromise,
translationApiDefinitionsPromise
]);

// Validate incompatible tabs configuration: sidebar placement + center alignment
Expand Down Expand Up @@ -227,6 +237,9 @@ export async function parseDocsConfiguration({
/* per-locale translated navigation overlays */
translationNavigationOverlays,

/* per-locale translated API definitions */
translationApiDefinitions,

/* navigation */
landingPage,
navigation,
Expand Down Expand Up @@ -2321,3 +2334,76 @@ function parseVariantOverlays(variants: unknown[]): docsYml.VariantOverlay[] {
}
return result;
}

/**
* Loads translated API definition JSON files from `translations/<lang>/apis/` directories.
*
* For each locale declared in `translations`, this function:
* 1. Checks for a `translations/<lang>/apis/` directory.
* 2. Reads all `.json` files from that directory.
* 3. Returns a map of locale → { apiName → parsed JSON definition }.
*
* The apiName is derived from the filename (e.g., `my-api.json` → `my-api`).
*/
async function loadTranslationApiDefinitions({
locales,
defaultLocale,
absolutePathToFernFolder,
context
}: {
locales: string[];
defaultLocale: string | undefined;
absolutePathToFernFolder: AbsoluteFilePath;
context: TaskContext;
}): Promise<Record<string, Record<string, unknown>> | undefined> {
if (locales.length === 0) {
return undefined;
}

const translationsRootDir = path.join(absolutePathToFernFolder, "translations") as AbsoluteFilePath;

const result: Record<string, Record<string, unknown>> = {};
let hasAny = false;

await Promise.all(
locales.map(async (lang) => {
if (lang === defaultLocale) {
return;
}

const apisDir = path.join(translationsRootDir, lang, "apis") as AbsoluteFilePath;

if (!(await doesPathExist(apisDir))) {
return;
}

const jsonFiles = await listFiles(apisDir, "json");
if (jsonFiles.length === 0) {
return;
}

const apiDefs: Record<string, unknown> = {};
await Promise.all(
jsonFiles.map(async (filePath) => {
const filename = path.basename(filePath);
const apiName = filename.replace(/\.json$/, "");
try {
const content = await readFile(filePath, "utf-8");
apiDefs[apiName] = JSON.parse(content);
} catch (error) {
context.logger.warn(
`Failed to parse translated API definition at translations/${lang}/apis/${filename}: ${String(error)}`
);
}
})
);

if (Object.keys(apiDefs).length > 0) {
result[lang] = apiDefs;
hasAny = true;
}
})
);

return hasAny ? result : undefined;
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ export interface ParsedDocsConfiguration {
/* per-locale translated navigation overlays: locale → NavigationOverlay */
translationNavigationOverlays: Record<string, TranslationNavigationOverlay> | undefined;

/* per-locale translated API definitions: locale → { apiName → JSON definition } */
translationApiDefinitions: Record<string, Record<string, unknown>> | undefined;

/* RBAC declaration */
roles: string[] | undefined;

Expand Down
9 changes: 9 additions & 0 deletions packages/cli/docs-resolver/src/DocsDefinitionResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,15 @@ export class DocsDefinitionResolver {
return this._parsedDocsConfig?.translationNavigationOverlays;
}

/**
* Returns per-locale translated API definitions loaded from `translations/<lang>/apis/` directories.
* Each entry maps locale → { apiName → JSON definition }.
* Must be called after `resolve()`.
*/
public getTranslationApiDefinitions(): Record<string, Record<string, unknown>> | undefined {
return this._parsedDocsConfig?.translationApiDefinitions;
}

/**
* Returns the map of absolute file paths to uploaded file IDs.
* Used by translation processing to rewrite image paths in translated pages.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,7 @@ export async function publishDocs({
// so that translated docs are visible in preview without overwriting production translations.
const translationPages = resolver.getTranslationPages();
const translationNavigationOverlays = resolver.getTranslationNavigationOverlays();
const translationApiDefinitions = resolver.getTranslationApiDefinitions();
const translationDomain = preview ? urlToOutput : domain;
if (translationPages != null && Object.keys(translationPages).length > 0) {
Comment on lines 638 to 639
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The translation registration logic only checks for translationPages but not translationApiDefinitions. If a user provides only translated API definitions without translated pages, the entire registration block is skipped and API definitions are never registered.

Fix:

if ((translationPages != null && Object.keys(translationPages).length > 0) || 
    (translationApiDefinitions != null && Object.keys(translationApiDefinitions).length > 0)) {
    const totalLocales = new Set([
        ...Object.keys(translationPages ?? {}),
        ...Object.keys(translationApiDefinitions ?? {})
    ]).size;
    context.logger.info(`Registering translations for ${totalLocales} locale(s)...`);
Suggested change
const translationDomain = preview ? urlToOutput : domain;
if (translationPages != null && Object.keys(translationPages).length > 0) {
const translationDomain = preview ? urlToOutput : domain;
if ((translationPages != null && Object.keys(translationPages).length > 0) ||
(translationApiDefinitions != null && Object.keys(translationApiDefinitions).length > 0)) {
const totalLocales = new Set([
...Object.keys(translationPages ?? {}),
...Object.keys(translationApiDefinitions ?? {})
]).size;
context.logger.info(`Registering translations for ${totalLocales} locale(s)...`);

Spotted by Graphite

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

context.logger.info(`Registering translations for ${Object.keys(translationPages).length} locale(s)...`);
Expand Down Expand Up @@ -727,12 +728,15 @@ export async function publishDocs({
announcement: translatedAnnouncement
}
};
// Use a raw fetch instead of the oRPC client to send `docsDefinition`
// (the live server expects that field; the published fdr-sdk still uses `content`).
const localeApiDefs = translationApiDefinitions?.[locale];
const pageCount = Object.keys(localePages).length;
const apiDefNames = localeApiDefs != null ? Object.keys(localeApiDefs) : [];
context.logger.debug(
`Sending translation for locale "${locale}" (${pageCount} page${pageCount === 1 ? "" : "s"})`
`Sending translation for locale "${locale}" (${pageCount} page${pageCount === 1 ? "" : "s"})` +
(apiDefNames.length > 0 ? `, apiDefinitions=${JSON.stringify(apiDefNames)}` : "")
);
// Use a raw fetch instead of the oRPC client to send `docsDefinition`
// (the live server expects that field; the published fdr-sdk still uses `content`).
const translationResponse = await fetch(`${fdrOrigin}/v2/registry/docs/translations/register`, {
method: "POST",
headers: {
Expand All @@ -744,7 +748,8 @@ export async function publishDocs({
domain: translationDomain,
orgId: organization,
locale,
docsDefinition: translatedDefinition
docsDefinition: translatedDefinition,
...(localeApiDefs != null ? { apiDefinitions: localeApiDefs } : {})
})
});
if (!translationResponse.ok) {
Expand Down
Loading