trans-spec 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +547 -0
- package/cli/index.js +329 -0
- package/cli/package-lock.json +278 -0
- package/cli/package.json +22 -0
- package/cli/src/auth.js +98 -0
- package/cli/src/config.js +70 -0
- package/cli/src/setup.js +39 -0
- package/cli/src/translate.js +71 -0
- package/package.json +47 -0
- package/viewer/README.md +16 -0
- package/viewer/eslint.config.js +29 -0
- package/viewer/index.html +12 -0
- package/viewer/package-lock.json +14016 -0
- package/viewer/package.json +35 -0
- package/viewer/public/trans-spec/i18n/en/api2.yaml +130 -0
- package/viewer/public/trans-spec/i18n/es/api2.yaml +137 -0
- package/viewer/public/trans-spec/i18n.json +17 -0
- package/viewer/public/trans-spec/i18n.lock +67 -0
- package/viewer/public/trans-spec/index.json +8 -0
- package/viewer/src/App.jsx +217 -0
- package/viewer/src/components/EndpointDetail.jsx +290 -0
- package/viewer/src/components/LanguageSwitcher.jsx +25 -0
- package/viewer/src/components/Sidebar.jsx +88 -0
- package/viewer/src/index.css +1 -0
- package/viewer/src/main.jsx +14 -0
- package/viewer/src/polyfills.js +2 -0
- package/viewer/src/utils/specLoader.js +79 -0
- package/viewer/vite.config.js +21 -0
package/cli/src/auth.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { execSync, spawn } from "child_process";
|
|
2
|
+
import ora from "ora";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
|
|
5
|
+
const MAX_RETRIES = 2;
|
|
6
|
+
|
|
7
|
+
function wait(ms) {
|
|
8
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function isAuthenticated() {
|
|
12
|
+
try {
|
|
13
|
+
const output = execSync("npx lingo.dev@latest auth 2>&1", {
|
|
14
|
+
encoding: "utf-8",
|
|
15
|
+
stdio: "pipe",
|
|
16
|
+
});
|
|
17
|
+
console.log("Auth output:", output);
|
|
18
|
+
return output.includes("Authenticated as");
|
|
19
|
+
} catch (err) {
|
|
20
|
+
console.log("Auth error:", err.stderr || err.stdout || err.message);
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function triggerLogin() {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const spinner = ora("Opening browser for authentication...").start();
|
|
28
|
+
|
|
29
|
+
const login = spawn("npx", ["lingo.dev@latest", "login"], {
|
|
30
|
+
stdio: "inherit",
|
|
31
|
+
shell: true,
|
|
32
|
+
windowsHide: false,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
login.on("close", (code) => {
|
|
36
|
+
if (code === 0) {
|
|
37
|
+
spinner.succeed("Login complete");
|
|
38
|
+
resolve();
|
|
39
|
+
} else {
|
|
40
|
+
spinner.fail("Login failed");
|
|
41
|
+
reject(new Error("Login process exited with code " + code));
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
login.on("error", (err) => {
|
|
46
|
+
spinner.fail("Login failed");
|
|
47
|
+
reject(err);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function checkAuth() {
|
|
53
|
+
const spinner = ora(
|
|
54
|
+
"Checking authentication (downloading dependencies on first run, this may take a minute)...",
|
|
55
|
+
).start();
|
|
56
|
+
|
|
57
|
+
// Already authenticated, no need to login
|
|
58
|
+
if (isAuthenticated()) {
|
|
59
|
+
spinner.succeed(chalk.green("Authenticated"));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
spinner.stop();
|
|
64
|
+
|
|
65
|
+
// Not authenticated, trigger login once
|
|
66
|
+
try {
|
|
67
|
+
await triggerLogin();
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.log(chalk.red("✖ Login failed: " + err.message));
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// After login, wait and retry auth check up to MAX_RETRIES times
|
|
74
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
75
|
+
await wait(2000); // wait 2 seconds before checking
|
|
76
|
+
|
|
77
|
+
if (isAuthenticated()) {
|
|
78
|
+
console.log(chalk.green("✔ Successfully authenticated"));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (attempt < MAX_RETRIES) {
|
|
83
|
+
console.log(
|
|
84
|
+
chalk.yellow(
|
|
85
|
+
`Auth check failed. Retrying (${attempt}/${MAX_RETRIES})...`,
|
|
86
|
+
),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
console.log(chalk.red("✖ Authentication failed after login."));
|
|
92
|
+
console.log(
|
|
93
|
+
chalk.white(
|
|
94
|
+
"Please run: npx lingo.dev@latest login manually and try again.",
|
|
95
|
+
),
|
|
96
|
+
);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import ora from "ora";
|
|
5
|
+
|
|
6
|
+
const TRANSSPEC_DIR = ".trans-spec";
|
|
7
|
+
const CONFIG_PATH = path.join(TRANSSPEC_DIR, "i18n.json");
|
|
8
|
+
|
|
9
|
+
export async function generateConfig(languages, source = "en") {
|
|
10
|
+
const spinner = ora("Generating config...").start();
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
// Handles both comma and space separated: "es,fr,de" or "es fr de"
|
|
14
|
+
// Parse new languages if provided
|
|
15
|
+
let newTargets = [];
|
|
16
|
+
if (languages) {
|
|
17
|
+
newTargets = languages
|
|
18
|
+
.split(/[\s,]+/)
|
|
19
|
+
.map((lang) => lang.trim())
|
|
20
|
+
.filter(Boolean);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Check if config already exists
|
|
24
|
+
let existingTargets = [];
|
|
25
|
+
if (fs.existsSync(CONFIG_PATH)) {
|
|
26
|
+
const existingConfig = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
|
|
27
|
+
existingTargets = existingConfig.locale?.targets || [];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Merge: existing + new, remove duplicates
|
|
31
|
+
const allTargets = [...new Set([...existingTargets, ...newTargets])];
|
|
32
|
+
|
|
33
|
+
if (allTargets.length === 0) {
|
|
34
|
+
spinner.fail(chalk.red("No target languages provided"));
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const config = {
|
|
39
|
+
$schema: "https://lingo.dev/schema/i18n.json",
|
|
40
|
+
version: "1.12",
|
|
41
|
+
locale: {
|
|
42
|
+
source: source,
|
|
43
|
+
targets: allTargets,
|
|
44
|
+
},
|
|
45
|
+
buckets: {
|
|
46
|
+
yaml: {
|
|
47
|
+
include: ["i18n/[locale]/*.yaml"],
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
53
|
+
|
|
54
|
+
if (newTargets.length > 0) {
|
|
55
|
+
spinner.succeed(
|
|
56
|
+
chalk.green(`Config updated: ${source} → ${allTargets.join(", ")}`),
|
|
57
|
+
);
|
|
58
|
+
} else {
|
|
59
|
+
spinner.succeed(
|
|
60
|
+
chalk.green(
|
|
61
|
+
`Using existing config: ${source} → ${allTargets.join(", ")}`,
|
|
62
|
+
),
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
return allTargets;
|
|
66
|
+
} catch (err) {
|
|
67
|
+
spinner.fail(chalk.red("Config generation failed: " + err.message));
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
}
|
package/cli/src/setup.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import ora from "ora";
|
|
5
|
+
|
|
6
|
+
const TRANSSPEC_DIR = ".trans-spec";
|
|
7
|
+
const I18N_DIR = path.join(TRANSSPEC_DIR, "i18n");
|
|
8
|
+
|
|
9
|
+
export async function setup(specPath, sourceLanguage) {
|
|
10
|
+
const spinner = ora("Setting up project...").start();
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const resolvedSpecPath = path.resolve(specPath);
|
|
14
|
+
|
|
15
|
+
if (!fs.existsSync(resolvedSpecPath)) {
|
|
16
|
+
spinner.fail(chalk.red(`Spec file not found: ${specPath}`));
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Use source language folder
|
|
21
|
+
const sourceDir = path.join(I18N_DIR, sourceLanguage);
|
|
22
|
+
|
|
23
|
+
if (!fs.existsSync(TRANSSPEC_DIR)) {
|
|
24
|
+
fs.mkdirSync(sourceDir, { recursive: true });
|
|
25
|
+
spinner.text = "Created .trans-spec folder structure";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Preserve the original filename
|
|
29
|
+
const originalFilename = path.basename(resolvedSpecPath);
|
|
30
|
+
const destPath = path.join(sourceDir, originalFilename);
|
|
31
|
+
|
|
32
|
+
fs.copyFileSync(resolvedSpecPath, destPath);
|
|
33
|
+
|
|
34
|
+
spinner.succeed(chalk.green("Project setup complete"));
|
|
35
|
+
} catch (err) {
|
|
36
|
+
spinner.fail(chalk.red("Setup failed: " + err.message));
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import ora from "ora";
|
|
4
|
+
import path from "path";
|
|
5
|
+
|
|
6
|
+
const TRANSSPEC_DIR = ".trans-spec";
|
|
7
|
+
const MAX_RETRIES = 2;
|
|
8
|
+
|
|
9
|
+
async function runTranslation() {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
let output = "";
|
|
12
|
+
let hasErrors = false;
|
|
13
|
+
|
|
14
|
+
const process = spawn("npx", ["lingo.dev@latest", "run"], {
|
|
15
|
+
cwd: path.resolve(TRANSSPEC_DIR),
|
|
16
|
+
shell: true,
|
|
17
|
+
stdio: "pipe",
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Capture stdout
|
|
21
|
+
process.stdout.on("data", (data) => {
|
|
22
|
+
const text = data.toString();
|
|
23
|
+
output += text;
|
|
24
|
+
console.log(text);
|
|
25
|
+
// Check for failure indicators
|
|
26
|
+
if (text.includes("❌") || text.includes("[Failed Files]")) {
|
|
27
|
+
hasErrors = true;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
process.on("close", (code) => {
|
|
32
|
+
if (code !== 0 || hasErrors) {
|
|
33
|
+
reject(new Error("Translation had errors or failures"));
|
|
34
|
+
} else {
|
|
35
|
+
resolve();
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
process.on("error", (err) => {
|
|
40
|
+
reject(err);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function translate(targets) {
|
|
46
|
+
const spinner = ora("Translating...").start();
|
|
47
|
+
spinner.stop();
|
|
48
|
+
|
|
49
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
50
|
+
try {
|
|
51
|
+
await runTranslation();
|
|
52
|
+
console.log(chalk.green("\n✔ Translation complete"));
|
|
53
|
+
return;
|
|
54
|
+
} catch (err) {
|
|
55
|
+
if (attempt < MAX_RETRIES) {
|
|
56
|
+
console.log(
|
|
57
|
+
chalk.yellow(
|
|
58
|
+
`Translation failed. Retrying (${attempt}/${MAX_RETRIES})...`,
|
|
59
|
+
),
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.log(chalk.red("Translation failed after 2 attempts."));
|
|
66
|
+
// console.log(chalk.red("Check the errors above for details."));
|
|
67
|
+
console.log(
|
|
68
|
+
chalk.white("Please try running manually: npx lingo.dev@latest run"),
|
|
69
|
+
);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "trans-spec",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Translate your OpenAPI specification into any language and generate a multilingual documentation site in seconds",
|
|
5
|
+
"main": "cli/index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"No tests yet\" && exit 0"
|
|
8
|
+
},
|
|
9
|
+
"bin": {
|
|
10
|
+
"trans-spec": "./cli/index.js"
|
|
11
|
+
},
|
|
12
|
+
"type": "module",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"api",
|
|
15
|
+
"documentation",
|
|
16
|
+
"openapi",
|
|
17
|
+
"swagger",
|
|
18
|
+
"translation",
|
|
19
|
+
"i18n",
|
|
20
|
+
"multilingual",
|
|
21
|
+
"ai"
|
|
22
|
+
],
|
|
23
|
+
"author": "Ademola Thompson",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "https://github.com/ade555/trans-spec.git"
|
|
28
|
+
},
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/ade555/trans-spec/issues"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/ade555/trans-spec#readme",
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18.0.0"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"cli",
|
|
38
|
+
"viewer",
|
|
39
|
+
"README.md",
|
|
40
|
+
"LICENSE"
|
|
41
|
+
],
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"chalk": "^5.6.2",
|
|
44
|
+
"commander": "^14.0.3",
|
|
45
|
+
"ora": "^9.3.0"
|
|
46
|
+
}
|
|
47
|
+
}
|
package/viewer/README.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# React + Vite
|
|
2
|
+
|
|
3
|
+
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
|
4
|
+
|
|
5
|
+
Currently, two official plugins are available:
|
|
6
|
+
|
|
7
|
+
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
|
|
8
|
+
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
|
9
|
+
|
|
10
|
+
## React Compiler
|
|
11
|
+
|
|
12
|
+
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
|
13
|
+
|
|
14
|
+
## Expanding the ESLint configuration
|
|
15
|
+
|
|
16
|
+
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import js from '@eslint/js'
|
|
2
|
+
import globals from 'globals'
|
|
3
|
+
import reactHooks from 'eslint-plugin-react-hooks'
|
|
4
|
+
import reactRefresh from 'eslint-plugin-react-refresh'
|
|
5
|
+
import { defineConfig, globalIgnores } from 'eslint/config'
|
|
6
|
+
|
|
7
|
+
export default defineConfig([
|
|
8
|
+
globalIgnores(['dist']),
|
|
9
|
+
{
|
|
10
|
+
files: ['**/*.{js,jsx}'],
|
|
11
|
+
extends: [
|
|
12
|
+
js.configs.recommended,
|
|
13
|
+
reactHooks.configs.flat.recommended,
|
|
14
|
+
reactRefresh.configs.vite,
|
|
15
|
+
],
|
|
16
|
+
languageOptions: {
|
|
17
|
+
ecmaVersion: 2020,
|
|
18
|
+
globals: globals.browser,
|
|
19
|
+
parserOptions: {
|
|
20
|
+
ecmaVersion: 'latest',
|
|
21
|
+
ecmaFeatures: { jsx: true },
|
|
22
|
+
sourceType: 'module',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
rules: {
|
|
26
|
+
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
])
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Trans-Spec API Docs viewer</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/main.jsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|