Files
Codeman/test/git-fetch-i18n.test.ts
T
Codeman maintainer c04fa97f78 chore: replace the em-dashes this release added with plain punctuation
The merged PRs (mostly #714's git fetch) added about 120 em-dashes in
comments, docs, test names and three settings strings. Each one is now a
comma, colon, semicolon or parentheses. The English settings strings in
index.html and their i18n.js keys change together, and the zh-CN value
for the fetch interval uses full-width parentheses instead of a double
dash. The one left is the regex in constants.js that matches a dash
typed as a word, which is behaviour, not punctuation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 09:11:19 +01:00

102 lines
4.5 KiB
TypeScript

/**
* @fileoverview The Git fetch interval rows (App Settings → Bottom bar, Session Options → Git) in Chinese
* (zh-CN): each label and description runs through the real translator over the real index.html in JSDOM and
* reads in Chinese with no English left beyond the product name and the technical names Git / git fetch, and
* every new key (the save toast in session-ui.js included) is in the dictionary exactly once (a repeated key
* in the object literal silently replaces the first).
*
* The Git tab's Status row is not here: its text is written at runtime by updateGitFetchStatus and carries
* data-i18n-skip, so translating it would rewrite the stamp the code builds. 'sec' is not a key either: only
* whole unit words are ('hours'), so 'sec' stays as written.
*
* Port: N/A.
*/
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import vm from 'node:vm';
import { JSDOM } from 'jsdom';
import { afterAll, describe, expect, it } from 'vitest';
const PUBLIC = resolve(import.meta.dirname, '../src/web/public');
const I18N = readFileSync(resolve(PUBLIC, 'i18n.js'), 'utf8');
const INDEX = readFileSync(resolve(PUBLIC, 'index.html'), 'utf8');
// Keys that contain a quote are written escaped in the object literal, so the dictionary is counted unescaped.
const DICT = I18N.replace(/\\'/g, "'").replace(/\\"/g, '"');
type Api = { configure(o: Record<string, string>): void; start(): void };
const ROWS = [
'appSettingsGitFetchEnabled',
'appSettingsGitFetchIntervalSeconds',
'modalGitFetchFollow',
'modalGitFetchEnabled',
'modalGitFetchInterval',
];
/** What may stay Latin: the product names, and Git / git fetch as technical names. */
const leftover = (text: string) =>
text.replace(/\b(Claude Code|Claude|Codeman|Ralph|Git|git|fetch)\b/g, '').match(/[A-Za-z]+/g) ?? [];
function rowStrings(doc: Document, id: string): { label: string; description: string } {
const row = doc.getElementById(id)!.closest('.set-row')!;
const label = row.querySelector('.set-row-label')!;
// The row's own scope marker ('synced') rides inside the label; the dictionary key is the label without it.
const scope = label.querySelector('.set-scope')?.textContent ?? '';
return {
label: label.textContent!.replace(scope, '').trim(),
description: row.querySelector('.set-row-desc')!.textContent!.trim(),
};
}
/** The Git section's blurb, which is not a row. */
function blurb(doc: Document): string {
const section = doc.getElementById('git-tab')!;
return section.querySelector('.set-section-blurb')!.textContent!.trim();
}
const source = new JSDOM(INDEX);
const dom = new JSDOM(INDEX, { runScripts: 'outside-only', url: 'http://localhost/' });
vm.runInContext(I18N, dom.getInternalVMContext(), { filename: 'i18n.js' });
const api = (dom.window as unknown as { CodemanI18n: Api }).CodemanI18n;
api.start();
api.configure({ language: 'zh-CN' });
afterAll(() => {
source.window.close();
dom.window.close();
});
describe('the Git fetch rows in zh-CN', () => {
it.each(ROWS)('translates the %s row, with no English left', (id) => {
const english = rowStrings(source.window.document, id);
const chinese = rowStrings(dom.window.document, id);
expect(english.label.length).toBeGreaterThan(0);
for (const key of ['label', 'description'] as const) {
expect(chinese[key]).not.toBe(english[key]);
expect(leftover(chinese[key])).toEqual([]);
}
});
it('translates the Git section blurb too, which is not a row', () => {
expect(blurb(dom.window.document)).not.toBe(blurb(source.window.document));
expect(leftover(blurb(dom.window.document))).toEqual([]);
});
it("leaves the Status row's runtime text to the code that writes it", () => {
// updateGitFetchStatus builds this row's sentence; a dictionary key for it would overwrite the stamp.
expect(INDEX).toContain('id="gitFetchStatus" data-i18n-skip hidden');
});
it('adds each key to the dictionary once, the save toast included', () => {
const texts = ROWS.flatMap((id) => Object.values(rowStrings(source.window.document, id)));
texts.push(blurb(source.window.document), 'Git fetch settings saved');
// 'sec' is deliberately not a key: only whole unit words are, so the unit stays as the markup writes it.
expect(DICT).not.toMatch(/\n sec: /);
for (const text of texts) {
const quoted = DICT.split(`'${text}'`).length + DICT.split(`"${text}"`).length - 2;
const bare = /^\w+$/.test(text) ? DICT.split(`\n ${text}: `).length - 1 : 0;
expect(quoted + bare, text).toBe(1);
}
});
});