| | import path from 'path' |
| |
|
| | import languages from '@/languages/lib/languages-server' |
| | import type { Language } from '@/languages/lib/languages' |
| | import type { UnversionedTree, UnversionLanguageTree, SiteTree, Tree } from '@/types' |
| |
|
| | import { allVersions } from '@/versions/lib/all-versions' |
| | import createTree from './create-tree' |
| | import nonEnterpriseDefaultVersion from '@/versions/lib/non-enterprise-default-version' |
| | import readFileContents from './read-file-contents' |
| | import Page from './page' |
| | import Permalink from './permalink' |
| | import frontmatterSchema from './frontmatter' |
| | import { correctTranslatedContentStrings } from '@/languages/lib/correct-translation-content' |
| |
|
| | interface FileSystemError extends Error { |
| | code?: string |
| | } |
| |
|
| | |
| | |
| | |
| | const DEBUG_TRANSLATION_FALLBACKS = Boolean( |
| | JSON.parse(process.env.DEBUG_TRANSLATION_FALLBACKS || 'false'), |
| | ) |
| | |
| | |
| | const THROW_TRANSLATION_ERRORS = Boolean( |
| | JSON.parse(process.env.THROW_TRANSLATION_ERRORS || 'false'), |
| | ) |
| |
|
| | const versions = Object.keys(allVersions) |
| |
|
| | class FrontmatterParsingError extends Error {} |
| |
|
| | |
| | |
| | const translatableFrontmatterKeys = Object.entries(frontmatterSchema.schema.properties) |
| | .filter(([, value]: [string, any]) => value.translatable) |
| | .map(([key]) => key) |
| |
|
| | |
| | |
| | |
| | |
| | |
| | export async function loadUnversionedTree( |
| | languagesOnly: string[] = [], |
| | ): Promise<UnversionLanguageTree> { |
| | if (languagesOnly && !Array.isArray(languagesOnly)) { |
| | throw new Error("'languagesOnly' has to be an array") |
| | } |
| | const unversionedTree: UnversionLanguageTree = {} as UnversionLanguageTree |
| | const enTree = await createTree(path.join(languages.en.dir, 'content')) |
| | if (enTree) { |
| | unversionedTree.en = enTree |
| | setCategoryApplicableVersions(unversionedTree.en) |
| | } |
| |
|
| | const languagesValues = Object.entries(languages) |
| | .filter(([language]) => { |
| | return !languagesOnly.length || languagesOnly.includes(language) |
| | }) |
| | .map(([, data]) => { |
| | return data |
| | }) |
| |
|
| | await Promise.all( |
| | languagesValues |
| | .filter((langObj) => langObj.code !== 'en') |
| | .map(async (langObj) => { |
| | const localizedContentPath = path.join(langObj.dir, 'content') |
| | unversionedTree[langObj.code] = await translateTree( |
| | localizedContentPath, |
| | langObj, |
| | unversionedTree.en, |
| | ) |
| | setCategoryApplicableVersions(unversionedTree[langObj.code]) |
| | }), |
| | ) |
| |
|
| | return unversionedTree |
| | } |
| |
|
| | function setCategoryApplicableVersions(tree: UnversionedTree): void { |
| | |
| | |
| | |
| | |
| | for (const childPage of tree.childPages) { |
| | if (childPage.page.relativePath.endsWith('index.md')) { |
| | const combinedApplicableVersions: string[] = [] |
| | let moreThanOneChild = false |
| | for (const childChildPage of childPage.childPages || []) { |
| | for (const version of childChildPage.page.applicableVersions) { |
| | if (!combinedApplicableVersions.includes(version)) { |
| | combinedApplicableVersions.push(version) |
| | } |
| | } |
| | setCategoryApplicableVersions(childPage) |
| | moreThanOneChild = true |
| | } |
| | if ( |
| | |
| | |
| | |
| | moreThanOneChild && |
| | !equalSets( |
| | new Set(childPage.page.applicableVersions), |
| | new Set(combinedApplicableVersions), |
| | ) && |
| | !childPage.page.relativePath.startsWith('early-access') |
| | ) { |
| | const newPermalinks = Permalink.derive( |
| | childPage.page.languageCode, |
| | childPage.page.relativePath, |
| | childPage.page.title, |
| | combinedApplicableVersions, |
| | ) |
| | childPage.page.permalinks = newPermalinks |
| | childPage.page.applicableVersions = combinedApplicableVersions |
| | } |
| | } |
| | } |
| | } |
| |
|
| | function equalSets(setA: Set<string>, setB: Set<string>): boolean { |
| | return setA.size === setB.size && [...setA].every((x) => setB.has(x)) |
| | } |
| |
|
| | async function translateTree( |
| | dir: string, |
| | langObj: Language, |
| | enTree: UnversionedTree, |
| | ): Promise<UnversionedTree> { |
| | const item: Partial<UnversionedTree> = {} |
| | const enPage = enTree.page |
| | const { ...enData } = enPage |
| |
|
| | const basePath = dir |
| | const relativePath = enPage.relativePath |
| | const fullPath = path.join(basePath, relativePath) |
| |
|
| | let data |
| | let content |
| | try { |
| | const read = await readFileContents(fullPath) |
| | |
| | content = read.content |
| | data = read.data |
| |
|
| | if (!data) { |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | throw new FrontmatterParsingError(read.errors) |
| | } |
| |
|
| | for (const { property } of read.errors) { |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | if (translatableFrontmatterKeys.includes(property)) { |
| | const message = `frontmatter error on '${property}' (in ${fullPath}) so falling back to English` |
| | if (DEBUG_TRANSLATION_FALLBACKS) { |
| | |
| | console.warn({ message, path: relativePath }) |
| | } |
| | if (THROW_TRANSLATION_ERRORS) { |
| | throw new Error(message) |
| | } |
| | |
| | ;(data as any)[property] = (enData as any)[property] |
| | } |
| | } |
| | } catch (error) { |
| | |
| | |
| | if ((error as FileSystemError).code === 'ENOENT' || error instanceof FrontmatterParsingError) { |
| | data = enData |
| | content = enPage.markdown |
| | const message = `Unable to initialize ${fullPath} because translation content file does not exist.` |
| | if (DEBUG_TRANSLATION_FALLBACKS) { |
| | |
| | console.warn({ message, path: relativePath }) |
| | } |
| | if (THROW_TRANSLATION_ERRORS) { |
| | throw new Error(message) |
| | } |
| | } else { |
| | throw error |
| | } |
| | } |
| |
|
| | const translatedData = Object.fromEntries( |
| | translatableFrontmatterKeys.map((key) => { |
| | return [key, data[key]] |
| | }), |
| | ) |
| |
|
| | |
| | translatedData.markdown = correctTranslatedContentStrings(content, enPage.markdown, { |
| | relativePath, |
| | code: langObj.code, |
| | }) |
| |
|
| | translatedData.title = correctTranslatedContentStrings(translatedData.title || '', enPage.title, { |
| | relativePath, |
| | code: langObj.code, |
| | }) |
| | if (translatedData.shortTitle) { |
| | translatedData.shortTitle = correctTranslatedContentStrings( |
| | translatedData.shortTitle, |
| | enPage.shortTitle || '', |
| | { |
| | relativePath, |
| | code: langObj.code, |
| | }, |
| | ) |
| | } |
| | if (translatedData.intro) { |
| | translatedData.intro = correctTranslatedContentStrings(translatedData.intro, enPage.intro, { |
| | relativePath, |
| | code: langObj.code, |
| | }) |
| | } |
| |
|
| | |
| | ;(item as UnversionedTree).page = new Page( |
| | Object.assign( |
| | {}, |
| | |
| | enData, |
| | |
| | { |
| | basePath, |
| | relativePath, |
| | languageCode: langObj.code, |
| | fullPath, |
| | }, |
| | |
| | translatedData, |
| | ) as any, |
| | ) as any |
| | if ( |
| | ((item as UnversionedTree).page as any).children && |
| | ((item as UnversionedTree).page as any).children.length > 0 |
| | ) { |
| | ;(item as UnversionedTree).childPages = await Promise.all( |
| | enTree.childPages |
| | .filter((childTree: UnversionedTree) => { |
| | |
| | return childTree.page.relativePath.split(path.sep)[0] !== 'early-access' |
| | }) |
| | .map((childTree: UnversionedTree) => translateTree(dir, langObj, childTree)), |
| | ) |
| | } |
| |
|
| | return item as UnversionedTree |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | export async function loadSiteTree( |
| | unversionedTree?: UnversionLanguageTree, |
| | languagesOnly: string[] = [], |
| | ): Promise<SiteTree> { |
| | const rawTree = Object.assign({}, unversionedTree || (await loadUnversionedTree(languagesOnly))) |
| | const siteTree: SiteTree = {} |
| |
|
| | const langCodes = (languagesOnly.length && languagesOnly) || Object.keys(languages) |
| | |
| | await Promise.all( |
| | langCodes.map(async (langCode) => { |
| | if (!(langCode in rawTree)) { |
| | throw new Error(`No tree for language ${langCode}`) |
| | } |
| | const treePerVersion: { [version: string]: Tree } = {} |
| | |
| | await Promise.all( |
| | versions.map(async (version) => { |
| | |
| | treePerVersion[version] = await versionPages( |
| | Object.assign({}, rawTree[langCode]), |
| | version, |
| | langCode, |
| | ) |
| | }), |
| | ) |
| |
|
| | siteTree[langCode] = treePerVersion |
| | }), |
| | ) |
| |
|
| | return siteTree |
| | } |
| |
|
| | export async function versionPages(obj: any, version: string, langCode: string): Promise<Tree> { |
| | |
| | const permalink = obj.page.permalinks.find( |
| | (pl: any) => |
| | pl.pageVersion === version || |
| | (pl.pageVersion === 'homepage' && version === nonEnterpriseDefaultVersion), |
| | ) |
| | if (!permalink) { |
| | throw new Error( |
| | `No permalink for ${obj.page.fullPath} in language ${langCode} for version ${version}`, |
| | ) |
| | } |
| | obj.href = permalink.href |
| |
|
| | if (!obj.childPages) return obj |
| | const versionedChildPages = await Promise.all( |
| | obj.childPages |
| | |
| | .filter((childPage: any) => childPage.page.applicableVersions.includes(version)) |
| | |
| | .map((childPage: any) => versionPages(Object.assign({}, childPage), version, langCode)), |
| | ) |
| |
|
| | obj.childPages = [...versionedChildPages] |
| |
|
| | return obj |
| | } |
| |
|
| | |
| | export async function loadPageList( |
| | unversionedTree?: UnversionLanguageTree, |
| | languagesOnly: string[] = [], |
| | ): Promise<Page[]> { |
| | if (languagesOnly && !Array.isArray(languagesOnly)) { |
| | throw new Error("'languagesOnly' has to be an array") |
| | } |
| | const rawTree = unversionedTree || (await loadUnversionedTree(languagesOnly)) |
| | const pageList: Page[] = [] |
| |
|
| | const langCodes = (languagesOnly.length && languagesOnly) || Object.keys(languages) |
| | await Promise.all( |
| | langCodes.map(async (langCode) => { |
| | if (!(langCode in rawTree)) { |
| | throw new Error(`No tree for language ${langCode}`) |
| | } |
| | await addToCollection(rawTree[langCode], pageList) |
| | }), |
| | ) |
| |
|
| | async function addToCollection(item: UnversionedTree, collection: Page[]): Promise<void> { |
| | if (!item.page) return |
| | collection.push(item.page as any) |
| |
|
| | if (!item.childPages) return |
| | await Promise.all( |
| | item.childPages.map( |
| | async (childPage: UnversionedTree) => await addToCollection(childPage, collection), |
| | ), |
| | ) |
| | } |
| |
|
| | return pageList |
| | } |
| |
|
| | export const loadPages = loadPageList |
| |
|
| | |
| | export function createMapFromArray(pageList: Page[]): Record<string, Page> { |
| | const pageMap = pageList.reduce( |
| | (accumulatedMap: Record<string, Page>, page: Page) => { |
| | for (const permalink of page.permalinks) { |
| | accumulatedMap[permalink.href] = page |
| | } |
| | return accumulatedMap |
| | }, |
| | {} as Record<string, Page>, |
| | ) |
| |
|
| | return pageMap |
| | } |
| |
|
| | export async function loadPageMap( |
| | pageList?: Page[], |
| | languagesOnly: string[] = [], |
| | ): Promise<Record<string, Page>> { |
| | const pages = pageList || (await loadPageList(undefined, languagesOnly)) |
| | const pageMap = createMapFromArray(pages) |
| | return pageMap |
| | } |
| |
|
| | export default { |
| | loadUnversionedTree, |
| | loadSiteTree, |
| | loadPages: loadPageList, |
| | loadPageMap, |
| | } |
| |
|