| | import fs from 'fs' |
| | import path from 'path' |
| |
|
| | import yaml from 'js-yaml' |
| | import matter from '@gr2m/gray-matter' |
| | import { merge, get } from 'lodash-es' |
| |
|
| | import languages from '@/languages/lib/languages-server' |
| | import { correctTranslatedContentStrings } from '@/languages/lib/correct-translation-content' |
| |
|
| | interface YAMLException extends Error { |
| | mark?: any |
| | } |
| |
|
| | interface FileSystemError extends Error { |
| | code?: string |
| | } |
| |
|
| | |
| | |
| | const DEBUG_JIT_DATA_READS = Boolean(JSON.parse(process.env.DEBUG_JIT_DATA_READS || 'false')) |
| |
|
| | |
| | |
| | |
| | |
| | const ALWAYS_ENGLISH_YAML_FILES = new Set(['data/variables/product.yml']) |
| | const ALWAYS_ENGLISH_MD_FILES = new Set([ |
| | 'data/reusables/ssh/fingerprints.md', |
| | 'data/reusables/ssh/known_hosts.md', |
| | ]) |
| |
|
| | |
| | export const getDeepDataByLanguage = memoize( |
| | (dottedPath: string, langCode: string, dir: string | null = null): any => { |
| | if (!(langCode in languages)) { |
| | throw new Error(`langCode '${langCode}' not a recognized language code`) |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | if (dir === null) { |
| | dir = languages[langCode].dir |
| | } |
| | return getDeepDataByDir(dottedPath, dir) |
| | }, |
| | ) |
| |
|
| | |
| | |
| | function getDeepDataByDir(dottedPath: string, dir: string): any { |
| | const fullPath = ['data'] |
| | const split = dottedPath.split(/\./g) |
| | fullPath.push(...split) |
| |
|
| | const things: any = {} |
| | const relPath = fullPath.join(path.sep) |
| | for (const dirent of getDirents(dir, relPath)) { |
| | if (dirent.name === 'README.md') continue |
| | const key = dirent.isDirectory() ? dirent.name : dirent.name.replace(/\.yml$/, '') |
| | if (dirent.isDirectory()) { |
| | things[key] = getDeepDataByDir(`${dottedPath}.${key}`, dir) |
| | } else if (dirent.name.endsWith('.yml')) { |
| | things[key] = getYamlContent(dir, path.join(relPath, dirent.name)) |
| | } else if (dirent.name.endsWith('.md')) { |
| | things[key] = getMarkdownContent(dir, path.join(relPath, dirent.name)) |
| | } else { |
| | throw new Error(`don't know how to read '${dirent.name}'`) |
| | } |
| | } |
| | return things |
| | } |
| |
|
| | function getDirents(root: string, relPath: string): fs.Dirent[] { |
| | const filePath = root ? path.join(root, relPath) : relPath |
| | return fs.readdirSync(filePath, { withFileTypes: true }) |
| | } |
| |
|
| | export const getUIDataMerged = memoize((langCode: string): any => { |
| | const uiEnglish = getUIData('en') |
| | if (langCode === 'en') return uiEnglish |
| | |
| | |
| | |
| | |
| | |
| | |
| | const combined: any = {} |
| | merge(combined, uiEnglish) |
| | merge(combined, getUIData(langCode)) |
| | return combined |
| | }) |
| |
|
| | |
| | |
| | const getUIData = (langCode: string): any => { |
| | const fullPath = ['data', 'ui.yml'] |
| | const { dir } = languages[langCode] |
| | return getYamlContent(dir, fullPath.join(path.sep)) |
| | } |
| |
|
| | export const getDataByLanguage = memoize((dottedPath: string, langCode: string): any => { |
| | if (!(langCode in languages)) |
| | throw new Error(`langCode '${langCode}' not a recognized language code`) |
| | const { dir } = languages[langCode] |
| |
|
| | try { |
| | const value = getDataByDir(dottedPath, dir, languages.en.dir, langCode) |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | if (value === undefined && langCode !== 'en') { |
| | return getDataByDir(dottedPath, languages.en.dir) |
| | } |
| | return value |
| | } catch (error) { |
| | if (error instanceof Error && (error as YAMLException).mark && error.message) { |
| | |
| | |
| | |
| | |
| | if (langCode !== 'en') { |
| | if (DEBUG_JIT_DATA_READS) { |
| | console.warn(`Unable to parse Yaml in (${langCode}) '${dottedPath}': ${error.message}`) |
| | } |
| | |
| | return getDataByDir(dottedPath, languages.en.dir) |
| | } |
| | |
| | |
| | throw error |
| | } |
| |
|
| | if ((error as FileSystemError).code === 'ENOENT') return undefined |
| | throw error |
| | } |
| | }) |
| |
|
| | function getDataByDir( |
| | dottedPath: string, |
| | dir: string, |
| | englishRoot?: string, |
| | langCode?: string, |
| | ): any { |
| | const fullPath = ['data'] |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | const split = dottedPath.startsWith('release-notes') |
| | ? dottedPath.split('.') |
| | : getSmartSplit(dottedPath) |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | if (split[0] === 'early-access') { |
| | fullPath.push(split.shift()!) |
| | } |
| | const first = split[0] |
| |
|
| | if (first === 'variables') { |
| | const key = split.pop()! |
| | const basename = split.pop()! |
| | fullPath.push(...split) |
| | fullPath.push(`${basename}.yml`) |
| | const allData = getYamlContent(dir, fullPath.join(path.sep), englishRoot) |
| | if (allData && key) { |
| | const value = allData[key] |
| | if (value) { |
| | return matter(value).content |
| | } |
| | } else { |
| | console.warn(`Unable to find variables Yaml file ${fullPath.join(path.sep)}`) |
| | } |
| | return undefined |
| | } |
| |
|
| | if (first === 'reusables') { |
| | const nakedname = split.pop()! |
| | fullPath.push(...split) |
| | fullPath.push(`${nakedname}.md`) |
| | const markdown = getMarkdownContent(dir, fullPath.join(path.sep), englishRoot) |
| | let { content } = matter(markdown) |
| | if (dir !== englishRoot) { |
| | |
| | |
| | |
| | let englishContent = content |
| | try { |
| | englishContent = getMarkdownContent(englishRoot, fullPath.join(path.sep), englishRoot) |
| | } catch (error) { |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | if ((error as FileSystemError).code !== 'ENOENT') { |
| | throw error |
| | } |
| | } |
| | content = correctTranslatedContentStrings(content, englishContent, { |
| | dottedPath, |
| | code: langCode, |
| | }) |
| | } |
| | return content |
| | } |
| |
|
| | |
| | if (first === 'ui') { |
| | const basename = split.shift() |
| | fullPath.push(`${basename}.yml`) |
| | const allData = getYamlContent(dir, fullPath.join(path.sep), englishRoot) |
| | return get(allData, split.join('.')) |
| | } |
| |
|
| | if (first === 'product-examples' || first === 'glossaries' || first === 'release-notes') { |
| | const basename = split.pop()! |
| | fullPath.push(...split) |
| | fullPath.push(`${basename}.yml`) |
| | return getYamlContent(dir, fullPath.join(path.sep), englishRoot) |
| | } |
| |
|
| | if (first === 'learning-tracks') { |
| | const key = split.pop()! |
| | const basename = split.pop()! |
| | fullPath.push(...split) |
| | fullPath.push(`${basename}.yml`) |
| | const allData = getYamlContent(dir, fullPath.join(path.sep), englishRoot) |
| | return key ? allData[key] : undefined |
| | } |
| |
|
| | throw new Error(`Can't find the key '${dottedPath}' in the scope.`) |
| | } |
| |
|
| | function getSmartSplit(dottedPath: string): string[] { |
| | const split = dottedPath.split('.') |
| | const bits = [] |
| | for (let i = 0, len = split.length; i < len; i++) { |
| | const bit = split[i] |
| | if (i === len - 1) { |
| | bits.push(bit) |
| | } else { |
| | const next = split[i + 1] |
| | if (/\d$/.test(bit) && /^\d/.test(next)) { |
| | bits.push([bit, next].join('.')) |
| | i++ |
| | } else { |
| | bits.push(bit) |
| | } |
| | } |
| | } |
| | return bits |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | const getYamlContent = memoize( |
| | (root: string | undefined, relPath: string, englishRoot?: string): any => { |
| | |
| | |
| | |
| | |
| | if (ALWAYS_ENGLISH_YAML_FILES.has(relPath)) { |
| | |
| | |
| | |
| | root = englishRoot |
| | } |
| | const fileContent = getFileContent(root, relPath, englishRoot) |
| | return yaml.load(fileContent, { filename: relPath }) |
| | }, |
| | ) |
| |
|
| | |
| | const getMarkdownContent = memoize( |
| | (root: string | undefined, relPath: string, englishRoot?: string): string => { |
| | |
| | |
| | |
| | |
| | if (ALWAYS_ENGLISH_MD_FILES.has(relPath)) { |
| | root = englishRoot |
| | } |
| |
|
| | const fileContent = getFileContent(root, relPath, englishRoot) |
| | return matter(fileContent).content.trimEnd() |
| | }, |
| | ) |
| |
|
| | const getFileContent = ( |
| | root: string | undefined, |
| | relPath: string, |
| | englishRoot?: string, |
| | ): string => { |
| | const filePath = root ? path.join(root, relPath) : relPath |
| | if (DEBUG_JIT_DATA_READS) console.log('READ', filePath) |
| | try { |
| | return fs.readFileSync(filePath, 'utf-8') |
| | } catch (err) { |
| | |
| | |
| | if ((err as FileSystemError).code === 'ENOENT') { |
| | |
| | |
| | if (englishRoot && root !== englishRoot) { |
| | |
| | return getFileContent(englishRoot, relPath, englishRoot) |
| | } |
| | } |
| | throw err |
| | } |
| | } |
| |
|
| | function memoize<T extends (...args: any[]) => any>(func: T): T { |
| | const cache = new Map<string, any>() |
| | return ((...args: any[]) => { |
| | if (process.env.NODE_ENV === 'development') { |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | return func(...args) |
| | } |
| |
|
| | const key = args.join(':') |
| | if (!cache.has(key)) { |
| | cache.set(key, func(...args)) |
| | } |
| | const value = cache.get(key) |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | if (Array.isArray(value)) return [...value] |
| | if (typeof value === 'object') return { ...value } |
| | return value |
| | }) as T |
| | } |
| |
|