Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 2s
170 lines
4.4 KiB
TypeScript
170 lines
4.4 KiB
TypeScript
import fs from 'fs';
|
|
import path from 'path';
|
|
|
|
import {
|
|
type BaseGroup,
|
|
type BaseStructure,
|
|
} from '../navigation/base-structure-types';
|
|
import {
|
|
DEFAULT_LANGUAGE,
|
|
SUPPORTED_LANGUAGES,
|
|
} from '../navigation/supported-languages';
|
|
|
|
type TranslationGroupEntry = {
|
|
label: string;
|
|
groups?: Record<string, TranslationGroupEntry>;
|
|
};
|
|
|
|
type TranslationTabEntry = {
|
|
label: string;
|
|
groups: Record<string, TranslationGroupEntry>;
|
|
};
|
|
|
|
type TranslationFile = {
|
|
tabs: Record<string, TranslationTabEntry>;
|
|
};
|
|
|
|
type TranslationMaps = {
|
|
tabLabels: Map<string, string>;
|
|
groupLabels: Map<string, string>;
|
|
};
|
|
|
|
type GeneratedLanguage = {
|
|
language: string;
|
|
tabs: Array<{
|
|
tab: string;
|
|
groups: GeneratedGroup[];
|
|
}>;
|
|
};
|
|
|
|
type GeneratedGroup = {
|
|
group: string;
|
|
icon?: string;
|
|
pages: Array<string | GeneratedGroup>;
|
|
};
|
|
|
|
const baseStructurePath = path.resolve(
|
|
__dirname,
|
|
'../navigation/base-structure.json',
|
|
);
|
|
const docsPath = path.resolve(__dirname, '../docs.json');
|
|
const localesRoot = path.resolve(__dirname, '../l');
|
|
|
|
const baseStructure: BaseStructure = JSON.parse(
|
|
fs.readFileSync(baseStructurePath, 'utf8'),
|
|
);
|
|
|
|
const docsConfig = JSON.parse(fs.readFileSync(docsPath, 'utf8'));
|
|
|
|
const collectTranslations = (file: TranslationFile | null): TranslationMaps => {
|
|
const tabLabels = new Map<string, string>();
|
|
const groupLabels = new Map<string, string>();
|
|
|
|
const collectGroups = (groups?: Record<string, TranslationGroupEntry>) => {
|
|
if (!groups) {
|
|
return;
|
|
}
|
|
|
|
Object.entries(groups).forEach(([key, group]) => {
|
|
groupLabels.set(key, group.label);
|
|
collectGroups(group.groups);
|
|
});
|
|
};
|
|
|
|
Object.entries(file?.tabs ?? {}).forEach(([key, tab]) => {
|
|
tabLabels.set(key, tab.label);
|
|
collectGroups(tab.groups);
|
|
});
|
|
|
|
return { tabLabels, groupLabels };
|
|
};
|
|
|
|
const loadTranslationFile = (language: string): TranslationFile | null => {
|
|
const translationPath = path.join(localesRoot, language, 'navigation.json');
|
|
|
|
if (!fs.existsSync(translationPath)) {
|
|
return null;
|
|
}
|
|
|
|
return JSON.parse(
|
|
fs.readFileSync(translationPath, 'utf8'),
|
|
) as TranslationFile;
|
|
};
|
|
|
|
const buildLanguageEntry = (language: string): GeneratedLanguage => {
|
|
const translationFile =
|
|
language === DEFAULT_LANGUAGE ? null : loadTranslationFile(language);
|
|
const translationMaps = collectTranslations(translationFile);
|
|
|
|
return {
|
|
language,
|
|
tabs: baseStructure.tabs
|
|
.map((tab) => ({
|
|
tab: translationMaps.tabLabels.get(tab.key) ?? tab.label,
|
|
groups: tab.groups
|
|
.map((group) => buildGroup(group, translationMaps, language))
|
|
.filter((group): group is GeneratedGroup => group !== null),
|
|
}))
|
|
.filter((tab) => tab.groups.length > 0),
|
|
};
|
|
};
|
|
|
|
// Mintlify requires each page path to appear in only one language's navigation.
|
|
// Duplicating a path across languages breaks the language switcher (it can no
|
|
// longer resolve the equivalent page and falls back to the first page). So a
|
|
// page is only included in a non-default language when its translation exists,
|
|
// and empty groups/tabs are dropped entirely.
|
|
const buildGroup = (
|
|
group: BaseGroup,
|
|
translations: TranslationMaps,
|
|
language: string,
|
|
): GeneratedGroup | null => {
|
|
const pages = group.pages
|
|
.map((page) =>
|
|
typeof page === 'string'
|
|
? formatPageSlug(page, language)
|
|
: buildGroup(page, translations, language),
|
|
)
|
|
.filter((page): page is string | GeneratedGroup => page !== null);
|
|
|
|
if (pages.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
group: translations.groupLabels.get(group.key) ?? group.label,
|
|
...(group.icon ? { icon: group.icon } : {}),
|
|
pages,
|
|
};
|
|
};
|
|
|
|
const formatPageSlug = (slug: string, language: string): string | null => {
|
|
if (language === DEFAULT_LANGUAGE) {
|
|
return slug;
|
|
}
|
|
|
|
const localizedPagePath = path.join(localesRoot, language, `${slug}.mdx`);
|
|
|
|
return fs.existsSync(localizedPagePath) ? `l/${language}/${slug}` : null;
|
|
};
|
|
|
|
const hasLocaleContent = (language: string): boolean => {
|
|
if (language === DEFAULT_LANGUAGE) {
|
|
return true;
|
|
}
|
|
|
|
const localeDir = path.join(localesRoot, language);
|
|
return fs.existsSync(localeDir);
|
|
};
|
|
|
|
const languages =
|
|
SUPPORTED_LANGUAGES.filter(hasLocaleContent).map(buildLanguageEntry);
|
|
|
|
if (!docsConfig.navigation) {
|
|
docsConfig.navigation = {};
|
|
}
|
|
|
|
docsConfig.navigation.languages = languages;
|
|
|
|
fs.writeFileSync(docsPath, `${JSON.stringify(docsConfig, null, 2)}\n`);
|