Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions .claude/skills/lich-changelog-i18n/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
---
name: lich-changelog-i18n
description: Translate the latest release's CHANGELOG.md section into every interface language lich ships, writing changelog/<locale>.md, the files the What's new popup reads. Use when cutting a release (right after its section moves out of [Unreleased]), when TestTranslations in internal/patchnotes fails, or when a language is added.
description: Translate the latest release's CHANGELOG.md section into every interface language lich ships, adding it to changelog/<locale>.md, the files the What's new popup and the site's changelog pages read. Use when cutting a release (right after its section moves out of [Unreleased]), when TestTranslations in internal/patchnotes fails, or when a language is added.
---

# lich changelog translations

The What's new popup shows the running build's `CHANGELOG.md` section (`internal/patchnotes`). A user whose
interface language is not English reads it from `changelog/<locale>.md` instead, embedded in the binary
next to `CHANGELOG.md`. Each file holds **only the latest release**: a build only ever reads its own version,
and git keeps the old translations. `CHANGELOG.md` stays English and canonical; the release page reads it.
next to `CHANGELOG.md`. Each file holds **every translated release, newest first**: a build only reads its own
version, but the site's changelog page in that language lists them all and sends the reader to the English page
where they stop. `CHANGELOG.md` stays English and canonical; the release page reads it.

## When

Expand All @@ -23,8 +24,9 @@ means running it again.
2. **One file per locale, except English.** The locales are `prompt.Langs` (`internal/prompt/prompt.go`),
file names are their tags as written there: `pt-BR.md`, `es.md`, `zh-CN.md`, `ja.md`, `ko.md`, `ru.md`.
A language added to lich gets its file in the same PR.
3. **Overwrite each file** with the translated section and nothing else: no `# Changelog` title, no
`[Unreleased]`, no older release, no compare links.
3. **Add the translated section at the top of each file**, above the previous release, with a blank line
between them, and nothing else: no `# Changelog` title, no `[Unreleased]`, no compare links. Running it again
for the same release replaces that release's section, never adds a second one.
4. **Translate from the English**, never from another translation. The locales are independent, so each
one can go to its own subagent in parallel, on a cheap model (Sonnet); hand each the English section and
the rules below.
Expand Down Expand Up @@ -61,7 +63,8 @@ The parser and both windows read these, so `TestTranslations` fails when one is

## If the check fails

- `version headings = …`: the file holds another release, or more than one. Rewrite it from step 1.
- `version headings = …`: the file does not open with the latest release, skips one, or holds one twice.
Put the latest release's section at the top, once, from step 1.
- `N groups` / `N items`: an item was merged, split or lost, often a blank line inside an item.
- `group N is "…"`: a `### ` heading was translated.
- `code spans …` or `bold markers …`: a code span was changed or a `**` was lost; the message prints the
Expand Down
334 changes: 334 additions & 0 deletions changelog/es.md

Large diffs are not rendered by default.

175 changes: 175 additions & 0 deletions changelog/ja.md

Large diffs are not rendered by default.

276 changes: 276 additions & 0 deletions changelog/ko.md

Large diffs are not rendered by default.

329 changes: 329 additions & 0 deletions changelog/pt-BR.md

Large diffs are not rendered by default.

329 changes: 329 additions & 0 deletions changelog/ru.md

Large diffs are not rendered by default.

