claude-translator 1.4.0 → 2.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/.claude-plugin/plugin.json +14 -0
- package/CHANGELOG.md +180 -0
- package/PRIVACY.md +71 -0
- package/README.md +241 -39
- package/bin/claude-translator.mjs +11 -2
- package/bin/cli.test.mjs +20 -1
- package/glossary.example.json +23 -0
- package/i18n.config.example.json +7 -0
- package/package.json +9 -5
- package/scripts/audit-seo.mjs +6 -3
- package/scripts/build-locales.mjs +55 -6
- package/scripts/config.mjs +66 -0
- package/scripts/credit.mjs +12 -5
- package/scripts/extract.mjs +21 -6
- package/scripts/format-locale.mjs +290 -0
- package/scripts/format-locale.test.mjs +171 -0
- package/scripts/glossary.mjs +229 -0
- package/scripts/glossary.test.mjs +188 -0
- package/scripts/roles.mjs +142 -0
- package/scripts/roles.test.mjs +140 -0
- package/scripts/tqa-score.mjs +127 -0
- package/scripts/tqa-score.test.mjs +144 -0
- package/scripts/tqa.mjs +449 -0
- package/scripts/translate.mjs +87 -7
- package/scripts/verify.mjs +113 -5
- package/{SKILL.md → skills/translate-site/SKILL.md} +45 -13
- package/{references → skills/translate-site/references}/quality-review.md +28 -0
- /package/{references → skills/translate-site/references}/adapting-generators.md +0 -0
- /package/{references → skills/translate-site/references}/failure-modes.md +0 -0
- /package/{references → skills/translate-site/references}/providers.md +0 -0
- /package/{references → skills/translate-site/references}/throughput-and-cost.md +0 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
|
+
"name": "conveythis-translator",
|
|
4
|
+
"version": "2.0.0",
|
|
5
|
+
"description": "Localize a built static site into dozens of languages as real static pages, by substituting into the built HTML instead of re-rendering — so Core Web Vitals hold. Works with Astro, Next export, Hugo, Eleventy, Jekyll or plain HTML, via Claude, Gemini, any OpenAI-compatible endpoint, or a fully local model.",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "ConveyThis",
|
|
8
|
+
"url": "https://www.conveythis.com"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/ConveyThis/claude-translator",
|
|
11
|
+
"repository": "https://github.com/ConveyThis/claude-translator",
|
|
12
|
+
"license": "AGPL-3.0-or-later",
|
|
13
|
+
"keywords": ["i18n", "l10n", "localization", "translation", "static-site", "hreflang", "seo"]
|
|
14
|
+
}
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,186 @@ All notable changes to this project are documented here.
|
|
|
5
5
|
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.0.0] — 2026-08-30
|
|
9
|
+
|
|
10
|
+
Answers to a set of questions from a localization professional, who asked what the tool
|
|
11
|
+
does about terminology, brand-name sense, register in tightly-spaced CTAs, locale number
|
|
12
|
+
conventions, and whether there was any TQA evidence at all. Four of those had no answer.
|
|
13
|
+
Now they do — and two README claims that did not survive being checked are corrected.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **A glossary.** `glossary` in the config, inline or a path to a JSON file. Two rules:
|
|
18
|
+
`keep` (leave in the source language, case-sensitive by default) and `translate` with
|
|
19
|
+
per-locale `targets`. Matching is whole-word, so `Apple` never matches inside
|
|
20
|
+
`Applesauce`, and `matchCase` is what separates **Apple** the company from **apple** the
|
|
21
|
+
fruit — a distinction the old flat `doNotTranslate` list could not express.
|
|
22
|
+
|
|
23
|
+
Only the terms present in a batch are injected into that batch's prompt, so a large
|
|
24
|
+
glossary does not inflate every request. `doNotTranslate.brands` is folded in
|
|
25
|
+
automatically as case-sensitive `keep` rules, so existing configs gain word-boundary
|
|
26
|
+
matching and verification without being edited.
|
|
27
|
+
|
|
28
|
+
Changing a term re-translates **only** the units that contain it, tracked by a
|
|
29
|
+
fingerprint sidecar at `i18n/tm/{lang}.meta.json`. It is a sidecar rather than a key
|
|
30
|
+
inside the memory because the memory is documented as a hand-editable `hash -> string`
|
|
31
|
+
map, and three other scripts iterate it — a `__meta` key would have made every coverage
|
|
32
|
+
count off by one.
|
|
33
|
+
|
|
34
|
+
- **Locale conventions for numbers, percentages and currency.** `localeFormat`. The model
|
|
35
|
+
is still told to leave numbers alone; `Intl` reformats them afterwards, deterministically.
|
|
36
|
+
`1,234.56` becomes `1.234,56` in German, `$5` becomes `5,00 $US` in French.
|
|
37
|
+
|
|
38
|
+
**Currency is formatted, never converted, and there is no option to convert it.** Every
|
|
39
|
+
monetary amount found is written to `i18n/locale-format.json` for a human to price per
|
|
40
|
+
market instead. Version numbers, times, IP addresses, ISO dates, phone numbers, fractions
|
|
41
|
+
and ungrouped numbers are deliberately left untouched — during development a greedy
|
|
42
|
+
pattern turned `192.168.1.1` into `1.921.681,1`, which is why only unambiguous quantities
|
|
43
|
+
are matched and why that case is now a test.
|
|
44
|
+
|
|
45
|
+
- **`tqa.mjs` — MQM translation quality assessment.** Scores a seeded, frequency-stratified
|
|
46
|
+
sample using the standard MQM typology and severity weights (minor 1, major 5,
|
|
47
|
+
critical 10). Writes `i18n/tqa/{lang}.json` and a Markdown scorecard.
|
|
48
|
+
|
|
49
|
+
The judge defaults to a **different provider than the translator**, because models prefer
|
|
50
|
+
their own output; when no second key is available it says so in the run and in the
|
|
51
|
+
report. `--repeat` scores the same sample twice and reports the gap, because a quality
|
|
52
|
+
number without its variance is marketing. A unit the judge cannot assess is **excluded,
|
|
53
|
+
not counted as clean** — an early version printed `100.00 / 100` from a sample where
|
|
54
|
+
every single unit had failed, and it now refuses to report a score at all in that case.
|
|
55
|
+
|
|
56
|
+
- **Element context for register (`el`).** The model now learns *what* a string is, not
|
|
57
|
+
only what it says. `extract.mjs` always knew — it walks the DOM with `node.tagName` in
|
|
58
|
+
hand — and threw it away, so rule 5's "Headings stay headings; button labels stay short"
|
|
59
|
+
was unenforceable: every string arrived looking like prose.
|
|
60
|
+
|
|
61
|
+
A `<button>`, a standalone `<a>`, a heading, a `<label>`, an `alt` attribute and a meta
|
|
62
|
+
description now each carry a short label, and the prompt gains a CONTEXT block describing
|
|
63
|
+
**only the roles present in that batch**. Ordinary prose sends no field at all, so a site
|
|
64
|
+
of nothing but paragraphs produces a byte-identical payload to 1.x.
|
|
65
|
+
|
|
66
|
+
Two deliberate restraints. The button guidance does **not** demand the imperative — German
|
|
67
|
+
UI prefers a verbal noun and French the infinitive, and ordering a literal command
|
|
68
|
+
everywhere is the exact defect this exists to avoid. And when the same string appears as
|
|
69
|
+
both a button and a paragraph, the hint is **cleared** rather than guessed: one hash means
|
|
70
|
+
one translation, and a confident wrong answer is worse than none.
|
|
71
|
+
|
|
72
|
+
`kind` is untouched. It looks like the natural place for this, but `build-locales.mjs`
|
|
73
|
+
switches on it to choose an escaper, and a value like `'block:button'` would fall through
|
|
74
|
+
to `escHtml` and print literal `<0>` placeholder tokens as visible text on every
|
|
75
|
+
localized page — with no test to catch it. The element travels in a new field instead.
|
|
76
|
+
|
|
77
|
+
**No translation memory is invalidated.** Unit hashes are computed from text alone, and a
|
|
78
|
+
fixture run proves the hashes are byte-identical before and after the change.
|
|
79
|
+
|
|
80
|
+
- **Gate 7 — glossary compliance.** Reports terms that were supposed to survive, or to be
|
|
81
|
+
rendered a particular way, and were not. Reports by default; `--strict` makes it fail the
|
|
82
|
+
build. It only warns because target languages inflect pinned terms, and
|
|
83
|
+
`references/quality-review.md` is explicit that over-flagging is worse than no check.
|
|
84
|
+
|
|
85
|
+
- **Gate 8 — numeric integrity.** Fails the build when the *value* of a number changes
|
|
86
|
+
between source and translation. A model that ships `$39` where the source said `$49`
|
|
87
|
+
passes every other gate: identical markup, matching placeholders, plausible length,
|
|
88
|
+
fluent target language.
|
|
89
|
+
|
|
90
|
+
### Fixed
|
|
91
|
+
|
|
92
|
+
- **The README claimed to emit things it only rewrites.** "What you get" promised "a
|
|
93
|
+
complete `hreflang` set" and "per-locale sitemaps". Neither is generated: the only write
|
|
94
|
+
into the build directory is one HTML file per page, and every locale-identity rule
|
|
95
|
+
*replaces an attribute on a tag the template already emits*. The section is now split
|
|
96
|
+
into what the tool rewrites and what your template must supply — and it explains that
|
|
97
|
+
the rewrite-only design is what preserves byte-identical markup.
|
|
98
|
+
|
|
99
|
+
- **`i18nDir` silently did nothing** in `verify.mjs` and `audit-seo.mjs`, which imported
|
|
100
|
+
`I18N_DIR` and then used hardcoded `i18n/...` paths. Same for `$I18N_ROOT`, which three
|
|
101
|
+
scripts ignored in favour of `process.cwd()`.
|
|
102
|
+
|
|
103
|
+
- **The generator tag stamped a stale version** — `1.4.0` while the package said `1.5.0`.
|
|
104
|
+
It cannot be read from `package.json` (once vendored, the nearest manifest is the user's
|
|
105
|
+
own application), so CI now asserts the literal matches instead.
|
|
106
|
+
|
|
107
|
+
- **`init` vendored our own contract tests** into the user's project, where they import a
|
|
108
|
+
test runner and assert on our internals.
|
|
109
|
+
|
|
110
|
+
### Changed
|
|
111
|
+
|
|
112
|
+
- `verify.mjs` runs eight gates, not six.
|
|
113
|
+
- `init` scaffolds a starter `glossary.json`.
|
|
114
|
+
- CI now runs the fixture through build and all eight gates, not just extraction, and
|
|
115
|
+
checks that the packaged CLI scaffolds every new script.
|
|
116
|
+
|
|
117
|
+
> **Upgrading:** `init` copies the pipeline into your project, so an existing install
|
|
118
|
+
> keeps running the scripts it already has. Re-run `npx claude-translator init --force` to
|
|
119
|
+
> pick up the new stages. Nothing in this release changes existing translations, and no
|
|
120
|
+
> memory is invalidated unless you add a glossary.
|
|
121
|
+
|
|
122
|
+
## [1.5.0] — 2026-08-25
|
|
123
|
+
|
|
124
|
+
### Added
|
|
125
|
+
|
|
126
|
+
- **The repo is now a Claude Code plugin, not only a skill.** A `.claude-plugin/plugin.json`
|
|
127
|
+
manifest gives it the name, version, description and licence the plugin directory needs in
|
|
128
|
+
order to list it. The plugin is named `conveythis-translator`, so the skill is invoked as
|
|
129
|
+
`/conveythis-translator:translate-site`
|
|
130
|
+
|
|
131
|
+
- **README section on installing it as a plugin**, covering the community-marketplace install,
|
|
132
|
+
`claude --plugin-dir` for a local clone, and the `~/.claude/skills/` directory-plugin path
|
|
133
|
+
|
|
134
|
+
### Changed
|
|
135
|
+
|
|
136
|
+
- **`SKILL.md` moved from the repo root to `skills/translate-site/SKILL.md`.** This was forced,
|
|
137
|
+
not cosmetic. The plugin documentation states that a single-skill plugin may keep `SKILL.md` at
|
|
138
|
+
the plugin root, but on Claude Code 2.1.126 that skill is never discovered — verified against
|
|
139
|
+
three variants (manifest present, manifest absent, and `"skills": "./"`), all of which loaded
|
|
140
|
+
no skill at all. The conventional `skills/<name>/SKILL.md` layout loads correctly
|
|
141
|
+
|
|
142
|
+
- **`references/` moved into `skills/translate-site/references/`, so the skill directory is
|
|
143
|
+
self-contained.** This keeps both install paths working from one copy: as a plugin, and as a
|
|
144
|
+
plain personal skill symlinked into `~/.claude/skills/`. The reference links in the skill stay
|
|
145
|
+
plain relative paths; only `LICENSING.md` and `i18n.config.example.json`, which remain at the
|
|
146
|
+
repo root, are addressed as `${CLAUDE_PLUGIN_ROOT}/…`
|
|
147
|
+
|
|
148
|
+
- Contrary to what an earlier draft of this entry claimed, a directory in `~/.claude/skills/`
|
|
149
|
+
carrying a `.claude-plugin/plugin.json` is **not** auto-loaded as a plugin on 2.1.126 —
|
|
150
|
+
`claude plugin list` reports nothing installed. The README now documents symlinking the skill
|
|
151
|
+
directory itself, which is verified to work
|
|
152
|
+
|
|
153
|
+
- The skill's frontmatter `name` is now `translate-site` rather than `claude-translator`, so the
|
|
154
|
+
namespaced invocation does not read `/conveythis-translator:claude-translator`
|
|
155
|
+
|
|
156
|
+
- `package.json` `files` ships `.claude-plugin/` and `skills/` in place of the root `SKILL.md`
|
|
157
|
+
|
|
158
|
+
### Fixed
|
|
159
|
+
|
|
160
|
+
- **The Pipeline commands in `SKILL.md` pointed at the wrong directory.** They read
|
|
161
|
+
`node scripts/extract.mjs`, but `claude-translator init` scaffolds into `scripts/i18n/`
|
|
162
|
+
(its `--dir` default), as the README has always shown correctly. Every command in that block
|
|
163
|
+
would have failed with `Cannot find module` for anyone following the skill rather than the
|
|
164
|
+
README
|
|
165
|
+
|
|
166
|
+
- **Setup instructions assumed a hand-clone.** The skill hardcoded
|
|
167
|
+
`~/.claude/skills/claude-translator/bin/claude-translator.mjs`, a path that does not exist for
|
|
168
|
+
a plugin install. It now leads with `npx claude-translator init` and offers
|
|
169
|
+
`"${CLAUDE_PLUGIN_ROOT}"/bin/claude-translator.mjs` as the offline fallback, which resolves to
|
|
170
|
+
wherever the plugin actually landed
|
|
171
|
+
|
|
172
|
+
### Verified
|
|
173
|
+
|
|
174
|
+
- `claude plugin validate .` passes with no errors and no warnings on Claude Code 2.1.126.
|
|
175
|
+
Note that this version rejects both `displayName` and `"skills": ["."]`, which the current
|
|
176
|
+
plugin reference documents — neither is used here
|
|
177
|
+
- Loaded through `claude --plugin-dir`, the skill is discovered and namespaced correctly. The
|
|
178
|
+
check used a uniquely renamed copy of the skill, so a same-named skill already installed in
|
|
179
|
+
`~/.claude/skills/` could not be mistaken for it
|
|
180
|
+
- `${CLAUDE_PLUGIN_ROOT}` resolution and the corrected setup line were confirmed end to end:
|
|
181
|
+
running the documented command against a throwaway project scaffolds `scripts/i18n/` with all
|
|
182
|
+
nine scripts, `i18n.config.json`, and the `.gitignore` entries
|
|
183
|
+
- Both install paths were checked against the final layout: the plugin loads through
|
|
184
|
+
`--plugin-dir`, and the skill directory symlinked into `~/.claude/skills/` is discovered too
|
|
185
|
+
- `npm run check` and `npm test` (39 tests) pass unchanged; `npm pack` ships
|
|
186
|
+
`.claude-plugin/plugin.json`, the skill and its references
|
|
187
|
+
|
|
8
188
|
## [1.4.0] — 2026-08-25
|
|
9
189
|
|
|
10
190
|
### Changed
|
package/PRIVACY.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Privacy
|
|
2
|
+
|
|
3
|
+
Short version: this tool runs on your machine, and it sends us nothing.
|
|
4
|
+
|
|
5
|
+
The canonical policy is the **Open-Source and Downloadable Software** section of the
|
|
6
|
+
[ConveyThis Privacy Policy](https://www.conveythis.com/legal/privacy-policy#open-source-and-downloadable-software).
|
|
7
|
+
This file is the code-adjacent summary, and it is meant to be checkable against the source.
|
|
8
|
+
|
|
9
|
+
## What it sends, and where
|
|
10
|
+
|
|
11
|
+
There is exactly **one** outbound network call in this repository. It lives in
|
|
12
|
+
[`scripts/translate.mjs`](scripts/translate.mjs), and its URL comes from whichever provider
|
|
13
|
+
adapter you selected in `i18n.config.json`:
|
|
14
|
+
|
|
15
|
+
| Provider | Endpoint | Key it reads |
|
|
16
|
+
| --- | --- | --- |
|
|
17
|
+
| `anthropic` (default) | `https://api.anthropic.com/v1` | `ANTHROPIC_API_KEY` |
|
|
18
|
+
| `gemini` | `https://generativelanguage.googleapis.com/v1beta/models` | `GEMINI_API_KEY` / `GOOGLE_API_KEY` |
|
|
19
|
+
| `openai` | `https://api.openai.com/v1`, or whatever you set as `apiBaseUrl` | `OPENAI_API_KEY` / `OPENAI_COMPATIBLE_API_KEY` |
|
|
20
|
+
|
|
21
|
+
Only `translate.mjs` makes any request at all. `extract`, `review`, `build-locales`, `verify`
|
|
22
|
+
and `audit-seo` are entirely offline — they read and write files on your disk and nothing else.
|
|
23
|
+
|
|
24
|
+
Verify it yourself:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
grep -rn "fetch(" scripts bin # one hit, in translate.mjs
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## What it never sends
|
|
31
|
+
|
|
32
|
+
- **Nothing goes to ConveyThis.** No analytics, no usage reporting, no error reporting, no
|
|
33
|
+
licence check. We do not know you installed this, ran it, or what you translated.
|
|
34
|
+
- **Your API key goes only to the provider it belongs to.** It is read from the environment or a
|
|
35
|
+
gitignored `.env`, used as that provider's auth header, and never written to output.
|
|
36
|
+
- **Your content goes only to the provider you chose.** Translating means sending your page text
|
|
37
|
+
to a model, which is inherent to the task — but it goes there directly from your machine.
|
|
38
|
+
|
|
39
|
+
## Running with zero network egress
|
|
40
|
+
|
|
41
|
+
Set the `openai` provider against a local endpoint and nothing leaves the machine:
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{ "provider": "openai", "apiBaseUrl": "http://localhost:11434/v1", "model": "qwen2.5:14b" }
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Ollama, LM Studio and vLLM all work this way. No key, no quota, no third party. Use this if the
|
|
48
|
+
content is confidential.
|
|
49
|
+
|
|
50
|
+
## The attribution tag
|
|
51
|
+
|
|
52
|
+
Generated pages carry a `<meta name="generator">` tag and an HTML comment naming ConveyThis,
|
|
53
|
+
written by [`scripts/credit.mjs`](scripts/credit.mjs). Both are **static markup**: they make no
|
|
54
|
+
request, load no script, set no cookie, and report nothing to us when someone views your page.
|
|
55
|
+
The `conveythis.com` and `doctranslator.com` strings in that file are written into HTML — they
|
|
56
|
+
are never fetched.
|
|
57
|
+
|
|
58
|
+
To remove them, set `credit.generatorTag` and `credit.htmlComment` to `false` in
|
|
59
|
+
`i18n.config.json`.
|
|
60
|
+
|
|
61
|
+
## Your translation memory
|
|
62
|
+
|
|
63
|
+
`i18n/tm/{lang}.json` holds your source and translated copy, and is meant to be committed —
|
|
64
|
+
losing it means paying to re-translate. Check that this suits your repository's visibility
|
|
65
|
+
before committing it to a public repo.
|
|
66
|
+
|
|
67
|
+
## Elsewhere
|
|
68
|
+
|
|
69
|
+
Opening issues or pull requests on GitHub, or installing from npm, is governed by those
|
|
70
|
+
platforms' own privacy policies. Security reports go to **security@conveythis.com** — see
|
|
71
|
+
[SECURITY.md](SECURITY.md).
|