diff --git a/.github/workflows/upload_sources_to_crowdin.yml b/.github/workflows/upload_sources_to_crowdin.yml index bf58b437..2ab0d8ab 100644 --- a/.github/workflows/upload_sources_to_crowdin.yml +++ b/.github/workflows/upload_sources_to_crowdin.yml @@ -2,10 +2,48 @@ name: Upload sources to Crowdin on: workflow_dispatch: + inputs: + paths_to_upload: + description: Optional docs-relative paths, comma or newline separated. + required: false + type: string jobs: upload-sources-to-crowdin: runs-on: ubuntu-latest + steps: - - name: Placeholder - run: echo "No sync steps implemented yet." + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + + - name: Setup Node.JS + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e + with: + node-version: 24 + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Generate docs structure manifest + run: npm run generate_doc_structure + + - name: Generate crowdin file + id: extract_files + env: + PATHS_TO_UPLOAD: ${{ github.event.inputs.paths_to_upload || '' }} + run: npm run generate_file + + - name: crowdin action + uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 + with: + upload_sources: true + upload_translations: false + auto_approve_imported: true + download_translations: false + create_pull_request: false + base_url: 'https://pagopa.crowdin.com' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} + CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..b512c09d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..931a54c6 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,265 @@ +{ + "name": "pagopatranslationspoc", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "pagopatranslationspoc", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "js-yaml": "^4.1.1" + }, + "devDependencies": { + "@types/js-yaml": "^4.0.9", + "@types/node": "^25.2.3", + "ts-node": "^10.9.2", + "typescript": "^5.9.3" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.2.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz", + "integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..f2cf2764 --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "devportal-docs", + "version": "1.0.0", + "description": "Devportal Docs", + "directories": { + "doc": "docs" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "generate_doc_structure": "ts-node-script --project tsconfig.json src/generateDocStructure.ts", + "generate_file": "ts-node-script --project tsconfig.json src/generateCrowdinConfig.ts" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/pagopa/devportal-docs.git" + }, + "dependencies": { + "js-yaml": "^4.1.1" + }, + "devDependencies": { + "@types/js-yaml": "^4.0.9", + "@types/node": "^25.2.3", + "ts-node": "^10.9.2", + "typescript": "^5.9.3" + } +} diff --git a/src/README.md b/src/README.md new file mode 100644 index 00000000..eeb397f3 --- /dev/null +++ b/src/README.md @@ -0,0 +1,36 @@ +# ita_documentation + +## Translation manifest + +Before uploading sources to Crowdin, the workflow now generates a `docs-structure.json` manifest. + +The manifest is meant to be translated together with the markdown sources and later consumed by the downstream repository to rename translated directories and files. + +### Schema + +The schema keeps the source folder and file names in object keys so they remain stable across locales. Only the `label` values are meant to be translated. + +Directory nodes expose: + +- `label`: the source name to translate. +- `directory`: `true`. +- `children`: nested directory and file entries. + +File nodes expose: + +- `label`: the source basename to translate. +- `directory`: `false`. + +### Scripts + +- `npm run generate_doc_structure`: generates `docs-structure.json`. +- `npm run generate_file`: regenerates `crowdin.yml`, including the manifest upload entry. + +### Workflow dispatch input + +When manually running `.github/workflows/upload_sources_to_crowdin.yml`, you can optionally set `paths_to_upload` to limit the uploaded markdown files. + +- Paths are resolved from `docs/` by default, so `app-io/manuale-gruppi-io/1.0` expands to every `.md` file under `docs/app-io/manuale-gruppi-io/1.0`. +- Explicit markdown files are still supported, with or without the `docs/` prefix. +- Provide one path per line or a comma-separated list. +- `.gitbook` directories are ignored. diff --git a/src/docsStructure.ts b/src/docsStructure.ts new file mode 100644 index 00000000..34e35184 --- /dev/null +++ b/src/docsStructure.ts @@ -0,0 +1,226 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +export const DOCS_DIR = 'docs'; +export const CONFIG_FILE = 'crowdin.yml'; +export const DOCS_STRUCTURE_FILE = 'docs-structure.json'; +export const DOCS_STRUCTURE_TRANSLATION_PATH = `${DOCS_DIR}/%locale%/_meta/docs-structure.json`; + +const IGNORED_DIRECTORY_NAMES = new Set(['.gitbook']); + +export interface CrowdinFileEntry { + source: string; + translation: string; +} + +export interface DocsStructureNode { + label: string; + directory: boolean; + children?: Record; +} + +export interface DocsStructureManifest { + version: number; + tree: Record; +} + +function toPosixPath(filePath: string): string { + return filePath.replace(/\\/g, '/'); +} + +function isMarkdownFile(entryName: string): boolean { + return path.extname(entryName) === '.md'; +} + +function shouldIgnoreDirectory(entryName: string): boolean { + return IGNORED_DIRECTORY_NAMES.has(entryName); +} + +function isPathWithinRoot(rootPath: string, candidatePath: string): boolean { + const relativePath = path.relative(rootPath, candidatePath); + return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath)); +} + +function toCrowdinSourcePath(entryPath: string, rootDir: string): string { + const rootPath = path.resolve(rootDir); + const absoluteEntryPath = path.resolve(entryPath); + const relativePath = toPosixPath(path.relative(rootPath, absoluteEntryPath)); + return toPosixPath(path.join(DOCS_DIR, relativePath)); +} + +function normalizeSelectedPath(selectedPath: string): string { + const normalizedPath = toPosixPath(selectedPath.trim()).replace(/^\.\//, ''); + + if (normalizedPath === DOCS_DIR) { + return ''; + } + + if (normalizedPath.startsWith(`${DOCS_DIR}/`)) { + return normalizedPath.slice(DOCS_DIR.length + 1); + } + + return normalizedPath; +} + +function includesIgnoredDirectory(candidatePath: string): boolean { + const segments = toPosixPath(candidatePath).split('/'); + return segments.some((segment) => IGNORED_DIRECTORY_NAMES.has(segment)); +} + +function listDirectoryEntries(dirPath: string): fs.Dirent[] { + return fs + .readdirSync(dirPath, { withFileTypes: true }) + .sort((left, right) => left.name.localeCompare(right.name)); +} + +function collectMarkdownFilesFromDirectory( + dirPath: string, + rootDir: string, + collectedFiles: Set, +) { + for (const entry of listDirectoryEntries(dirPath)) { + if (entry.isDirectory()) { + if (shouldIgnoreDirectory(entry.name)) { + continue; + } + + collectMarkdownFilesFromDirectory(path.join(dirPath, entry.name), rootDir, collectedFiles); + continue; + } + + if (!entry.isFile() || !isMarkdownFile(entry.name)) { + continue; + } + + collectedFiles.add(toCrowdinSourcePath(path.join(dirPath, entry.name), rootDir)); + } +} + +function buildDirectoryNode( + dirPath: string, + rootDir: string, + mdFiles: string[], +): DocsStructureNode { + const children: Record = {}; + + for (const entry of listDirectoryEntries(dirPath)) { + const entryPath = path.join(dirPath, entry.name); + + if (entry.isDirectory()) { + if (shouldIgnoreDirectory(entry.name)) { + continue; + } + + children[entry.name] = buildDirectoryNode(entryPath, rootDir, mdFiles); + continue; + } + + if (!entry.isFile() || !isMarkdownFile(entry.name)) { + continue; + } + + mdFiles.push(toCrowdinSourcePath(entryPath, rootDir)); + children[entry.name] = { + label: path.basename(entry.name, '.md').replace(/[-_]+/g, ' '), + directory: false, + }; + } + + return { + label: path.basename(dirPath).replace(/[-_]+/g, ' '), + directory: true, + children, + }; +} + +export function collectDocsData(rootDir: string = DOCS_DIR): { + manifest: DocsStructureManifest; + mdFiles: string[]; +} { + if (!fs.existsSync(rootDir)) { + throw new Error(`The directory "${rootDir}" does not exist.`); + } + + const mdFiles: string[] = []; + const rootName = path.basename(rootDir); + const rootNode = buildDirectoryNode(rootDir, rootDir, mdFiles); + + return { + manifest: { + version: 1, + tree: { + [rootName]: rootNode, + }, + }, + mdFiles, + }; +} + +export function collectSelectedMarkdownFiles( + selectedPaths: string[], + rootDir: string = DOCS_DIR, +): string[] { + if (!fs.existsSync(rootDir)) { + throw new Error(`The directory "${rootDir}" does not exist.`); + } + + const rootPath = path.resolve(rootDir); + const collectedFiles = new Set(); + + for (const rawSelectedPath of selectedPaths) { + const normalizedPath = normalizeSelectedPath(rawSelectedPath); + + if (!normalizedPath || includesIgnoredDirectory(normalizedPath)) { + continue; + } + + const absoluteSelectedPath = path.resolve(rootPath, normalizedPath); + + if (!isPathWithinRoot(rootPath, absoluteSelectedPath)) { + throw new Error(`The path "${rawSelectedPath}" must stay within "${DOCS_DIR}/".`); + } + + if (!fs.existsSync(absoluteSelectedPath)) { + throw new Error(`The path "${rawSelectedPath}" does not exist within "${DOCS_DIR}/".`); + } + + const selectedPathStat = fs.statSync(absoluteSelectedPath); + + if (selectedPathStat.isDirectory()) { + collectMarkdownFilesFromDirectory(absoluteSelectedPath, rootDir, collectedFiles); + continue; + } + + if (!selectedPathStat.isFile() || !isMarkdownFile(path.basename(absoluteSelectedPath))) { + throw new Error(`The path "${rawSelectedPath}" must be a Markdown file or a directory.`); + } + + collectedFiles.add(toCrowdinSourcePath(absoluteSelectedPath, rootDir)); + } + + return Array.from(collectedFiles).sort((left, right) => left.localeCompare(right)); +} + +export function buildCrowdinFileEntries(mdFiles: string[]): CrowdinFileEntry[] { + const markdownEntries = mdFiles.map((sourcePath) => ({ + source: sourcePath, + translation: sourcePath.replace(`${DOCS_DIR}/`, `${DOCS_DIR}/%locale%/`), + })); + + return [ + ...markdownEntries, + { + source: DOCS_STRUCTURE_FILE, + translation: DOCS_STRUCTURE_TRANSLATION_PATH, + }, + ]; +} + +export function writeDocsStructureManifest( + outputPath: string = DOCS_STRUCTURE_FILE, + rootDir: string = DOCS_DIR, +): DocsStructureManifest { + const { manifest } = collectDocsData(rootDir); + fs.writeFileSync(outputPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8'); + return manifest; +} diff --git a/src/generateCrowdinConfig.ts b/src/generateCrowdinConfig.ts new file mode 100644 index 00000000..33b63eb8 --- /dev/null +++ b/src/generateCrowdinConfig.ts @@ -0,0 +1,107 @@ + +import * as fs from 'fs'; +import * as yaml from 'js-yaml'; +import * as os from 'os'; +import { + buildCrowdinFileEntries, + collectDocsData, + collectSelectedMarkdownFiles, + CONFIG_FILE, + DOCS_DIR, + type CrowdinFileEntry, +} from './docsStructure'; + +interface CrowdinConfig { + base_path?: string; + preserve_hierarchy?: boolean; + files: CrowdinFileEntry[]; +} + +function parseRequestedDocsPaths(rawValue: string | undefined): string[] { + if (!rawValue) { + return []; + } + + const trimmedValue = rawValue.trim(); + + if (!trimmedValue) { + return []; + } + + try { + const parsedValue = JSON.parse(trimmedValue) as unknown; + + if (Array.isArray(parsedValue)) { + return parsedValue + .map((entry) => `${entry}`.trim()) + .filter((entry) => entry.length > 0); + } + } catch { + // Fall back to simple text parsing for workflow_dispatch input values. + } + + return trimmedValue + .split(/\r?\n|,/) + .map((entry) => entry.trim()) + .filter((entry) => entry.length > 0); +} + +function generateCrowdinConfig() { + console.log(`🔍 Scanning the "${DOCS_DIR}" directory...`); + + if (!fs.existsSync(DOCS_DIR)) { + console.error(`❌ Error: The directory "${DOCS_DIR}" does not exist.`); + process.exit(1); + } + + const pathsToUpload = parseRequestedDocsPaths(process.env.PATHS_TO_UPLOAD); + const mdFiles = pathsToUpload.length > 0 + ? collectSelectedMarkdownFiles(pathsToUpload) + : collectDocsData().mdFiles; + + if (pathsToUpload.length > 0) { + console.log(`đŸŽ¯ Limiting the upload to ${pathsToUpload.length} selected path(s).`); + } + + if (mdFiles.length === 0) { + console.warn(`âš ī¸ No .md files found in "${DOCS_DIR}".`); + } + + const files: CrowdinFileEntry[] = buildCrowdinFileEntries(mdFiles); + + const nextConfig: CrowdinConfig = { + base_path: '.', + preserve_hierarchy: true, + files, + }; + + try { + const serializedConfig = yaml.dump(nextConfig, { + lineWidth: -1, + noRefs: true, + sortKeys: false, + }); + fs.writeFileSync(CONFIG_FILE, serializedConfig, 'utf8'); + console.log(`✅ Updated ${CONFIG_FILE}.`); + } catch (error) { + console.error('❌ Error saving crowdin.yml.', error); + process.exit(1); + } + + const githubOutputPath = process.env.GITHUB_OUTPUT; + + if (githubOutputPath) { + const pathsJson = JSON.stringify(mdFiles); + + try { + fs.appendFileSync(githubOutputPath, `found_files=${pathsJson}${os.EOL}`); + console.log("🚀 Sent the 'found_files' output to GitHub Actions."); + } catch (error) { + console.error('❌ Unable to write to GITHUB_OUTPUT:', error); + } + } else { + console.log('â„šī¸ GITHUB_OUTPUT not detected (are you running locally?). Skipping this step.'); + } +} + +generateCrowdinConfig(); diff --git a/src/generateDocStructure.ts b/src/generateDocStructure.ts new file mode 100644 index 00000000..26bf6cb9 --- /dev/null +++ b/src/generateDocStructure.ts @@ -0,0 +1,30 @@ +import * as fs from 'fs'; +import { + DOCS_DIR, + DOCS_STRUCTURE_FILE, + writeDocsStructureManifest, +} from './docsStructure'; + +function generateDocStructure() { + console.log(`🔍 Generating a manifest for the "${DOCS_DIR}" directory...`); + + if (!fs.existsSync(DOCS_DIR)) { + console.error(`❌ Error: The directory "${DOCS_DIR}" does not exist.`); + process.exit(1); + } + + let manifest: ReturnType; + try { + manifest = writeDocsStructureManifest(); + } catch (error) { + console.error('❌ Error while generating the manifest:', error); + process.exit(1); + } + console.log(`✅ Updated ${DOCS_STRUCTURE_FILE}.`); + + if (!manifest.tree.docs) { + console.warn('âš ī¸ Generated manifest without a "docs" root node.'); + } +} + +generateDocStructure(); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..0f881e47 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "Node16", + "moduleResolution": "Node16", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "types": ["node"] + }, + "include": [ + "*.ts", + "src/docsStructure.ts", + "src/generateCrowdinConfig.ts", + "src/generateDocStructure.ts" + ] +}