diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 761550d..27a3185 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,13 +20,13 @@ jobs: - name: Every script parses run: | - for f in scripts/*.mjs scripts/providers/*.mjs; do + for f in scripts/*.mjs scripts/providers/*.mjs bin/*.mjs; do node --check "$f" || { echo "::error file=$f::syntax error"; exit 1; } done bash -n scripts/finalize.sh echo "all scripts parse" - - name: Provider contract tests + - name: Contract tests (providers + CLI) run: npm test - name: Example config is valid JSON @@ -46,6 +46,18 @@ jobs: fi echo "no key material found" + - name: Packaged CLI installs and scaffolds + run: | + npm pack --pack-destination /tmp + mkdir -p /tmp/cli-smoke && cd /tmp/cli-smoke + npm init -y > /dev/null + npm install --no-audit --no-fund --save-dev /tmp/claude-translator-*.tgz + ./node_modules/.bin/claude-translator init + test -f scripts/i18n/extract.mjs || { echo "::error::init did not scaffold"; exit 1; } + test -f i18n.config.json || { echo "::error::init wrote no config"; exit 1; } + node -e "JSON.parse(require('fs').readFileSync('i18n.config.json','utf8'))" + echo "packaged CLI works" + - name: Config loader smoke test run: | mkdir -p /tmp/fixture/dist/about /tmp/fixture/dist/pricing diff --git a/CHANGELOG.md b/CHANGELOG.md index f7710e5..ecaef9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,29 @@ All notable changes to this project are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.3.0] — 2026-08-25 + +### Added + +- **`npx claude-translator init`** — a scaffolder that replaces the four manual install + steps. Copies the pipeline into `scripts/i18n/` (or `--dir`), writes `i18n.config.json`, + declares `parse5`, and appends the derived `i18n/` paths to `.gitignore`. It never + overwrites without `--force` and prints every path it touched +- `--help`, `--version`, `--dir`, `--force` +- **12 CLI contract tests** (`npm test`) covering the refusal to install outside a Node + project, idempotency, `--force`, `--dir`, appending rather than replacing an existing + `.gitignore`, and that `package.json` "files" actually ships what `init` copies +- CI now packs the tarball, installs it into a scratch project and runs `init`, so a + broken published package fails the build rather than a user's first command + +### Changed + +- `parse5` moved from `dependencies` to `devDependencies`. Nothing in the published + package needs it at runtime — the CLI only copies files — so `npx` no longer downloads + it. A clone plus `npm install` is unaffected +- `package.json` gained a `files` allowlist, so the tarball carries the scripts, + references, `SKILL.md` and the example config, and nothing else + ## [1.2.0] — 2026-08-25 Renamed to **Claude Translator**, and the translation step is no longer tied to one vendor. @@ -106,6 +129,7 @@ into dozens of languages. and cost, and adapting to other static site generators - `SKILL.md`, so the repository can be installed directly as a Claude Code skill +[1.3.0]: https://github.com/ConveyThis/claude-translator/releases/tag/v1.3.0 [1.2.0]: https://github.com/ConveyThis/claude-translator/releases/tag/v1.2.0 [1.1.0]: https://github.com/ConveyThis/claude-translator/releases/tag/v1.1.0 [1.0.0]: https://github.com/ConveyThis/claude-translator/releases/tag/v1.0.0 diff --git a/README.md b/README.md index 0cd8832..b9e5092 100644 --- a/README.md +++ b/README.md @@ -99,16 +99,15 @@ The builder restores the original tags by index. ```bash # 1. Install into your project -git clone https://github.com/ConveyThis/claude-translator.git -cp -r claude-translator/scripts /scripts/i18n -cd && npm install --save-dev parse5 +cd +npx claude-translator init +npm install # parse5, the only dependency -# 2. Configure -cp ../claude-translator/i18n.config.example.json i18n.config.json +# 2. Configure — baseUrl, locales, provider $EDITOR i18n.config.json -# 3. Provide your key (or skip entirely for a local model) -echo "ANTHROPIC_API_KEY=your-key-here" >> .env # make sure .env is gitignored +# 3. Provide your key (skip entirely for a local model) +echo "ANTHROPIC_API_KEY=your-key-here" >> .env # .gitignore this # 4. Run npm run build # your normal build @@ -121,6 +120,29 @@ node scripts/i18n/audit-seo.mjs # full SEO audit Deploy the resulting build directory exactly as you deploy it today. +`init` copies the pipeline into `scripts/i18n/`, writes a config, declares `parse5`, and adds +the derived `i18n/` paths to `.gitignore`. It **never overwrites anything without `--force`** +and prints every file it touched. `--dir ` puts the scripts somewhere else. + +
+Installing without npx + +```bash +git clone https://github.com/ConveyThis/claude-translator.git +cp -r claude-translator/scripts /scripts/i18n +cp claude-translator/i18n.config.example.json /i18n.config.json +cd && npm install --save-dev parse5 +``` + +Or run the scaffolder straight from the clone: +`node claude-translator/bin/claude-translator.mjs init` + +
+ +The scripts are copied into your project rather than run from `node_modules` on purpose: they +resolve `parse5` and every relative path from the project they live in, they are short enough to +read, and this is AGPL software whose point is that you can change them. + --- ## Configuration @@ -340,8 +362,8 @@ git clone https://github.com/ConveyThis/claude-translator.git \ ~/.claude/skills/claude-translator ``` -Then ask Claude to "localize this site" and it will follow `SKILL.md`, including the -failure modes documented in `references/`. +Then ask Claude to "localize this site". It follows `SKILL.md`, including the failure modes in +`references/` and the routing rules for when this is the wrong tool entirely. --- diff --git a/SKILL.md b/SKILL.md index 7c8339f..2c40955 100644 --- a/SKILL.md +++ b/SKILL.md @@ -14,7 +14,7 @@ argument-hint: "[project-dir]" license: AGPL-3.0 metadata: author: ConveyThis - version: "1.2.0" + version: "1.3.0" category: i18n --- @@ -97,19 +97,21 @@ do not take the same parameters and guessing costs money. ## Setup ```bash -cp ~/.claude/skills/claude-translator/scripts/*.mjs /scripts/ -cp ~/.claude/skills/claude-translator/scripts/*.sh /scripts/ -cp ~/.claude/skills/claude-translator/i18n.config.example.json /i18n.config.json -cd && npm i -D parse5 # the only dependency +cd +node ~/.claude/skills/claude-translator/bin/claude-translator.mjs init +npm install # parse5, the only dependency ``` +That copies the pipeline into `scripts/i18n/`, writes `i18n.config.json`, declares +`parse5` and adds the derived paths to `.gitignore`. It never overwrites without +`--force`, so it is safe to re-run; `--dir ` puts the scripts elsewhere. + Scripts run **from inside the project** so `parse5` and relative paths resolve. Edit `i18n.config.json` — five keys cover everything; see `i18n.config.example.json`. -Add to `.gitignore`: `i18n/source.json`, `i18n/manifest.json`, `i18n/segments/`, -`i18n/tm/*.failures.json`, `i18n/tm/*.review.json`, and `i18n` in `.prettierignore`. -**Commit `i18n/tm/{lang}.json`** — the memory is the asset; losing it means paying for a -full re-translation. +**Commit `i18n/tm/{lang}.json`.** The scaffolder deliberately does not ignore it: the +memory is the asset, and losing it means paying for a full re-translation. Everything +else under `i18n/` is derived and is ignored for you. Add `i18n` to `.prettierignore`. ## Pipeline diff --git a/bin/claude-translator.mjs b/bin/claude-translator.mjs new file mode 100755 index 0000000..8b12df3 --- /dev/null +++ b/bin/claude-translator.mjs @@ -0,0 +1,230 @@ +#!/usr/bin/env node +/** + * claude-translator — project scaffolder. + * + * npx claude-translator init + * + * Does in one command what the README's four manual steps do: copies the pipeline into + * your project, writes a config, adds the one dependency, and tells you what to run next. + * + * ── Why it copies the scripts instead of running them from node_modules ────── + * The scripts are meant to be yours. They resolve `parse5` and every relative path from + * the project they sit in, they are short enough to read, and this is AGPL software whose + * whole point is that you can change it. Vendoring them keeps all of that true. The + * alternative — a black-box binary reaching into your build output — is the thing this + * project exists as an alternative to. + * + * ── The one rule ──────────────────────────────────────────────────────────── + * Never overwrite anything the user has without --force, and print every path touched. + * A scaffolder that silently clobbers a config people have edited is worse than no + * scaffolder at all. + */ + +import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, copyFileSync, statSync } from 'fs'; +import { join, dirname, resolve, relative } from 'path'; +import { fileURLToPath } from 'url'; + +const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const pkg = JSON.parse(readFileSync(join(PKG_ROOT, 'package.json'), 'utf8')); + +// ── Output ─────────────────────────────────────────────────────────────────── + +const c = process.stdout.isTTY + ? { dim: '\x1b[2m', bold: '\x1b[1m', green: '\x1b[32m', yellow: '\x1b[33m', reset: '\x1b[0m' } + : { dim: '', bold: '', green: '', yellow: '', reset: '' }; + +const wrote = []; +const skipped = []; + +const say = (s = '') => console.log(s); +const ok = (path) => wrote.push(path); +const skip = (path, why) => skipped.push(`${path} ${c.dim}(${why})${c.reset}`); + +// ── CLI ────────────────────────────────────────────────────────────────────── + +const HELP = ` +${c.bold}claude-translator${c.reset} ${pkg.version} +Static-site localization: translate a built site into dozens of languages as real +static pages, without re-rendering and without breaking Core Web Vitals. + +${c.bold}USAGE${c.reset} + npx claude-translator init [options] + +${c.bold}OPTIONS${c.reset} + --dir Where to put the pipeline scripts (default: scripts/i18n) + --force Overwrite files that already exist (default: never) + --help, -h Show this + --version, -v Print the version + +${c.bold}AFTER INIT${c.reset} + npm install install parse5, the only dependency + \$EDITOR i18n.config.json set baseUrl, locales, provider + node /extract.mjs find translatable units + node /translate.mjs --lang es,fr translate + node /build-locales.mjs --lang all write the localized pages + node /verify.mjs --lang all six gates + node /audit-seo.mjs full SEO audit + +${c.bold}DOCS${c.reset} https://github.com/ConveyThis/claude-translator +`; + +const argv = process.argv.slice(2); +const has = (...names) => names.some((n) => argv.includes(n)); +const valueOf = (name, fallback) => { + const i = argv.indexOf(name); + return i !== -1 && argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[i + 1] : fallback; +}; + +if (has('--help', '-h') || argv.length === 0) { + say(HELP); + process.exit(0); +} +if (has('--version', '-v')) { + say(pkg.version); + process.exit(0); +} + +const command = argv[0]; +if (command !== 'init') { + console.error(`Unknown command "${command}". Run \`npx claude-translator --help\`.`); + process.exit(1); +} + +const FORCE = has('--force'); +const DIR = valueOf('--dir', 'scripts/i18n'); +const CWD = process.cwd(); + +// ── Guard: is this a project? ──────────────────────────────────────────────── +// Scattering files into whatever directory someone happened to be in is the kind of +// thing people remember about a tool. + +const pkgJsonPath = join(CWD, 'package.json'); +if (!existsSync(pkgJsonPath)) { + console.error(`No package.json in ${CWD} + +claude-translator init installs into a Node project, because the scripts need parse5 +and resolve paths from the project root. Run it from your project, or create one first: + + npm init -y && npx claude-translator init +`); + process.exit(1); +} + +say(`\n${c.bold}claude-translator ${pkg.version}${c.reset} → ${CWD}\n`); + +// ── 1. The pipeline scripts ────────────────────────────────────────────────── + +/** Copy one file, honouring --force, recording what happened. */ +function place(from, to, label = relative(CWD, to)) { + if (existsSync(to) && !FORCE) { + skip(label, 'exists — use --force to replace'); + return; + } + mkdirSync(dirname(to), { recursive: true }); + copyFileSync(from, to); + ok(label); +} + +const targetDir = resolve(CWD, DIR); +const srcDir = join(PKG_ROOT, 'scripts'); + +for (const entry of readdirSync(srcDir)) { + const from = join(srcDir, entry); + if (statSync(from).isDirectory()) { + for (const sub of readdirSync(from)) { + place(join(from, sub), join(targetDir, entry, sub)); + } + continue; + } + if (!/\.(mjs|sh)$/.test(entry)) continue; + place(from, join(targetDir, entry)); +} + +// ── 2. The config ──────────────────────────────────────────────────────────── + +place(join(PKG_ROOT, 'i18n.config.example.json'), join(CWD, 'i18n.config.json'), 'i18n.config.json'); + +// ── 3. The one dependency ──────────────────────────────────────────────────── +// Written into package.json rather than installed here: running npm from inside npx is +// slow, can pick the wrong package manager, and rewrites a lockfile the user did not ask +// us to touch. Editing the manifest and saying "now run install" is the honest version. + +let needsInstall = false; +{ + const manifest = JSON.parse(readFileSync(pkgJsonPath, 'utf8')); + const declared = + manifest.dependencies?.parse5 ?? manifest.devDependencies?.parse5 ?? manifest.peerDependencies?.parse5; + if (declared) { + skip('package.json', `parse5 already declared (${declared})`); + } else { + manifest.devDependencies = { ...(manifest.devDependencies ?? {}), parse5: '^7.3.0' }; + // Keep devDependencies sorted so the diff is one line, not a reshuffle. + manifest.devDependencies = Object.fromEntries(Object.entries(manifest.devDependencies).sort()); + writeFileSync(pkgJsonPath, `${JSON.stringify(manifest, null, 2)}\n`); + ok('package.json (added parse5 to devDependencies)'); + needsInstall = true; + } +} + +// ── 4. .gitignore ──────────────────────────────────────────────────────────── +// The memory files are the asset. Everything else under i18n/ is derived and noisy. + +const GITIGNORE_BLOCK = [ + '', + '# claude-translator — derived files. Do NOT ignore i18n/tm/{lang}.json:', + '# the translation memory is the asset, and losing it means paying to rebuild it.', + 'i18n/source.json', + 'i18n/manifest.json', + 'i18n/segments/', + 'i18n/seo-audit.json', + 'i18n/tm/*.failures.json', + 'i18n/tm/*.review.json', + '', +]; + +{ + const gitignorePath = join(CWD, '.gitignore'); + const existing = existsSync(gitignorePath) ? readFileSync(gitignorePath, 'utf8') : ''; + if (existing.includes('claude-translator — derived files')) { + skip('.gitignore', 'already has the block'); + } else { + writeFileSync(gitignorePath, existing.replace(/\n*$/, '\n') + GITIGNORE_BLOCK.join('\n')); + ok(existing ? '.gitignore (appended)' : '.gitignore'); + } +} + +// ── Report ─────────────────────────────────────────────────────────────────── + +if (wrote.length) { + say(`${c.green}Wrote${c.reset}`); + for (const w of wrote) say(` ${w}`); +} +if (skipped.length) { + say(`\n${c.yellow}Left alone${c.reset}`); + for (const s of skipped) say(` ${s}`); +} + +// Column width is computed, not guessed: --dir changes how long these commands are, and +// a hardcoded pad lets the longest one collide with its own description. +const rows = [ + ...(needsInstall ? [['npm install', 'parse5, the only dependency']] : []), + ['$EDITOR i18n.config.json', 'baseUrl, locales, provider'], + [], + ['npm run build', 'your normal build, source language only'], + [`node ${DIR}/extract.mjs`, 'find translatable units'], + [`node ${DIR}/translate.mjs --lang es,fr`, 'translate (needs a provider key)'], + [`node ${DIR}/build-locales.mjs --lang all`, 'write the localized pages'], + [`node ${DIR}/verify.mjs --lang all`, 'six gates'], + [`node ${DIR}/audit-seo.mjs`, 'full SEO audit'], +]; +const width = Math.max(...rows.filter((r) => r.length).map(([cmd]) => cmd.length)) + 2; + +say(`\n${c.bold}Next${c.reset}`); +for (const row of rows) { + if (!row.length) { say(''); continue; } + const [cmd, note] = row; + say(` ${cmd.padEnd(width)}${c.dim}${note}${c.reset}`); +} + +say(`\n${c.dim}Deploy the build directory exactly as you deploy it today.`); +say(`Docs: https://github.com/ConveyThis/claude-translator${c.reset}\n`); diff --git a/bin/cli.test.mjs b/bin/cli.test.mjs new file mode 100644 index 0000000..0e3669c --- /dev/null +++ b/bin/cli.test.mjs @@ -0,0 +1,165 @@ +/** + * CLI contract tests. + * + * node --test bin/cli.test.mjs + * + * `init` writes into whatever directory it is run from, so every test here works in a + * throwaway directory under os.tmpdir() and never touches the repository. + * + * The point of these is the promise the CLI makes in its own help text: it does not + * overwrite what you already have, and it tells you every path it touched. A scaffolder + * that quietly breaks that promise is worse than no scaffolder. + */ + +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { execFileSync } from 'node:child_process'; +import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from 'node:fs'; +import { join, dirname, resolve } from 'node:path'; +import { tmpdir } from 'node:os'; +import { fileURLToPath } from 'node:url'; + +const CLI = resolve(dirname(fileURLToPath(import.meta.url)), 'claude-translator.mjs'); +const PKG = JSON.parse(readFileSync(resolve(dirname(CLI), '..', 'package.json'), 'utf8')); + +/** Run the CLI in a fresh directory. Returns { stdout, status }. */ +function run(args, { project = true, dir } = {}) { + const cwd = dir ?? mkdtempSync(join(tmpdir(), 'ct-cli-')); + if (project && !existsSync(join(cwd, 'package.json'))) { + writeFileSync(join(cwd, 'package.json'), JSON.stringify({ name: 'fixture', version: '1.0.0' }, null, 2)); + } + try { + const stdout = execFileSync(process.execPath, [CLI, ...args], { cwd, encoding: 'utf8' }); + return { stdout, status: 0, cwd }; + } catch (err) { + return { stdout: `${err.stdout ?? ''}${err.stderr ?? ''}`, status: err.status, cwd }; + } +} + +test('--version prints the package version', () => { + const { stdout, status } = run(['--version']); + assert.equal(status, 0); + assert.equal(stdout.trim(), PKG.version); +}); + +test('--help documents every command it accepts, and no command it does not', () => { + const { stdout, status } = run(['--help']); + assert.equal(status, 0); + for (const flag of ['init', '--dir', '--force', '--help', '--version']) { + assert.ok(stdout.includes(flag), `help omits ${flag}`); + } + // Every pipeline step the help promises must be a script the package actually ships. + for (const s of ['extract.mjs', 'translate.mjs', 'build-locales.mjs', 'verify.mjs', 'audit-seo.mjs']) { + assert.ok(stdout.includes(s), `help omits ${s}`); + assert.ok(existsSync(resolve(dirname(CLI), '..', 'scripts', s)), `help names ${s}, which does not exist`); + } +}); + +test('bare invocation shows help rather than doing something', () => { + const { stdout, status } = run([]); + assert.equal(status, 0); + assert.ok(stdout.includes('USAGE')); +}); + +test('an unknown command fails loudly', () => { + const { status, stdout } = run(['frobnicate']); + assert.equal(status, 1); + assert.ok(/unknown command/i.test(stdout)); +}); + +test('refuses to install outside a Node project, and leaves nothing behind', () => { + const cwd = mkdtempSync(join(tmpdir(), 'ct-cli-')); + const { status, stdout } = run(['init'], { project: false, dir: cwd }); + assert.equal(status, 1); + assert.ok(stdout.includes('No package.json')); + assert.equal(existsSync(join(cwd, 'scripts')), false, 'scattered files into a non-project'); + rmSync(cwd, { recursive: true, force: true }); +}); + +test('init scaffolds a runnable pipeline', () => { + const { stdout, status, cwd } = run(['init']); + assert.equal(status, 0); + + for (const f of [ + 'scripts/i18n/extract.mjs', + 'scripts/i18n/translate.mjs', + 'scripts/i18n/build-locales.mjs', + 'scripts/i18n/verify.mjs', + 'scripts/i18n/audit-seo.mjs', + 'scripts/i18n/config.mjs', + 'scripts/i18n/credit.mjs', + 'scripts/i18n/providers/index.mjs', + 'scripts/i18n/providers/anthropic.mjs', + 'i18n.config.json', + ]) { + assert.ok(existsSync(join(cwd, f)), `init did not write ${f}`); + assert.ok(stdout.includes(f.split('/').pop()), `init wrote ${f} without reporting it`); + } + + const manifest = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf8')); + assert.ok(manifest.devDependencies?.parse5, 'parse5 was not declared'); + assert.ok(readFileSync(join(cwd, '.gitignore'), 'utf8').includes('i18n/segments/')); + rmSync(cwd, { recursive: true, force: true }); +}); + +test('the config it writes is valid JSON with the keys the scripts require', () => { + const { cwd } = run(['init']); + const cfg = JSON.parse(readFileSync(join(cwd, 'i18n.config.json'), 'utf8')); + for (const key of ['buildDir', 'baseUrl', 'locales']) { + assert.ok(key in cfg, `config is missing ${key}`); + } + rmSync(cwd, { recursive: true, force: true }); +}); + +test('a second init overwrites nothing and says so', () => { + const { cwd } = run(['init']); + const configPath = join(cwd, 'i18n.config.json'); + writeFileSync(configPath, '{"buildDir":"MINE"}'); + + const { stdout, status } = run(['init'], { dir: cwd }); + assert.equal(status, 0); + assert.equal(JSON.parse(readFileSync(configPath, 'utf8')).buildDir, 'MINE', 'clobbered an edited config'); + assert.ok(stdout.includes('Left alone')); + assert.ok(stdout.includes('already has the block'), 'appended the gitignore block twice'); + rmSync(cwd, { recursive: true, force: true }); +}); + +test('--force is the only way to replace an edited file', () => { + const { cwd } = run(['init']); + const configPath = join(cwd, 'i18n.config.json'); + writeFileSync(configPath, '{"buildDir":"MINE"}'); + + run(['init', '--force'], { dir: cwd }); + assert.notEqual(JSON.parse(readFileSync(configPath, 'utf8')).buildDir, 'MINE', '--force did not replace'); + rmSync(cwd, { recursive: true, force: true }); +}); + +test('--dir puts the scripts where it is told', () => { + const { cwd, stdout } = run(['init', '--dir', 'tools/localize']); + assert.ok(existsSync(join(cwd, 'tools/localize/extract.mjs'))); + assert.equal(existsSync(join(cwd, 'scripts/i18n')), false); + // The next-steps block must reference the chosen directory, not the default. + assert.ok(stdout.includes('tools/localize/extract.mjs')); + rmSync(cwd, { recursive: true, force: true }); +}); + +test('an existing .gitignore is appended to, not replaced', () => { + const cwd = mkdtempSync(join(tmpdir(), 'ct-cli-')); + writeFileSync(join(cwd, 'package.json'), '{"name":"f","version":"1.0.0"}'); + writeFileSync(join(cwd, '.gitignore'), 'node_modules\n.env\n'); + run(['init'], { dir: cwd }); + const gi = readFileSync(join(cwd, '.gitignore'), 'utf8'); + assert.ok(gi.includes('node_modules'), 'dropped existing entries'); + assert.ok(gi.includes('.env'), 'dropped existing entries'); + assert.ok(gi.includes('i18n/segments/'), 'did not append ours'); + rmSync(cwd, { recursive: true, force: true }); +}); + +test('package.json "files" ships everything init needs to copy', () => { + const root = resolve(dirname(CLI), '..'); + for (const needed of ['bin', 'scripts', 'i18n.config.example.json']) { + assert.ok(PKG.files.includes(needed), `"files" omits ${needed}; npx would install a broken package`); + } + assert.equal(PKG.bin['claude-translator'], 'bin/claude-translator.mjs'); + assert.ok(existsSync(join(root, PKG.bin['claude-translator'])), 'bin path does not exist'); +}); diff --git a/package.json b/package.json index e709389..7e59f40 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "claude-translator", - "version": "1.2.0", + "version": "1.3.0", "description": "Translate a static website into dozens of languages as real static pages, by substituting into built HTML instead of re-rendering.", "type": "module", "license": "AGPL-3.0-or-later", @@ -32,7 +32,19 @@ "engines": { "node": ">=20" }, - "dependencies": { + "bin": { + "claude-translator": "bin/claude-translator.mjs" + }, + "files": [ + "bin", + "scripts", + "references", + "SKILL.md", + "LICENSING.md", + "CHANGELOG.md", + "i18n.config.example.json" + ], + "devDependencies": { "parse5": "^7.3.0" }, "scripts": { @@ -42,7 +54,7 @@ "i18n:build": "node scripts/build-locales.mjs", "i18n:verify": "node scripts/verify.mjs", "i18n:audit": "node scripts/audit-seo.mjs", - "check": "for f in scripts/*.mjs scripts/providers/*.mjs; do node --check \"$f\" || exit 1; done && echo 'all scripts parse'", - "test": "node --test scripts/providers/providers.test.mjs" + "check": "for f in scripts/*.mjs scripts/providers/*.mjs bin/*.mjs; do node --check \"$f\" || exit 1; done && echo 'all scripts parse'", + "test": "node --test scripts/providers/providers.test.mjs bin/cli.test.mjs" } } diff --git a/scripts/credit.mjs b/scripts/credit.mjs index 1f86f45..a40481e 100644 --- a/scripts/credit.mjs +++ b/scripts/credit.mjs @@ -32,7 +32,7 @@ import { CREDIT } from './config.mjs'; -export const VERSION = '1.2.0'; +export const VERSION = '1.3.0'; /** * The product name written into every localized page's generator tag.