버전 업데이트에 대비한 번역 비교 도구와 유지 절차 추가

새 원문과 이전 한국어를 키별로 비교해 추가와 변경 및 삭제를 구분한다. 배포 파일 변경 비교와 기준 해시 및 후속 작업 문서를 추가했다. 자동 검사 4개와 UI 문구 1496개 비교 및 기존 배포 파일 492개 무결성 검사를 통과했다. 운영 앱과 DB는 변경하지 않았으며 기존 배포 커밋 eddda22를 보존한다. 코드와 도움말 이식 및 새 버전 실행 검증은 대상 버전 작업 때 수행한다.
This commit is contained in:
블리어
2026-09-20 20:30:00 +09:00
parent eddda22eb6
commit 426a416e7b
8 changed files with 226 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
# Trilium 한국어판 작업 기준
## 2026-09-20 추가 번역 준비
대상 저장소는 `syskiller/triliumnext-korean-patch`다. 운영 기준은 0.95.0이며 배포판은 2.0.0이다. 최신 버전으로 이식한 상태는 아니다.
다음 작업은 `docs/TRANSLATION-MAINTENANCE.ko.md`에서 시작한다. `localization/baseline.json`에 기존 UI 번역 기준과 해시를 기록했다. 새 원문의 추가와 변경 및 삭제를 비교하는 도구와 배포 파일 변경을 비교하는 도구를 추가했다.
기존 UI 사전 자체 비교 결과는 재사용 후보 1482개와 원문 동일 검토 항목 14개다. 원문 동일 항목은 고유 명칭일 수 있으므로 미번역으로 단정하지 않는다. 서버 사전의 이전 영문은 별도로 확보해야 한다.
기존 이미지 유지와 한국어 설명 보완 정책을 유지한다. 코드 식별자와 URL은 원문을 유지한다. 직접 수정한 번들은 다른 버전에 복사하지 않는다. 운영 코드와 DB 변경 및 데이터 이전은 없다. 이번 작업은 도구와 문서 추가이므로 운영 데이터 백업을 새로 만들지 않는다. 기존 배포 커밋은 `eddda22`다.
+2
View File
@@ -76,6 +76,8 @@ bash install.sh
## 검증과 재생성
다음 버전의 추가 번역은 [버전 업데이트와 추가 번역 절차](docs/TRANSLATION-MAINTENANCE.ko.md)를 따릅니다. 기존 한국어와 새 원문을 비교하는 도구 및 코드와 도움말 파일의 변경 확인 도구를 제공합니다. 현재 배포판의 지원 버전은 그대로 0.95.0입니다.
검증 결과는 [배포 검증 기록](docs/DEPLOYMENT-TESTS.ko.md)에 있습니다. 배포 파일은 Docker 빌드 중 SHA-256 검사와 JavaScript 구문 검사를 통과해야 합니다. 개발 환경에 Node.js가 있다면 다음 명령으로도 확인할 수 있습니다.
```bash
+80
View File
@@ -0,0 +1,80 @@
# 버전 업데이트와 추가 번역
이 저장소는 Trilium 한국어 배포판 전용이다. 게임 저장소나 게임 문서에 작업 기록을 넣지 않는다.
현재 운영 기준은 Trilium 0.95.0과 한국어 배포판 2.0.0이다. 다음 버전 번역을 이어 갈 준비를 위한 도구이며 최신 Trilium 지원을 완료한 것은 아니다. 기존 배포 파일과 운영 데이터는 이 도구로 변경하지 않는다.
## 이어서 작업할 자료
| 대상 | 기준 자료 | 다음 버전에서 할 일 |
|---|---|---|
| UI 번역 | `localization/baseline.json`의 영문과 한국어 파일 | 키와 원문을 비교한 뒤 새 문구와 변경된 문구 번역 |
| 서버 번역 | `release/app/assets/translations/ko/server.json` | 같은 버전의 공식 영문을 확보한 뒤 비교 도구에 명시적으로 전달 |
| 직접 수정한 코드 | `release/app/main.cjs`와 `release/app/public/src` | 새 버전 원본 소스에서 해당 기능을 찾아 번역 키로 수정 |
| 도움말 | `release/app/assets/doc_notes/en` | 원본 문서의 변경 내용과 내부 링크를 대조 |
| 예제와 PDF | `tools/demo-source`와 `tools/build-demo-archive.py` | 새 예제 구조와 노트 연결을 확인한 뒤 새 설치 환경에서 검사 |
| 이미지 | 도움말 이미지와 한국어 설명 | 원본 이미지를 유지하고 설명을 보완하는 기존 정책 유지 |
| 변경 이력 | `updates/2026-09-05` | 과거 작업의 참고 자료로 보존 |
`tools/build-release.mjs`는 0.95.0용 과거 작업 결과를 다시 모으는 도구다. 새 버전 이식 도구로 사용하지 않는다. 예제 생성 도구도 기존 노트 구조에 맞춰져 있으므로 새 버전에 바로 적용하지 않는다.
## 1. 원본과 작업 기준 확보
운영과 분리된 작업 폴더에서 대상 버전의 공식 소스와 배포 이미지를 확보한다. 버전과 커밋 및 이미지 다이제스트를 기록한다. 작업 브랜치는 `translation/대상버전` 형태로 구분한다. 기존 데이터와 비밀 파일은 저장소에 넣지 않는다.
영문 파일은 반드시 대상 버전에서 가져온다. 현재 저장소에 이전 서버 영문 사전은 없으므로 한국어 파일을 영문 기준으로 간주하면 안 된다. UI 기준은 `localization/baseline.json`에 경로와 SHA-256을 고정했다.
## 2. UI 문구 비교
Node.js 20 이상에서 저장소 루트를 기준으로 실행한다. 출력 폴더는 아직 존재하지 않는 경로를 지정한다.
```bash
node tools/translation-upgrade.mjs 대상버전 /작업/새버전/translation.en.json /작업/번역검토
```
다른 사전이나 이후 버전의 검토 완료 자료를 기준으로 삼을 때는 이전 영문과 한국어를 함께 지정한다.
```bash
node tools/translation-upgrade.mjs 대상버전 /작업/새영문.json /작업/서버검토 /작업/이전영문.json /작업/이전한국어.json
```
`review.json`에는 키 경로와 새 원문 및 이전 원문과 한국어 후보가 함께 들어간다.
| 상태 | 처리 |
|---|---|
| `reusable` | 원문과 자리표시자가 같아 재사용 후보로 분류됨. 화면 문맥은 확인해야 함 |
| `added` | 새 키. 한국어를 새로 작성 |
| `source-changed` | 원문이 바뀜. 이전 한국어를 참고해 다시 번역 |
| `missing` | 한국어가 없거나 빈 문자열임 |
| `placeholder-mismatch` | 변수 자리표시자가 다름. 적용 전 수정 |
| `review-identical` | 원문과 번역이 같음. 고유 명칭인지 미번역인지 확인 |
| `removed` | 새 사전에서 없어짐. 이전 기록에만 보존 |
이 도구는 i18next 중괄호 변수와 템플릿 변수 및 일반 printf 자리표시자를 비교한다. 모든 메시지 문법이나 HTML 및 복수형 의미를 검증하지는 않는다. 새 원문과 한국어를 검토한 뒤 대상 버전의 번역 파일에 반영한다. JSON 키를 바꾸거나 원문이 바뀐 문구에 이전 한국어를 일괄 덮어쓰지 않는다.
## 3. 코드와 도움말 수정 대상 비교
공식 이전 이미지와 새 이미지에서 앱 파일을 각각 별도 폴더로 추출한다. 두 폴더 모두 `main.cjs`와 `assets` 등이 바로 아래에 있어야 한다.
```bash
node tools/translation-files.mjs /작업/이전원본앱 /작업/새원본앱 /작업/파일비교.json
```
현재 배포판의 수정 대상 492개 경로를 비교한다. 변경되거나 이동한 파일은 수동 검토 대상으로 삼는다. 한국어판에만 있는 파일도 별도로 표시한다. 새 버전에 추가된 다른 파일은 이 목록에 없으므로 공식 소스의 버전 간 Git diff도 함께 확인한다.
빌드된 `main.cjs`를 새 버전에 덮어쓰지 않는다. 바뀐 기능을 원본 소스에서 찾아 번역 기능을 연결하고 다시 빌드한다. 공식 기여를 준비할 때도 UI 사전과 번역 기능 추가 및 도움말 변경을 나누어 검토한다.
## 4. 검증과 배포
1. 자연스러운 한국어와 변수 및 링크를 검사한다. 문장을 잇기 위한 쉼표를 사용하지 않는다.
2. 대상 버전 소스의 빌드와 관련 검사를 수행한다.
3. 별도 Docker 환경에서 신규 설치와 한국어 선택 및 기본 예제를 확인한다.
4. 운영 데이터의 백업 사본으로 업데이트와 첨부 파일 및 검색을 확인한다.
5. 실패 시 이전 이미지와 해당 버전 데이터 백업으로 복원할 수 있는지 검사한다.
6. 기반 이미지와 배포 버전 및 생성 도구를 새 버전에 맞춰 수정한다. 파일 해시 검증과 실제 화면 확인을 마친 뒤 배포한다.
## 5. 다음 작업을 위한 기록
검토를 마친 새 영문과 한국어 사전의 경로와 해시를 새 기준으로 기록한다. 이전 기준은 Git 기록에 보존한다. `PROJECT_MEMORY.md`와 `수정기록.txt`에 버전과 변경 내용 및 검사 결과와 배포 여부를 남긴다. 번역 검토 자료는 비밀값이 없는지 확인한 뒤 해당 버전 폴더에 보관한다. 운영 노트를 기록할 때는 Trilium의 `Ubuntu 서버 운영` 아래 기존 Trilium 분류를 찾아 사용한다.
이번 준비 작업은 운영 앱과 데이터베이스를 변경하지 않는다. 새 버전 자동 배포나 자동 번역을 예약하지 않는다.
+7
View File
@@ -0,0 +1,7 @@
{
"appVersion": "0.95.0",
"koreanSha256": "e1d34275426dabf9aabf2f9b3ec880157946975f9f3f5a9035cf412ce02c9114",
"english": "updates/2026-09-05/work/trilium/scoped/translation.en.json",
"englishSha256": "3a08afa9a2215e53245466bc2ca84e5c2ff24edbfce0ee25577ff680cdb332ce",
"korean": "release/app/public/translations/ko/translation.json"
}
+51
View File
@@ -0,0 +1,51 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { compare } from './translation-upgrade.mjs';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
test('추가와 변경 및 삭제를 구분하고 기존 번역을 검토 후보로 보존한다', () => {
const result = compare({ same: 'Hello', changed: 'Old', gone: 'Gone' }, { same: '안녕하세요', changed: '이전', gone: '삭제' }, { same: 'Hello', changed: 'New', added: 'Add' });
assert.deepEqual(result.map(x => x.status), ['reusable', 'source-changed', 'added', 'removed']);
assert.equal(result[1].korean, '이전');
});
test('자리표시자 누락과 미번역을 자동 재사용에서 제외한다', () => {
const en = { a: 'Hi {{name}}', b: 'Name', c: 'Value', d: 'Brand' };
assert.deepEqual(compare(en, { a: '안녕', b: '', d: 'Brand' }, en).map(x => x.status), ['placeholder-mismatch', 'missing', 'missing', 'review-identical']);
});
test('점이 포함된 키와 중첩 키 및 배열을 구분한다', () => {
const en = { 'a.b': 'One', a: { b: 'Two' }, list: ['Three'] };
const ko = { 'a.b': '하나', a: { b: '둘' }, list: ['셋'] };
assert.equal(compare(en, ko, en).filter(x => x.status === 'reusable').length, 3);
});
test('파일 비교는 변경과 삭제 및 추가를 구분하고 기존 보고서를 덮어쓰지 않는다', () => {
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'trilium-translation-'));
try {
const old = path.join(temp, 'old'), next = path.join(temp, 'next');
fs.mkdirSync(old); fs.mkdirSync(next);
fs.writeFileSync(path.join(old, 'main.cjs'), 'old');
fs.writeFileSync(path.join(next, 'main.cjs'), 'next');
fs.mkdirSync(path.join(old, 'assets/db'), { recursive: true });
fs.writeFileSync(path.join(old, 'assets/db/demo.zip'), 'old-demo');
fs.mkdirSync(path.join(next, 'public/src'), { recursive: true });
fs.writeFileSync(path.join(next, 'public/src/ko.js'), 'next-ko');
const output = path.join(temp, 'report.json');
const tool = fileURLToPath(new URL('./translation-files.mjs', import.meta.url));
const run = () => spawnSync(process.execPath, [tool, old, next, output], { encoding: 'utf8' });
assert.equal(run().status, 0);
const saved = fs.readFileSync(output, 'utf8');
const entries = JSON.parse(saved).files;
assert.equal(entries.find(x => x.file === 'main.cjs').status, 'upstream-changed');
assert.equal(entries.find(x => x.file === 'assets/db/demo.zip').status, 'removed-or-moved');
assert.equal(entries.find(x => x.file === 'public/src/ko.js').status, 'new-upstream-path');
assert.notEqual(run().status, 0);
assert.equal(fs.readFileSync(output, 'utf8'), saved);
} finally {
// 이 검사에서 만든 임시 폴더만 정리한다. 사용자 자료 경로는 받지 않는다.
fs.rmSync(temp, { recursive: true, force: true });
}
});
+19
View File
@@ -0,0 +1,19 @@
import fs from 'node:fs';
import path from 'node:path';
import crypto from 'node:crypto';
import { fileURLToPath } from 'node:url';
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const [oldRoot, nextRoot, output] = process.argv.slice(2);
if (!oldRoot || !nextRoot || !output) throw new Error('사용법: node tools/translation-files.mjs 이전원본앱폴더 새원본앱폴더 새보고서JSON');
for (const dir of [oldRoot, nextRoot]) if (!fs.statSync(dir).isDirectory()) throw new Error(`앱 폴더가 아닙니다: ${dir}`);
const digest = file => fs.existsSync(file) && fs.statSync(file).isFile() ? crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex') : null;
const manifest = JSON.parse(fs.readFileSync(path.join(root, 'release/manifest.json'), 'utf8'));
// 배포판의 수정 대상만 검사한다. 새로 추가된 전체 파일은 원본 소스의 Git 비교로 별도 확인해야 한다.
const files = Object.keys(manifest.files).map(file => {
const before = digest(path.join(oldRoot, file)), after = digest(path.join(nextRoot, file));
const status = !before ? (after ? 'new-upstream-path' : 'local-only') : !after ? 'removed-or-moved' : before === after ? 'upstream-unchanged' : 'upstream-changed';
return { file, status, previousSha256: before, nextSha256: after, koreanSha256: manifest.files[file].sha256 };
});
fs.writeFileSync(output, JSON.stringify({ baselineVersion: manifest.appVersion, warning: '같은 경로의 변경 여부만 확인합니다. 번역 패치 적용 가능 여부와 실행 호환성을 보장하지 않습니다.', files }, null, 2) + '\n', { flag: 'wx' });
console.log(`수정 대상 ${files.length}개 비교 완료`);
+48
View File
@@ -0,0 +1,48 @@
import fs from 'node:fs';
import path from 'node:path';
import crypto from 'node:crypto';
import { fileURLToPath } from 'node:url';
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const read = p => JSON.parse(fs.readFileSync(p, 'utf8'));
const hash = p => crypto.createHash('sha256').update(fs.readFileSync(p)).digest('hex');
// 경로를 배열로 보관하므로 점이 들어간 번역 키도 다른 키와 혼동하지 않는다.
export function flatten(value, parts = [], result = new Map()) {
if (typeof value === 'string') result.set(JSON.stringify(parts), value);
else if (value && typeof value === 'object') {
for (const [key, child] of Object.entries(value)) flatten(child, [...parts, key], result);
} else throw new Error(`문자열이 아닌 번역 값: ${JSON.stringify(parts)}`);
return result;
}
const tokens = text => (text.match(/\{\{[^}]+\}\}|\$\{[^}]+\}|%(?:\d+\$)?[sdif]/g) || []).sort();
export function compare(previous, korean, next) {
const before = flatten(previous), translated = flatten(korean), after = flatten(next);
const entries = [];
for (const [key, source] of after) {
const old = before.get(key), ko = translated.get(key);
let status = old === undefined ? 'added' : old !== source ? 'source-changed' : !ko ? 'missing' : ko === source ? 'review-identical' : 'reusable';
if (status === 'reusable' && JSON.stringify(tokens(source)) !== JSON.stringify(tokens(ko))) status = 'placeholder-mismatch';
entries.push({ path: JSON.parse(key), source, previousSource: old ?? null, korean: ko ?? null, status });
}
for (const [key, source] of before) if (!after.has(key)) entries.push({ path: JSON.parse(key), previousSource: source, korean: translated.get(key) ?? null, status: 'removed' });
return entries;
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
const [version, nextFile, output, previousFile, koreanFile] = process.argv.slice(2);
if (!version || !nextFile || !output) throw new Error('사용법: node tools/translation-upgrade.mjs 대상버전 새영문JSON 새출력폴더 [이전영문JSON 이전한국어JSON]');
if (!!previousFile !== !!koreanFile) throw new Error('이전 영문과 한국어 파일은 함께 지정해야 합니다.');
const baseline = read(path.join(root, 'localization/baseline.json'));
const oldPath = previousFile || path.join(root, baseline.english);
const koPath = koreanFile || path.join(root, baseline.korean);
if (!previousFile && (hash(oldPath) !== baseline.englishSha256 || hash(koPath) !== baseline.koreanSha256)) throw new Error('기준 번역 파일이 변경됐습니다. 기준 메타데이터를 검토하세요.');
const entries = compare(read(oldPath), read(koPath), read(nextFile));
const counts = {};
for (const entry of entries) counts[entry.status] = (counts[entry.status] || 0) + 1;
// 기존 검토 내용이 사라지지 않도록 출력 폴더는 새 경로만 허용한다.
fs.mkdirSync(output, { recursive: false });
const metadata = { targetVersion: version, baselineVersion: previousFile ? '사용자 지정 기준' : baseline.appVersion, sourceSha256: hash(nextFile), previousSha256: hash(oldPath), koreanSha256: hash(koPath), counts };
fs.writeFileSync(path.join(output, 'review.json'), JSON.stringify({ ...metadata, entries }, null, 2) + '\n');
fs.writeFileSync(path.join(output, 'SUMMARY.ko.md'), `# ${version} 번역 비교\n\n${Object.entries(counts).map(([key, count]) => `- ${key}: ${count}`).join('\n')}\n\nreview.json의 한국어는 검토용 후보입니다. 원문이 바뀐 항목은 기존 번역을 그대로 적용하지 마세요. 이 도구는 배포 파일과 운영 데이터를 변경하지 않습니다.\n`);
console.log(JSON.stringify(metadata, null, 2));
}
+8
View File
@@ -0,0 +1,8 @@
2026-09-20 버전별 추가 번역 준비
변경 이유: 다음 Trilium 버전에서 기존 번역을 다시 찾는 시간을 줄이고 원문 변경을 놓치지 않기 위해 준비했다.
주요 파일: tools/translation-upgrade.mjs, tools/translation-files.mjs, tools/test-translation-upgrade.mjs, localization/baseline.json, docs/TRANSLATION-MAINTENANCE.ko.md, PROJECT_MEMORY.md, README.md
검사: 번역 추가와 변경 및 삭제 분류 검사 통과. 자리표시자 불일치와 미번역 분류 검사 통과. 점이 포함된 키와 중첩 키 및 배열 구분 검사 통과. 실제 UI 기준 1496개 비교 완료.
운영 반영: 운영 앱과 DB 변경 없음. 데이터 이전 없음. 배포 도구와 번역 절차만 추가.
복구 기준: 기존 배포 커밋 eddda22와 release/manifest.json 유지. 운영 데이터를 수정하지 않아 신규 운영 백업 없음.
제한: 코드와 도움말은 수동 검토가 필요하다. 최신 버전 지원과 공식 프로젝트 기여 완료를 뜻하지 않는다.