Files

78 lines
11 KiB
JavaScript

import fs from 'node:fs';
import path from 'node:path';
import crypto from 'node:crypto';
const b='work/trilium',src=b+'/phase3/doc_notes/en',out=b+'/phase8';
fs.mkdirSync(out,{recursive:true});
const hash=s=>crypto.createHash('sha256').update(s).digest('hex');
const files={},previous={},audit=[];
function save(rel,old,value){const dest=out+'/'+rel;fs.mkdirSync(path.dirname(dest),{recursive:true});fs.writeFileSync(dest,value);files[rel]=hash(value);previous[rel]=hash(old);}
const en=JSON.parse(fs.readFileSync(b+'/scoped/translation.en.json')),ko=JSON.parse(fs.readFileSync(b+'/phase6/translation.json'));
const glossary=new Map();
function walk(a,c){for(const[k,v]of Object.entries(a)){if(typeof v==='object'&&v)walk(v,c?.[k]||{});else if(typeof v==='string'&&typeof c?.[k]==='string'&&/^[A-Za-z][A-Za-z /-]{2,48}$/.test(v)&&/[가-힣]/.test(c[k])&&!/[<>{}]/.test(c[k]))glossary.set(v,c[k]);}}
walk(en,ko);
const dashboardMap={
'🏠 Trilium Overview':'🏠 Trilium 개요','Current Trilium version and build information':'현재 Trilium 버전과 빌드 정보','📋 Instance Information':'📋 실행 환경 정보','Database file size in human-readable format':'읽기 쉬운 단위로 표시한 데이터베이스 파일 크기','💾 Database Size':'💾 데이터베이스 크기','Total active notes in your Trilium instance':'현재 Trilium에서 사용 중인 전체 노트 수','📝 Active Notes':'📝 사용 중인 노트','📊 Key Metrics':'📊 주요 지표','Total notes including deleted ones':'삭제된 노트를 포함한 전체 노트 수','Active Notes':'사용 중인 노트','Deleted Notes':'삭제된 노트','Protected Notes':'보호된 노트','📝 Notes Distribution':'📝 노트 분포','Breakdown of attachments by MIME type':'MIME 유형별 첨부 파일 분포','🖼️ Attachments by Type':'🖼️ 유형별 첨부 파일','Distribution of notes by their content type':'내용 유형별 노트 분포','📄 Notes by Content Type':'📄 내용 유형별 노트','📈 Trends & Time Series':'📈 시간에 따른 변화','Growth of notes over time':'시간에 따른 노트 수 변화','Total Notes':'전체 노트','📈 Notes Growth Over Time':'📈 시간에 따른 노트 수 변화','Attachment storage trends':'첨부 파일 저장량 변화','Active Attachments':'사용 중인 첨부 파일','Total Attachments':'전체 첨부 파일','📎 Attachments Growth Over Time':'📎 시간에 따른 첨부 파일 수 변화','🔧 Advanced Metrics':'🔧 고급 지표','Number of branches connecting notes':'노트를 연결하는 가지 수','🌳 Total Branches':'🌳 전체 가지','Number of note attributes':'노트 속성 수','🏷️ Attributes':'🏷️ 속성','Number of note revisions':'노트 수정 이력 수','🔄 Revisions':'🔄 수정 이력','Number of ETAPI tokens':'ETAPI 토큰 수','🔑 API Tokens':'🔑 API 토큰','Various storage and system metrics':'저장 공간과 시스템의 주요 지표','Blob Records':'본문 데이터 레코드','Recent Notes':'최근 노트','Embeddings':'임베딩','Embedding Providers':'임베딩 제공자','📊 Storage & System Metrics':'📊 저장 공간과 시스템 지표','Timeline showing when content was created and last modified':'콘텐츠 생성 시각과 마지막 수정 시각','Oldest Note':'가장 오래된 노트','Newest Note':'가장 최근 노트','Last Modified':'마지막 수정','⏰ Content Timeline':'⏰ 콘텐츠 생성 및 수정 시각','TriliumNext Dashboard':'TriliumNext 대시보드','Build Date':'빌드 날짜','Git Revision':'Git 커밋','Sync Version':'동기화 버전','DB Version':'데이터베이스 버전','Version':'버전','Datasource':'데이터 소스','Instance':'실행 환경','Job':'작업'};
for(const[k,v]of Object.entries(dashboardMap))glossary.set(k.replace(/^\P{L}+/u,''),v.replace(/^\P{L}+/u,''));
const ocr=JSON.parse(fs.readFileSync(b+'/help-image-ocr.json','utf8').replace(/^\uFEFF/,''));
const byFile=new Map(ocr.filter(x=>x.latinWords>0).map(x=>[x.file,x]));
const esc=s=>s.replaceAll('&','&amp;').replaceAll('<','&lt;').replaceAll('>','&gt;').replaceAll('"','&quot;');
const used=new Set();
const explanations={
'표시점의 아이콘과 색상':'지도 위 표시점은 연결된 노트의 아이콘과 색상을 따릅니다. 표시점을 누르면 해당 노트를 열 수 있습니다.',
'GPX 파일 가져오기':'GPX는 이동 경로를 저장하는 파일 형식입니다. 경로 파일을 지도 노트로 가져오는 과정을 보여 줍니다.',
'지도에 표시된 경로':'가져온 경로가 지도 위의 선으로 표시됩니다. 지도 지명과 경로 이름은 예제 데이터입니다.',
'문단 도구 모음':'문단 도구 모음에서 제목 단계와 목록 및 정렬 방식을 고릅니다. 아이콘 위에 마우스를 올리면 기능 이름을 확인할 수 있습니다.',
'글자 도구 모음':'선택한 글자에 굵게와 기울임 및 밑줄 같은 서식을 적용하는 도구 모음입니다.',
'테마 보관용 노트':'사용자 지정 테마 코드를 보관할 노트입니다. 노트 제목과 CSS 코드를 구분해서 입력하세요.',
'탭에 표시된 작업 공간':'현재 작업 공간의 이름이 탭에 표시됩니다. 작업 공간을 바꾸면 해당 공간에 저장된 탭 구성을 불러옵니다.',
'실행 도구 모음 설정':'실행 도구 모음에 표시할 항목을 설정하는 화면입니다. 각 실행 도구가 어떤 노트나 기능을 여는지 본문 설명과 함께 확인하세요.',
'안내 상자 메뉴':'일반 인용문과 주의 사항 등의 안내 상자 모양을 고르는 메뉴입니다.',
'JavaScript 구문 강조를 적용한 코드 블록':'코드 언어를 JavaScript로 지정한 예시입니다. 색으로 구분된 영문은 실행할 코드이므로 그대로 유지합니다.',
'각주 예시':'본문의 작은 각주 번호와 아래쪽 각주 내용이 연결된 모습입니다. 예문 대신 원하는 설명을 입력하세요.',
'글자 서식 팝업':'글자를 선택하면 나타나는 서식 메뉴입니다. 원하는 아이콘을 눌러 선택한 부분의 서식을 바꿉니다.',
'색상 선택':'글자색이나 배경색을 고르는 화면입니다. 색상 견본에서 원하는 색을 선택합니다.',
'이모지 선택기':'본문에 넣을 이모지를 검색하고 선택하는 창입니다. 현재 앱의 이모지 이름은 한국어로 표시됩니다.',
'이모지 자동 완성':'이모지 이름을 입력했을 때 표시되는 후보 목록입니다. 원하는 후보를 선택하면 본문에 이모지가 들어갑니다.',
'본문 안의 Mermaid 도표':'Mermaid 문법으로 작성한 도표를 본문 안에 표시한 예시입니다. 도표의 글자는 코드에 입력한 내용에 따라 달라집니다.',
'가로 구분선':'문단 사이에 가로선을 넣어 내용을 구분한 예시입니다.',
'페이지 나누기 표시':'인쇄할 때 새 페이지를 시작할 위치를 표시한 예시입니다.',
'수식 입력 창':'수식 원문을 입력하고 결과를 확인하는 창입니다. 수학 기호와 LaTeX 명령은 번역하지 않고 입력합니다.',
'들여쓰기 예시':'문단의 시작 위치를 안쪽으로 옮긴 모습입니다. 들여쓰기 늘리기와 줄이기로 깊이를 조절합니다.',
'Ollama 정상 실행 응답':'Ollama 서버가 실행 중임을 알리는 응답입니다. 이 응답이 보이면 본문에 안내된 주소를 연결 설정에 사용합니다.',
'여러 날짜를 선택해 일정 만들기':'달력에서 날짜 범위를 선택해 여러 날에 걸친 일정을 만드는 예시입니다.',
'일정에 속성 표시':'달력의 일정에 노트 속성 값을 함께 표시한 모습입니다. 예제의 속성 이름과 값을 본문 설정과 비교하세요.',
'여러 노트 선택':'노트 트리에서 여러 항목이 동시에 선택된 모습입니다. 선택한 노트들에 같은 작업을 적용할 수 있습니다.',
'코드 블록 예시':'코드 블록에 입력한 프로그램 예시입니다. 영문 명령과 기호는 프로그램 문법이므로 원래 표기를 유지합니다.'
};
for(const rel0 of fs.readdirSync(src,{recursive:true}).filter(p=>p.endsWith('.html'))){
const rel=rel0.replaceAll('\\','/'),old=fs.readFileSync(src+'/'+rel,'utf8');let idx=0;
const annotations=[];
const html=old.replace(/<img\b[^>]*>/gi,tag=>{
idx++;const url=tag.match(/\bsrc="([^"]+)"/i)?.[1];if(!url)return tag;
let img;try{img=path.posix.normalize(path.posix.join(path.posix.dirname(rel),decodeURIComponent(url.replaceAll('&amp;','&'))));}catch{return tag;}
const record=byFile.get(img);if(!record)return tag;
used.add(img);
const alt=tag.match(/\balt="([^"]*)"/i)?.[1]||'';
const pairs=[...glossary].filter(([k])=>new RegExp('(?:^|[^a-z])'+k.replace(/[.*+?^${}()|[\]\\]/g,'\\$&')+'(?:$|[^a-z])','i').test(record.text)).sort((a,c)=>c[0].length-a[0].length).filter((entry,i,all)=>!all.slice(0,i).some(([k])=>k.toLowerCase().includes(entry[0].toLowerCase()))).slice(0,12);
annotations.push({image:img,number:idx,description:alt,pairs});
return tag;
});
if(!annotations.length)continue;
let appendix='<section class="korean-image-guide"><h2>예전 화면 이미지 읽는 법</h2><p>아래 설명은 본문에 나온 이미지 순서에 맞췄습니다. 이미지는 원본을 유지했으므로 영문 메뉴가 보일 수 있습니다. 현재 앱에서는 한국어 메뉴를 사용합니다. 코드와 주소 및 제품 이름은 원래 표기를 따릅니다.</p>';
for(const a of annotations){appendix+=`<h3>이미지 ${a.number}${/[가-힣]/.test(a.description)?' · '+a.description:''}</h3>`;if(a.pairs.length)appendix+='<p>화면에서 사용하는 주요 표현은 다음과 같습니다.</p><ul>'+a.pairs.map(([e,k])=>`<li><span lang="en">${esc(e)}</span>: ${esc(k)}</li>`).join('')+'</ul>';else {if(!explanations[a.description])throw Error('Missing image explanation '+a.image);appendix+='<p>'+esc(explanations[a.description])+'</p>';}}
appendix+='</section>';
const updated=/<\/body>/i.test(html)?html.replace(/<\/body>/i,appendix+'</body>'):html+'\n'+appendix+'\n';
const links=s=>[...s.matchAll(/\b(?:href|src)="[^"]*"/g)].map(x=>x[0]).sort();
if(JSON.stringify(links(old))!==JSON.stringify(links(updated)))throw Error('Link changed '+rel);
save('doc_notes/en/'+rel,old,updated);audit.push({file:rel,images:annotations});
}
const dashboard='User Guide/User Guide/Advanced Usage/Metrics/grafana-dashboard.json';
const oldDashboard=fs.readFileSync(src+'/'+dashboard,'utf8');let replaced=0;
const translated=JSON.parse(oldDashboard,(key,v)=>typeof v==='string'&&Object.hasOwn(dashboardMap,v)?(replaced++,dashboardMap[v]):v);
save('doc_notes/en/'+dashboard,oldDashboard,JSON.stringify(translated,null,2)+'\n');
const oldMain=fs.readFileSync(b+'/phase7/main.cjs','utf8');
const next=oldMain.replace('jg=`assets/v${CR.version}-ko-20260913-r6`','jg=`assets/v${CR.version}-ko-20260913-r7`');if(oldMain===next)throw Error('Cache marker missing');save('main.cjs',oldMain,next);
const report={htmlPages:audit.length,referencedImages:used.size,detectedImages:byFile.size,unreferenced:[...byFile.keys()].filter(k=>!used.has(k)),dashboardStrings:replaced,images:audit};
fs.writeFileSync(out+'/image-guide-audit.json',JSON.stringify(report,null,2));fs.writeFileSync(out+'/manifest.json',JSON.stringify({files,previous},null,2));
console.log({...report,images:undefined});