173 changes: 173 additions & 0 deletions changelog/zh-CN.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/changelog.es.html
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
<h1>Changelog</h1>
<p>Novedades, cambios y correcciones de cada versión de lich.</p>
</div>
<div id="releases" data-locale="es" data-groups="{&quot;Added&quot;: &quot;Novedades&quot;, &quot;Changed&quot;: &quot;Cambios&quot;, &quot;Deprecated&quot;: &quot;Obsoleto&quot;, &quot;Removed&quot;: &quot;Eliminado&quot;, &quot;Fixed&quot;: &quot;Correcciones&quot;, &quot;Security&quot;: &quot;Seguridad&quot;}" data-release="Release en GitHub" data-english="Las versiones anteriores están en inglés."><p class="cl-loading">Leyendo el changelog…</p></div>
<div id="releases" data-locale="es" data-groups="{&quot;Added&quot;: &quot;Novedades&quot;, &quot;Changed&quot;: &quot;Cambios&quot;, &quot;Deprecated&quot;: &quot;Obsoleto&quot;, &quot;Removed&quot;: &quot;Eliminado&quot;, &quot;Fixed&quot;: &quot;Correcciones&quot;, &quot;Security&quot;: &quot;Seguridad&quot;}" data-release="Release en GitHub" data-english="Las versiones anteriores están en el changelog en inglés"><p class="cl-loading">Leyendo el changelog…</p></div>
<p id="fallback" class="cl-loading" hidden>No se pudo leer el changelog aquí. Está en <a href="https://github.com/lichdotdev/lich/blob/main/CHANGELOG.md">GitHub</a>.</p>
<button id="older" class="btn ghost cl-older" type="button" hidden>Versiones anteriores</button>
</main>
Expand Down
2 changes: 1 addition & 1 deletion docs/changelog.ja.html
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
<h1>変更履歴</h1>
<p>lich の各リリースの追加・変更・修正。</p>
</div>
<div id="releases" data-locale="ja" data-groups="{&quot;Added&quot;: &quot;追加&quot;, &quot;Changed&quot;: &quot;変更&quot;, &quot;Deprecated&quot;: &quot;非推奨&quot;, &quot;Removed&quot;: &quot;削除&quot;, &quot;Fixed&quot;: &quot;修正&quot;, &quot;Security&quot;: &quot;セキュリティ&quot;}" data-release="GitHub のリリース" data-english="これより前のリリースは英語のみです。"><p class="cl-loading">変更履歴を読み込んでいます…</p></div>
<div id="releases" data-locale="ja" data-groups="{&quot;Added&quot;: &quot;追加&quot;, &quot;Changed&quot;: &quot;変更&quot;, &quot;Deprecated&quot;: &quot;非推奨&quot;, &quot;Removed&quot;: &quot;削除&quot;, &quot;Fixed&quot;: &quot;修正&quot;, &quot;Security&quot;: &quot;セキュリティ&quot;}" data-release="GitHub のリリース" data-english="これより前のリリースは英語の変更履歴にあります"><p class="cl-loading">変更履歴を読み込んでいます…</p></div>
<p id="fallback" class="cl-loading" hidden>ここでは変更履歴を読み込めませんでした。<a href="https://github.com/lichdotdev/lich/blob/main/CHANGELOG.md">GitHub</a> で確認できます。</p>
<button id="older" class="btn ghost cl-older" type="button" hidden>以前のリリース</button>
</main>
Expand Down
29 changes: 15 additions & 14 deletions docs/changelog.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Renders the changelog from main, so this page never needs editing: each "## [X.Y.Z] - date" section becomes a
// release, its GitHub alert blocks become the headline and callouts, its "###" groups keep their order. A page in
// another language reads changelog/<locale>.md too and shows each release it translates in place of the English
// one; older releases, which only CHANGELOG.md holds, stay in English under a note that says so.
// another language shows only the releases changelog/<locale>.md translates, then a link to the English page for
// the older ones, so a page never mixes languages; with no translation to read it falls back to the English list.
// The file only uses headings, "- " items with indented continuation lines, alert blocks and inline
// **bold**, `code` and [links](url), which is all this reads.
(function () {
Expand All @@ -13,7 +13,7 @@
var locale = list.getAttribute("data-locale") || "en";
var groups = JSON.parse(list.getAttribute("data-groups") || "{}");
var releaseLink = list.getAttribute("data-release") || "Release on GitHub";
var englishNote = list.getAttribute("data-english");
var olderInEnglish = list.getAttribute("data-english");

function escape(text) {
return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
Expand Down Expand Up @@ -59,8 +59,7 @@
}
function render(r) {
var important = r.alerts.filter(function (a) { return a.kind === "important"; })[0];
var note = r.firstEnglish ? '<p class="rel-english">' + escape(englishNote) + "</p>" : "";
var html = note + '<article class="rel"' + (r.english ? ' lang="en"' : "") + '><div class="rel-date">' + date(r.date) + "</div>" +
var html = '<article class="rel"' + (r.english ? ' lang="en"' : "") + '><div class="rel-date">' + date(r.date) + "</div>" +
'<a class="rel-ver" href="' + RELEASES + r.version + '">' + r.version + "</a>";
if (important) html += '<p class="rel-lead">' + inline(important.text.trim()) + "</p>";
r.alerts.forEach(function (a) {
Expand All @@ -73,11 +72,14 @@
return html + '<a class="rel-more" href="' + RELEASES + r.version + '">' + escape(releaseLink) + " →</a></article>";
}

var releases = [], shown = 0;
var releases = [], shown = 0, untranslated = false;
function show() {
list.insertAdjacentHTML("beforeend", releases.slice(shown, shown + PAGE).map(render).join(""));
shown += PAGE;
more.hidden = shown >= releases.length;
if (more.hidden && untranslated) {
list.insertAdjacentHTML("beforeend", '<p class="rel-english"><a href="changelog.html">' + escape(olderInEnglish) + " →</a></p>");
}
}
more.addEventListener("click", show);
function read(path) {
Expand All @@ -89,14 +91,13 @@
.then(function (got) {
var local = {};
parse(got[1]).forEach(function (r) { local[r.version] = r; });
var shownEnglish = false;
releases = parse(got[0]).map(function (r) {
if (local[r.version]) return local[r.version];
r.english = locale !== "en";
r.firstEnglish = r.english && !shownEnglish && Boolean(englishNote);
if (r.english) shownEnglish = true;
return r;
});
var english = parse(got[0]);
releases = english.filter(function (r) { return local[r.version]; }).map(function (r) { return local[r.version]; });
untranslated = releases.length > 0 && releases.length < english.length;
if (!releases.length) {
releases = english;
releases.forEach(function (r) { r.english = locale !== "en"; });
}
list.innerHTML = "";
show();
})
Expand Down
2 changes: 1 addition & 1 deletion docs/changelog.ko.html
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
<h1>변경 내역</h1>
<p>lich 각 릴리스의 추가, 변경, 수정 사항입니다.</p>
</div>
<div id="releases" data-locale="ko" data-groups="{&quot;Added&quot;: &quot;추가&quot;, &quot;Changed&quot;: &quot;변경&quot;, &quot;Deprecated&quot;: &quot;지원 중단&quot;, &quot;Removed&quot;: &quot;제거&quot;, &quot;Fixed&quot;: &quot;수정&quot;, &quot;Security&quot;: &quot;보안&quot;}" data-release="GitHub 릴리스" data-english="이전 릴리스는 영어로만 제공됩니다."><p class="cl-loading">변경 내역을 읽는 중…</p></div>
<div id="releases" data-locale="ko" data-groups="{&quot;Added&quot;: &quot;추가&quot;, &quot;Changed&quot;: &quot;변경&quot;, &quot;Deprecated&quot;: &quot;지원 중단&quot;, &quot;Removed&quot;: &quot;제거&quot;, &quot;Fixed&quot;: &quot;수정&quot;, &quot;Security&quot;: &quot;보안&quot;}" data-release="GitHub 릴리스" data-english="이전 릴리스는 영어 변경 기록에 있습니다"><p class="cl-loading">변경 내역을 읽는 중…</p></div>
<p id="fallback" class="cl-loading" hidden>여기서 변경 내역을 읽지 못했습니다. <a href="https://github.com/lichdotdev/lich/blob/main/CHANGELOG.md">GitHub</a>에서 볼 수 있습니다.</p>
<button id="older" class="btn ghost cl-older" type="button" hidden>이전 릴리스</button>
</main>
Expand Down
2 changes: 1 addition & 1 deletion docs/changelog.pt-BR.html
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
<h1>Changelog</h1>
<p>Novidades, mudanças e correções de cada versão do lich.</p>
</div>
<div id="releases" data-locale="pt-BR" data-groups="{&quot;Added&quot;: &quot;Novidades&quot;, &quot;Changed&quot;: &quot;Mudanças&quot;, &quot;Deprecated&quot;: &quot;Obsoleto&quot;, &quot;Removed&quot;: &quot;Removido&quot;, &quot;Fixed&quot;: &quot;Correções&quot;, &quot;Security&quot;: &quot;Segurança&quot;}" data-release="Release no GitHub" data-english="As versões anteriores estão em inglês."><p class="cl-loading">Lendo o changelog…</p></div>
<div id="releases" data-locale="pt-BR" data-groups="{&quot;Added&quot;: &quot;Novidades&quot;, &quot;Changed&quot;: &quot;Mudanças&quot;, &quot;Deprecated&quot;: &quot;Obsoleto&quot;, &quot;Removed&quot;: &quot;Removido&quot;, &quot;Fixed&quot;: &quot;Correções&quot;, &quot;Security&quot;: &quot;Segurança&quot;}" data-release="Release no GitHub" data-english="As versões anteriores estão no changelog em inglês"><p class="cl-loading">Lendo o changelog…</p></div>
<p id="fallback" class="cl-loading" hidden>Não deu para ler o changelog aqui. Ele está no <a href="https://github.com/lichdotdev/lich/blob/main/CHANGELOG.md">GitHub</a>.</p>
<button id="older" class="btn ghost cl-older" type="button" hidden>Versões anteriores</button>
</main>
Expand Down
2 changes: 1 addition & 1 deletion docs/changelog.ru.html
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
<h1>Журнал изменений</h1>
<p>Новое, изменения и исправления в каждом релизе lich.</p>
</div>
<div id="releases" data-locale="ru" data-groups="{&quot;Added&quot;: &quot;Добавлено&quot;, &quot;Changed&quot;: &quot;Изменено&quot;, &quot;Deprecated&quot;: &quot;Устарело&quot;, &quot;Removed&quot;: &quot;Удалено&quot;, &quot;Fixed&quot;: &quot;Исправлено&quot;, &quot;Security&quot;: &quot;Безопасность&quot;}" data-release="Релиз на GitHub" data-english="Более ранние релизы доступны только на английском."><p class="cl-loading">Читаем журнал изменений…</p></div>
<div id="releases" data-locale="ru" data-groups="{&quot;Added&quot;: &quot;Добавлено&quot;, &quot;Changed&quot;: &quot;Изменено&quot;, &quot;Deprecated&quot;: &quot;Устарело&quot;, &quot;Removed&quot;: &quot;Удалено&quot;, &quot;Fixed&quot;: &quot;Исправлено&quot;, &quot;Security&quot;: &quot;Безопасность&quot;}" data-release="Релиз на GitHub" data-english="Более ранние релизы есть в английском списке изменений"><p class="cl-loading">Читаем журнал изменений…</p></div>
<p id="fallback" class="cl-loading" hidden>Не удалось загрузить журнал изменений. Он есть на <a href="https://github.com/lichdotdev/lich/blob/main/CHANGELOG.md">GitHub</a>.</p>
<button id="older" class="btn ghost cl-older" type="button" hidden>Ранние релизы</button>
</main>
Expand Down
2 changes: 1 addition & 1 deletion docs/changelog.zh-CN.html
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
<h1>更新日志</h1>
<p>lich 每个版本的新增、变更与修复。</p>
</div>
<div id="releases" data-locale="zh-CN" data-groups="{&quot;Added&quot;: &quot;新增&quot;, &quot;Changed&quot;: &quot;变更&quot;, &quot;Deprecated&quot;: &quot;弃用&quot;, &quot;Removed&quot;: &quot;移除&quot;, &quot;Fixed&quot;: &quot;修复&quot;, &quot;Security&quot;: &quot;安全&quot;}" data-release="在 GitHub 上查看" data-english="更早的版本仅提供英文。"><p class="cl-loading">正在读取更新日志…</p></div>
<div id="releases" data-locale="zh-CN" data-groups="{&quot;Added&quot;: &quot;新增&quot;, &quot;Changed&quot;: &quot;变更&quot;, &quot;Deprecated&quot;: &quot;弃用&quot;, &quot;Removed&quot;: &quot;移除&quot;, &quot;Fixed&quot;: &quot;修复&quot;, &quot;Security&quot;: &quot;安全&quot;}" data-release="在 GitHub 上查看" data-english="更早的版本见英文更新日志"><p class="cl-loading">正在读取更新日志…</p></div>
<p id="fallback" class="cl-loading" hidden>这里无法读取更新日志,请到 <a href="https://github.com/lichdotdev/lich/blob/main/CHANGELOG.md">GitHub</a> 查看。</p>
<button id="older" class="btn ghost cl-older" type="button" hidden>更早的版本</button>
</main>
Expand Down
3 changes: 3 additions & 0 deletions docs/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -724,6 +724,9 @@ footer nav { display: flex; flex-direction: column; gap: .6rem; }
.rel li code, .rel-note code, .rel-lead code { font-size: .86em; color: var(--fg); }
.rel a:not(.rel-ver):not(.rel-more) { color: var(--fg); }
.rel-english { margin: 0 0 2rem; padding: .75rem 1rem; border-radius: .6rem; background: var(--island); color: var(--muted); font-size: .9rem; }
.rel-english a { color: inherit; text-decoration: none; }
.rel-english a:hover { color: var(--fg); }
.rel-more { display: inline-block; margin-top: 1.2rem; color: var(--muted); font: .8rem var(--mono); text-decoration: none; }
.rel-more:hover { color: var(--fg); }
.cl-older { display: flex; margin: 0 auto; }
.cl-older[hidden] { display: none; }
4 changes: 2 additions & 2 deletions internal/patchnotes/patchnotes.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@
// title, "warning" is something the reader must do or avoid, "note" a detail
// worth a line. The window decides how each kind looks.
//
// A translation of the latest release lives in changelog/<locale>.md, written
// Translations live in changelog/<locale>.md, newest release first, each added
// by the lich-changelog-i18n skill when a release is cut: the same section,
// headings, alert openers and code spans left in English. TestTranslations
// holds each file to the release it translates.
// holds each file to the releases it translates.
package patchnotes

import (
Expand Down
37 changes: 23 additions & 14 deletions internal/patchnotes/translations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,31 @@ const repoRoot = "../.."

var codeSpan = regexp.MustCompile("`[^`]+`")

// TestTranslations holds every changelog/<locale>.md to the latest release in
// CHANGELOG.md: the one section, under the same heading, with the same groups,
// item counts, alert kinds, code spans and bold lead-ins. A release cut without
// running the lich-changelog-i18n skill fails here.
// TestTranslations holds every changelog/<locale>.md to CHANGELOG.md: its
// releases are the newest ones there, latest first and none skipped, each under
// the same heading, with the same groups, item counts, alert kinds, code spans
// and bold lead-ins. A release cut without running the lich-changelog-i18n
// skill fails here.
func TestTranslations(t *testing.T) {
english := readRepoFile(t, "CHANGELOG.md")
heading := latestReleaseHeading(t, english)
version := heading[len("## ["):strings.Index(heading, "]")]
released := releaseHeadings(t, english)
for _, lang := range prompt.Langs {
if lang == prompt.English {
continue
}
t.Run(string(lang), func(t *testing.T) {
translated := readRepoFile(t, filepath.Join("changelog", string(lang)+".md"))
if got := versionHeadings(translated); !reflect.DeepEqual(got, []string{heading}) {
t.Fatalf("version headings = %q, want only %q", got, heading)
headings := versionHeadings(translated)
if len(headings) == 0 || len(headings) > len(released) || !reflect.DeepEqual(headings, released[:len(headings)]) {
t.Fatalf("version headings = %q, want the newest releases of CHANGELOG.md in order, starting at %q", headings, released[0])
}
for _, heading := range headings {
version := heading[len("## ["):strings.Index(heading, "]")]
t.Run(version, func(t *testing.T) {
assertSameShape(t, Section(english, version), Section(translated, version))
assertSameHighlights(t, Highlights(english, version), Highlights(translated, version))
})
}
assertSameShape(t, Section(english, version), Section(translated, version))
assertSameHighlights(t, Highlights(english, version), Highlights(translated, version))
})
}
}
Expand All @@ -60,15 +66,18 @@ func versionHeadings(changelog string) []string {
return out
}

func latestReleaseHeading(t *testing.T, changelog string) string {
func releaseHeadings(t *testing.T, changelog string) []string {
t.Helper()
var out []string
for _, heading := range versionHeadings(changelog) {
if !strings.HasPrefix(heading, "## [Unreleased]") {
return heading
out = append(out, heading)
}
}
t.Fatal("CHANGELOG.md has no released version")
return ""
if len(out) == 0 {
t.Fatal("CHANGELOG.md has no released version")
}
return out
}

func assertSameShape(t *testing.T, english, translated []Group) {
Expand Down
Loading