Skip to content

Latest commit

 

History

History
436 lines (359 loc) · 30.2 KB

ehnd-compat.md

File metadata and controls

436 lines (359 loc) · 30.2 KB

Ehnd 사전 호환

Ehnd는 이지트랜스 앞뒤에서 전처리 필터, 후처리 필터, SkipLayer, 사용자 사전을 적용하던 확장입니다. hdTrans는 Ehnd 폴더를 그대로 읽어 같은 순서와 의미로 적용합니다. 새로 사전을 만들 때는 Ehnd 파일 이름 대신 사전 폴더 안의 #! layer=prefilter, #! layer=postfilter 파일을 써도 됩니다 (hdTrans 필터 파일).

Ehnd 호환은 기능일 뿐입니다. 꿀도르 같은 Ehnd 사전은 dicts/에 들어 있지 않고 기본으로 켜지지 않습니다. 설정 파일의 ehnd, CLI -ehnd나 API로 명시적으로 켤 때만 씁니다. 켜면 번역 품질이 떨어질 수 있습니다.

이 문서는 Ehnd 코드가 파일을 읽고 적용하는 방식을 그대로 적고, hdTrans가 다르게 하는 점을 따로 밝힙니다. 기준은 Ehnd 공개 소스(EHND_VER "V3.20": filter.cpp, ehnd.cpp, watch.cpp, config.cpp)와, 꿀도르 사전이 요구하는 3.20 꿀도르 수정판의 행 확장(_TGL, _SkipLayer, _skip_sub, _MATCH_CHECK, _AUX:)입니다. 공개 소스에는 이 확장이 없어서 해당 줄이 _TGL 같은 리터럴 행으로 읽히고 아무 데도 맞지 않습니다.

쓰는 법

  • hdtrans-data/user/hdtrans.config.yaml에 ehnd: true를 적으면 hdtrans-data/ehnd/를 씁니다. ehnd: 경로는 지정한 폴더(상대 경로는 설정 파일 기준)를 씁니다. 생략하거나 ehnd: false이면 폴더가 있어도 쓰지 않습니다.
  • 라이브러리와 DLL은 켠 폴더를 userdict.tsv처럼 1초에 한 번까지 확인해, 바뀌면 사전과 함께 백그라운드에서 다시 읽습니다. 설정 자체를 바꾼 뒤에는 다시 초기화하거나 hdTrans_Reload, J2K_ReloadUserDict를 부릅니다.
  • CLI: hdtrans -dicts dicts -ehnd "C:\ezTrans XP\Ehnd"로 다른 폴더를 줍니다. 건너뛴 줄은 표준 오류로 알립니다.
  • C API: hdTrans_OpenEhnd(dir), hdTrans_FilterErrors(...) (api.md).
  • TypeScript: setEhnd(files)로 파일 이름과 내용을 넘깁니다. 이전 묶음 전체를 교체하고 다음 번역부터 반영합니다. clearEhnd()로 제거합니다(TypeScript API).
  • C++: Engine::OpenEhnd(dir)로 폴더를 정하면 그 뒤에 만들거나 Reset한 세션부터 씁니다. Reload()와 UserReload::Auto의 다시 읽기는 사전 폴더와 함께 Ehnd 폴더를 다시 읽고, FilterErrors()는 건너뛴 줄을 파일:줄: 이유로 돌려줍니다. 빈 경로는 Ehnd 폴더를 뗍니다. 정의: include/core/engine.h.
  • Ehnd 폴더도 필터 파일도 없으면 번역은 바이트 단위로 이전과 같습니다.

언제 적용되나

Ehnd의 J2K_TranslateMMNTW(ehnd.cpp):

  1. 입력 전체가 /reload, /log 같은 명령어이면 번역하지 않고 답합니다.
  2. 길이가 0이 아니면 요청 문자열 전체에 전처리 필터를 적용합니다(줄로 나누지 않음).
  3. UTF-16을 CP932로 바꿔(없는 글자는 ? 등) 이지트랜스로 번역합니다. 사용자 사전은 Ehnd가 만든 임시 파일로 바꿔치기되어 있습니다.
  4. CP949 결과를 UTF-16으로 바꿔 후처리 필터를 적용합니다.
  5. 3.20 꿀도르 수정판은 후처리 뒤에 토글을 선언 상태로 되돌립니다.

hdTrans:

  • 문서의 줄마다 전처리 → 번역(문서 문맥 포함) → 후처리를 합니다. 줄 끝의 \r은 떼고 적용한 뒤 다시 붙입니다. 빈 줄에는 둘 다 적용하지 않습니다.
  • 토글은 줄마다 선언 상태에서 시작하고, 전처리가 바꾼 상태가 같은 줄의 후처리에 이어집니다.
  • 전처리가 숫자나 라틴 문자와 그 뒤의 한자, 가타카나, 라틴 문자 단어 사이의 공백을 지우면, 전처리 뒤에 그 공백을 되돌립니다(ステージ3 ボス戦). hdTrans는 이 공백을 원문의 띄어쓰기로 읽기 때문입니다 (3 보스전, 1 레벨). 전처리 전후 줄을 글자 단위로 맞춰 보고(전각 영숫자는 반각과 같은 글자로 봅니다: 12 月 → 12月 → 12 月), 공백 양옆 글자가 둘 다 남아 붙어 있을 때만 되돌립니다. 두 줄의 같은 앞부분과 뒷부분을 뺀 가운데가 글자 쌍 2^22개를 넘으면 그 안의 공백은 전처리가 남긴 대로 둡니다.
  • 불러 온 전처리 행의 바꿀 말에는 있고 찾는 말에는 없는 글자 가운데 가나, 한자, 한글, ASCII, 숫자, 문장 부호, 기호, 공백이 아닌 것(ρ, ч, ʞ, ҡ, ħ, 결합 문자 U+033A)은 표지입니다. 표지 한 글자는 낱말로 읽지 않아 이웃에 붙어 나오고, 후처리가 그 꼴을 찾습니다(LAY-15). 원문에 쓴 같은 글자도 그렇습니다. 표지는 불러 온 파일에서 정하므로 다른 기호(①, α, +1)는 Ehnd를 켜도 낱말입니다.
  • 전처리 행이 찾는 말에 없이 가나 앞에 써 넣는 글자 가운데 가나(・, ー 포함), 한자, 한글, ASCII, 숫자, 문장 부호, 공백이 아닌 것(표지와 기호: (⊆るな의 ⊆) 바로 뒤의 미해석 가나는 전처리가 쓴 글이라 번역에 넣지 않습니다(MNG-5). 후처리는 그 자리를 쓴 그대로 찾습니다((⊆인). 전처리 전의 원문이 그 글자를 담고 있으면 그 호출에서는 이 규칙이 없습니다 (∴まりです。 그대로 ∴마리입니다.).
  • CP932, CP949 변환이 없으므로 그 손실도 없습니다. 명령어와 ehnd_conf.ini 스위치 (PREFILTER_SWITCH, POSTFILTER_SWITCH, USERDIC_SWITCH, JKDIC_SWITCH, EHNDWATCH_SWITCH, COMMAND_SWITCH, 로그 키)는 읽지 않습니다. 모두 켜진 상태로 동작합니다.
  • 사용자 사전 레코드는 번역의 단어 찾기에 들어갑니다(텍스트 경로와 -raw 경로 모두).

파일 찾기, 인코딩, 순서

  • 폴더: Ehnd는 <J2KEngine.dll 폴더>\Ehnd\(3.20은 FILTER_FILE_PATH 설정으로 바꿀 수 있음), hdTrans는 명시적으로 켠 hdtrans-data/ehnd/ 또는 지정한 폴더. 하위 폴더는 읽지 않습니다.
  • 파일: PreFilter*.txt, PostFilter*.txt, SkipLayer*.txt, UserDict*.txt. 이름은 대소문자를 가리지 않습니다. 접두사 뒤(_@Hdor, #0#req, _C 등)는 이름표일 뿐 뜻이 없고 순서에만 영향을 줍니다.
  • 순서: Ehnd는 FindFirstFile이 돌려주는 순서, 곧 NTFS에서는 대소문자를 무시한 이름 순입니다 (FAT에서는 만든 순서). hdTrans는 ASCII 대문자로 바꾼 이름의 바이트 순으로 정렬합니다. 꿀도르 순서:
    • PreFilter: @1Hdor_Set, @Custom, @Hdor, @Hdor_C
    • PostFilter: @Hdor, @Hdor_C, @Hdor_User
    • UserDict: @Custom, @Hdor#0#req, @Hdor#1, @Hdor#name
  • 인코딩: _wfopen(…, L"rt,ccs=UTF-8"). 바이트 순서 표시가 있으면 그에 따라 UTF-16LE 또는 UTF-8, 없으면 UTF-8입니다(꿀도르의 SkipLayer_@Hdor.txt는 UTF-16LE). 텍스트 모드라 CR LF는 LF가 되고, 홀로 있는 CR은 줄 안에 남습니다.
  • fgetws(buf, 1000, fp)라서 999자가 넘는 줄은 두 줄로 나뉘어 읽힙니다. hdTrans는 줄을 나누지 않습니다(꿀도르의 가장 긴 줄은 611자).
  • 종류마다 전역 줄 번호 g_line이 파일 순서대로 모든 물리 줄(주석 포함)을 셉니다. 이 번호가 같은 차수 안의 순서를 정하므로, 같은 차수 안에서는 파일 순서, 그다음 줄 순서입니다.

필터 행 (PreFilter, PostFilter)

두 종류는 문법과 적용 방식이 같고 적용 시점만 다릅니다.

열

SRC <TAB> DEST <TAB> LAYER [<TAB> REGEX [<TAB> _AUX:텍스트]] [<TAB> 무시…]
ヲ	ヲ	11100110	0
お堂(?![一-龥])	▂$0梦名	29101500	1	_AUX:お堂
  • 0열에서 시작하는 //만 주석입니다. 앞에 공백이 있으면 주석이 아닙니다.
  • 필드는 탭, 줄 끝의 \n, 문자열 끝, 그리고 //의 둘째 /에서 끝납니다. //는 줄도 끝냅니다. // 앞의 필드는 첫 /를 품습니다: REGEX 자리의 0//설명은 0/이고 숫자로 0입니다. 그래서 SRC와 DEST에는 //를 쓸 수 없습니다(http://가 든 정규식은 잘립니다).
  • 필드가 셋(SRC, DEST, LAYER) 이상인 줄만 행이 됩니다. 줄 끝에 \n이 있으면 마지막에 빈 필드가 하나 더 생기므로 가<TAB>나\n은 LAYER 0인 행이지만, 파일 끝에 개행 없이 있는 가<TAB>나는 버려집니다. 빈 줄도 버려집니다.
  • LAYER와 REGEX는 _wtoi로 읽습니다: 앞 공백과 부호 다음의 숫자까지, 숫자가 없으면 0.
  • REGEX가 0이면 리터럴, 0이 아니면 정규식입니다. Ehnd는 값이 정확히 1인 행만 읽을 때 정규식을 검사하고, 다른 값(열이 밀린 행)은 적용할 때마다 컴파일하다 실패하면 그 행을 건너뜁니다. hdTrans는 0이 아닌 행을 모두 읽을 때 검사해서, 안 되는 행을 버리고 기록합니다.
  • REGEX 필드가 없는 행은 Ehnd에서 초기화되지 않은 값을 읽습니다(대개 직전 행의 값). hdTrans는 0 (리터럴)으로 봅니다.
  • 다섯째 필드가 _AUX:로 시작하면 그 뒤가 AUX 텍스트입니다. 다른 필드는 무시합니다.
  • DEST가 정확히 _MATCH_CHECK이면 검사 전용 행입니다. 텍스트를 바꾸지 않고, 맞았는지만 토글과 가드에 씁니다.
  • hdTrans는 SRC가 빈 리터럴 행을 버리고 기록합니다. Ehnd의 replace_all은 빈 패턴에서 끝나지 않습니다.

행 접두 (3.20 꿀도르 수정판)

줄 첫머리의 정해진 위치에서 대소문자를 가려 비교합니다.

접두 뜻
_TGL⇥_DECR:이름⇥_ON / _OFF 토글 선언. 행이 아니며 차수와 위치는 상관없습니다. 상태는 _ON/_OFF로 시작하는지로 정합니다
_TGL⇥_ON:이름⇥ 행 행이 발동하면 토글을 켭니다
_TGL⇥_OFF:이름⇥ 행 행이 발동하면 토글을 끕니다
_TGL⇥_CONN:이름⇥ 행 토글이 켜져 있을 때만 행을 적용합니다. 뒤에 _ON:X⇥, _OFF:X⇥, _SkipLayer⇥를 더 붙일 수 있습니다
_SkipLayer⇥ 행 가드. 발동하면 뒤따르는 _skip_sub 묶음을 엽니다
_skip_sub⇥ 행 가드가 열려 있을 때만 적용합니다

(⇥는 탭.) 이름 뒤에 탭이 없는 _TGL 줄은 Ehnd에서 범위를 넘어 읽고, 상태가 _ON/_OFF가 아닌 선언은 SRC가 빈 행이 되므로 hdTrans는 둘 다 버리고 기록합니다. 그 밖의 _TGL⇥… 줄은 공개 소스와 같이 SRC가 _TGL인 리터럴 행이 됩니다.

정렬

종류마다 모든 파일의 행을 모아 (LAYER, g_line) 오름차순으로 정렬합니다. 파일 안의 위치는 같은 차수 안의 순서에만 쓰입니다. 꿀도르는 8자리 차수의 앞자리로 구역을 나눕니다(전처리 10 사용자, 11 기본, 문자 변환, 13~24 문법 구역, 29 마무리, 29999xxx 한글 운반; 후처리 10 한글, 어미, 19 조사, 20 사용자, 29 마무리).

적용

텍스트 하나(전처리는 입력, 후처리는 번역 결과)에 정렬된 행을 차례로 적용합니다. 각 행은 앞 행들이 바꾼 텍스트를 봅니다.

layer_prev = -1, guard = false
행 F마다:
  F.layer != layer_prev 이면:        # SkipLayer 검사 (아래)
      layer_prev = F.layer
      건너뜀 = 이 단계, 이 차수의 SkipLayer 조건 중 하나라도 현재 텍스트에 안 맞음
  건너뜀이면 다음 행
  F에 _CONN:T가 있고 T가 꺼져 있으면 다음 행
  F가 _skip_sub이면: guard가 꺼져 있으면 다음 행;
                     다음 행이 _skip_sub가 아니면 guard를 끔
  리터럴:  _skip_sub 행 → 바꾸기 (AUX 무시)
           검사 행     → SRC가 텍스트에 있으면 발동 (AUX 무시)
           그 밖       → AUX가 없거나 텍스트에 있으면 바꾸기
  정규식:  AUX가 있고 텍스트에 없으면 다음 행
           검사 행     → 텍스트에 맞으면 발동
           그 밖       → 맞는 곳을 모두 바꾸고, 텍스트가 바뀌면 발동
  발동했으면: _ON:X → X 켬, _OFF:X → X 끔, _SkipLayer → guard 켬
  • 리터럴 바꾸기는 왼쪽부터 모든 SRC를 DEST로 바꾸고, 넣은 DEST 뒤에서 다시 찾습니다(자기가 넣은 글자를 다시 보지 않음). 마지막으로 바꾼 자리의 끝이 0이 아니면 발동입니다. 그래서 텍스트 맨 앞을 지우기만 한 행(DEST가 비고 마지막 일치가 0번째)은 텍스트를 바꾸고도 발동하지 않습니다.
  • 정규식 바꾸기는 겹치지 않는 모든 일치를 바꿉니다. 빈 일치 바로 뒤에서는 빈 일치를 다시 찾지 않습니다(x*를 -로 바꾸면 axb는 -a--b-).
  • AUX는 정규식이 아닌 단순 찾기이며, 원래 입력이 아니라 앞 행들이 바꾼 현재 텍스트에서 찾습니다. 3.20 바이너리는 보통 리터럴 행에도 AUX를 검사합니다(소스 스냅숏은 무시). hdTrans는 바이너리를 따릅니다.
  • 처음 layer_prev가 -1이므로 차수 -1인 행이 맨 앞에 오면 그 차수의 SkipLayer 조건은 검사하지 않습니다.

토글

  • 선언은 이름과 처음 상태를 정합니다. 같은 이름의 첫 선언(읽은 순서: 전처리 파일들, 그다음 후처리 파일들)이 이깁니다. 선언되지 않은 이름은 꺼진 상태로 봅니다.
  • 꿀도르의 사용자 스위치는 PreFilter_@1Hdor_Set.txt의 선언입니다: _TGL⇥_DECR:DEL_COSNNT_⇥_OFF(촉음 삭제), DEL_YOMI_(《》()() 안 요미가나 삭제), ARAL_HANGUL_(아랄트랜스용 한글 처리). _OFF를 _ON으로 바꾸면 켜집니다.
  • 나머지 선언(I0_MASEN_ 등)은 대부분 꺼진 채 시작하고, _ON: 검사 행(ません ⇥ _MATCH_CHECK)이 맞을 때 켜져서 뒤의 비싼 _CONN: 행들을 그 텍스트에서만 돌게 합니다.
  • 토글 상태는 전처리와 후처리가 함께 씁니다. 전처리 행이 후처리 행을 켤 수 있습니다(꿀도르는 엔진을 통과하는 ASCII 표지 pC&Irn:와 함께 씀).
  • Ehnd는 후처리가 끝나면 상태를 선언대로 되돌립니다(후처리가 꺼져 있으면 되돌리지 않아 다음 요청으로 이어짐). hdTrans는 줄마다 선언 상태에서 시작합니다.

가드: _SkipLayer 행과 _skip_sub

  • _SkipLayer 행은 SkipLayer 파일과 상관없는 보통 행(바꾸기 또는 _MATCH_CHECK 검사)이며, 발동하면 단계 안의 가드 표시를 켭니다.
  • 정렬된 순서에서 이어진 _skip_sub 행들이 한 묶음입니다. 가드가 켜져 있으면 묶음이 돌고, 묶음의 마지막 행이 가드를 끕니다. 앞에 가드가 여럿이면 OR입니다. 가드는 묶음보다 앞 차수에 있어도 됩니다.
  • 묶음의 마지막 행이 토글이나 SkipLayer 때문에 건너뛰어지면 가드가 꺼지지 않고 다음 묶음으로 넘어갑니다.
  • 가드는 단계마다 꺼진 채 시작하며 전처리에서 후처리로 넘어가지 않습니다.
  • 꿀도르는 이것을 값싼 사전 검사로 씁니다: _SkipLayer ⇥ 戰戰 ⇥ _MATCH_CHECK 뒤의 정규식 30여 개는 텍스트에 戰戰이 있을 때만 돕니다.

SkipLayer*.txt

TYPE <TAB> LAYER <TAB> COND [<TAB> 무시…]
PRE	29999010	[가-깋]	1	//한글 치환
POST	10000010	[г-ё]	1	//한글치환1
  • 0열의 //는 주석입니다. 필드 분리는 필터 행과 같지만 //가 줄을 끝내지 않습니다(필드만 나눔).
  • TYPE이 PRE 또는 POST로 시작하지 않는 줄과 필드가 둘보다 적은 줄은 버립니다. COND가 없으면 빈 정규식이라 늘 맞습니다. COND는 넷째 필드와 상관없이 늘 정규식이며, 안 되는 정규식은 버립니다.
  • 단계가 차수 L의 첫 행에 이르면, 같은 TYPE에 LAYER가 정확히 L인 모든 COND를 그때의 텍스트에 regex_search합니다. 하나라도 안 맞으면 차수 L의 모든 행을 건너뜁니다(AND). 행이 없는 차수의 조건은 검사하지 않습니다.
  • 꿀도르의 용도는 속도입니다. 차수 29999010의 한글 운반 행 130여 개는 텍스트에 가~깋이 있을 때만 돕니다. 파일 첫 줄의 "SkipLayer는 홀수 차수로 할 것"은 데이터와 맞지 않으며 의미에는 정확히 같은 차수만 중요합니다.

UserDict*.txt

열

JP <TAB> KR [<TAB> TYPE [<TAB> ATTR]] [<TAB> 무시…] [//주석]
クロムウェル	크롬웰#N	1	VHn	//인간
一時なりとも	한 때일지라도	0
門前	문 앞#M
  • //로 시작하는 줄은 주석입니다. 줄 끝의 \n, 그다음 \r을 뗍니다.
  • 필드는 탭이나 // 앞에서 끝납니다. 넷째 필드까지 안에서 만난 //는 줄을 끝내고 그 필드는 세지 않습니다. 그래서 JP⇥KR//설명은 필드가 하나뿐이라 버려집니다(JP⇥KR⇥//설명은 됩니다). 마지막 필드는 줄 끝까지이며 끝의 탭도 품습니다.
  • JP와 KR이 있어야 합니다.
  • TYPE이 정확히 0 또는 2이면 상용어구, 그 밖(없음, 1, 1#M 등)은 명사입니다.
  • ATTR은 이지트랜스 의미 코드를 그대로 이어 쓴 문자열입니다: VHn(인간), VPl(장소), VBp, VFd, VTw(무기) 등. 3바이트 코드가 아닌 값도 그대로 넘어갑니다.
  • 길이: JP는 CP932로 30바이트, KR은 CP949로 30바이트, ATTR은 36바이트까지입니다. 넘는 줄을 만나면 Ehnd는 그 파일의 나머지 줄을 읽지 않습니다. hdTrans는 그 줄만 건너뛰고 기록하며, 코드 페이지 표 없이 ASCII(JP는 반각 가타카나도) 1바이트, 나머지 2바이트로 셉니다.
  • 키가 빈 줄은 버립니다.
  • Ehnd는 Dat\UserDict.jk(JKDIC_SWITCH)와, 아네모네 안에서는 실행 파일 옆 anedic.txt도 합칩니다. hdTrans는 Ehnd 폴더의 UserDict*.txt를 읽고, Windows의 J2KEngine.dll만 아네모네 안에서 AneDic.txt도 읽습니다(아네모네). Dat\UserDict.jk는 읽지 않습니다.

엔진에 넘기는 방식

Ehnd는 모든 항목을 CP932 바이트 내림차순, 상용어구 먼저, g_line 순으로 정렬해 임시 파일 %TEMP%\UserDict_<tick>.ehnd에 이지트랜스 UserDict.jk 레코드로 쓰고, 엔진이 UserDict.jk 대신 그 파일을 열게 합니다. 레코드의 품사는 명사 I110, 상용어구 A9D0이고 ATTR은 의미 코드 (규칙의 A 피연산자)입니다. 21바이트를 넘는 ATTR은 문법 코드 칸으로 넘칩니다. 분야는 GN입니다. 엔진은 사용자 사전 일치를 자기 사전보다 앞에 두되 태그 순(A9D0이 I110보다 먼저)과 긴 키 순으로 놓고, KR을 그대로 출력합니다. 해석하는 것은 (이)가, (은)는 같은 엔진 자체 조사 표기뿐입니다.

hdTrans는 같은 레코드를 사용자 계층(#! layer=user 열 파일과 같은 계층)에 넣습니다: 키 JP, 한국어 KR, 품사 I110 또는 A9D0, 의미 코드 ATTR, 문법 코드 없음, 사전 코드 GN. 사전 폴더의 사용자 레코드 다음에 맞춰 보므로, 같은 키이면 사전 폴더 쪽이 먼저입니다. ATTR의 넘침은 재현하지 않습니다 (꿀도르에는 21바이트를 넘는 ATTR이 없습니다).

KR 표기와 더미 명사

엔진은 KR 안의 표기를 해석하지 않으므로 꿀도르는 표기를 후처리 필터로 처리합니다.

  • #N(사람), #M(명사): 후처리가 앞 글자의 받침을 보고 조사를 고친 뒤 지웁니다.

  • @@(띄어쓰기 없는 분리), ##(띄어쓰기 있는 분리), ≪어간∬&≫(동사 활용), ∫(형용사), ∥r/∥t(부사 표지), 풀어 쓴 자모(ㄱㅣㄴ): 후처리가 활용하고 합칩니다.

  • 더미 명사: UserDict_@Hdor#0#req.txt 첫머리의 항목은 본문에 나오지 않는 글자로 만든 키입니다.

    키 KR 쓰임
    乘名 名: 명사 접두
    乘動 動: 동사 생성 접두
    乘形 形: 형용사 생성 접두
    乘受 受: 수동형 접두
    乘加 加: 더미 명사
    梦名 #M二 명사 접미
    梦彼 (VHn) #N二 호칭 명사, 이름 접미
    戰戰 #M二 ~する형 접미

    전처리 마무리 차수가 名:를 乘名の:로 바꾸어 넣으면, 엔진은 乘名을 사용자 사전 명사로 읽어 名:를 내고 の는 의로 옮깁니다. 후처리가 名:의:… 꼴을 찾아 처리합니다. XXする는 전처리가 XX戰戰으로 바꾸어 명사처럼 보이는 키로 만들고, 사용자 사전 KR ≪…∬&≫를 후처리가 활용합니다. 戰은 문자 변환 차수에서 戦으로 바꾼 뒤라 본문과 겹치지 않습니다. 한글은 CP932에 없으므로 전처리 29999xxx가 키릴 문자로 바꾸어 엔진을 통과시키고 후처리 10000010, 10999xxx가 되돌립니다.

정규식

Ehnd는 2015-03-23 커밋 24dac4e("regex 라이브러리를 boost로 변경")부터 Boost.Regex(boost::wregex, Perl 문법, 기본 플래그)를 썼고 꿀도르는 그 문법으로 쓰였습니다(그 전의 초기 3.0x만 MSVC std::wregex). hdTrans는 함께 넣은 PCRE2(third_party/pcre2, 16비트 라이브러리)로 같은 Perl 문법을 씁니다.

  • 텍스트와 패턴은 Windows의 wchar_t처럼 UTF-16 코드 단위입니다. .과 {n}은 코드 단위를 셉니다 (PCRE2 비UTF 모드).
  • 옵션: 대소문자 구분, PCRE2_UCP(\d, \w, \s, [[:alpha:]]가 유니코드 분류를 따르므로 \d는 전각 숫자에도 맞음), PCRE2_MULTILINE과 PCRE2_DOTALL(Boost 기본값처럼 ^ $는 줄 경계, .은 줄바꿈에도 맞음; hdTrans는 줄마다 적용하므로 차이가 없음).
  • lookahead와 lookbehind (?<=…), (?<!…)를 씁니다. lookbehind는 Boost처럼 고정 길이여야 하며, 가지마다 길이가 같아야 하는 Boost와 달리 PCRE2는 가지마다 길이가 달라도 받습니다. 꿀도르의 lookbehind는 모두 가지 길이가 같습니다. possessive 수량자 ?+, 원자 그룹, lookahead 안의 역참조, \_ 같은 기호 이스케이프도 됩니다.
  • PCRE2가 컴파일하지 못하는 행은 파일:줄: regex rejected (PCRE2 메시지 at 위치): 패턴으로 기록하고 건너뜁니다. 꿀도르는 모든 행이 컴파일됩니다. 일치 중 한도(PCRE2 기본 일치, 힙 한도)에 걸리면 그 행은 그 텍스트에서 아무것도 하지 않습니다(Ehnd는 예외를 잡아 그 행을 건너뜀).
  • 치환 문자열은 Boost의 기본(Perl) 형식입니다: $0$99…(그룹, 숫자를 이어 읽음), ${n}, $&, $`(이전 일치 끝부터 이 일치까지), $', $$, 그 밖의 $는 그대로. \1\9(그룹), \a \e \f \n \r \t \v, \xHH, \x{H…}, \cX, \0(8진수), 그 밖의 \는 뒤 글자 그대로. 없거나 맞지 않은 그룹은 빈 문자열입니다. Boost의 대소문자 바꾸기 \l \u \L \U \E는 지우기만 합니다(꿀도르는 $0~$9만 씁니다).
  • HDTRANS_PCRE2_JIT(기본 꺼짐)로 PCRE2 JIT를 넣을 수 있습니다. WebAssembly에서는 쓰지 않습니다.

Boost와 비교한 범위

2026-10-19에 꿀도르의 전처리 19,198행과 후처리 6,815행을 같은 순서와 토글 조건으로 실행해 비교했습니다. 참조는 macOS Boost.Regex 1.92, en_US.UTF-8 로캘입니다. 서로 다른 정규식 11,788개는 양쪽에서 모두 컴파일됐습니다.

비교 전처리 후처리
입력의 비어 있지 않은 줄 7,336 19,967
정규식 실행 횟수 7,721,573 5,943,667
실제 맞은 서로 다른 정규식 행 2,104 169
최종 결과 차이 0 0
발동한 규칙과 각 단계의 중간 결과 차이 0 0

이 결과는 모든 Boost 정규식과의 동등성을 보장하지 않습니다. 원본 Windows Boost 버전과 문자 분류는 검증하지 않았고, 후처리는 대응하는 일본어 전처리의 토글 상태를 이어받지 않고 초기 토글 상태에서 비교했습니다. 검사한 입력에 맞지 않은 규칙의 치환 결과도 보장하지 않습니다.

별도로 만든 예에서는 차이도 확인했습니다. 대소문자 치환은 위에 적은 미지원 동작이며, (?<=a|bc)x는 PCRE2만 받습니다. 유니코드 \\d, \\w, \\s는 Boost의 C 로캘과 달랐고, 시험한 경우에는 en_US.UTF-8 로캘과 같았습니다. 현재 꿀도르 비교에서는 이 차이로 인한 결과 차이를 발견하지 못해 PCRE2를 유지합니다.

재현 도구는 tools/filter_compat.cpp입니다. 일반 빌드에는 Boost가 필요하지 않으며, 이 도구를 별도로 빌드할 때만 필요합니다. clang/GCC에서 -std=c++20 -O2 -fno-access-control -DPCRE2_CODE_UNIT_WIDTH=16과 저장소의 include, src, PCRE2 헤더 경로, 빌드한 hdTransCore, pcre2-16, Boost.Regex 라이브러리를 사용합니다.

filter_compat --ehnd EHND_DIR --pre JP_DIR --post KO_DIR --report NEW_REPORT_DIR --max-lines 0 --locale en_US.UTF-8

기본값은 단계마다 500줄이며 --max-lines 0은 전체 입력입니다. 표준 출력은 집계 숫자만 담습니다. 보고서에는 외부 규칙과 원문이 들어갈 수 있으므로 비공개 경로를 쓰고 공개 저장소에 올리지 않습니다.

속도

꿀도르 전처리만 2만 행이 넘으므로 hdTrans는 행을 하나씩 찾지 않습니다.

  • 리터럴 SRC, AUX 텍스트, 그리고 PCRE2가 패턴에서 찾은 "모든 일치가 이 글자로 시작함"과 "모든 일치에 이 글자가 있음"을 한 Aho-Corasick 자동자에 넣고, 텍스트가 바뀔 때마다 한 번 훑어 어떤 것이 들어 있는지 표시합니다. 대소문자가 있을 수 있는 글자는 넣지 않습니다.
  • 리터럴 행은 SRC가 있을 때만 바꾸기를 하고, 정규식 행은 AUX와 그 글자들이 모두 있을 때만 PCRE2를 부릅니다. 결과는 행을 하나씩 찾을 때와 같습니다.

실행 로그

CLI는 --why, TypeScript는 translate(text, { json: true }).logs로 받습니다. 파일을 읽을 때의 FilterErrors()와는 별개입니다.

PreFilter : [PreFilter_test.txt:0] | 검색식 | 치환식 | 차수 | 정규식 여부
PostFIlter : [PostFilter_test.txt:0] | 검색식 | 치환식 | 차수 | 정규식 여부

행은 0부터 시작하고 주석과 빈 줄도 셉니다. PostFIlter는 원본 로그의 표기를 따릅니다. 실제 발동한 규칙과 _MATCH_CHECK, 토글 ON/OFF 변경, PreSkipLayer와 PostSkipLayer 집계를 기록합니다. 맞지 않은 규칙이나 꺼진 토글로 건너뛴 규칙마다 별도 로그를 만들지는 않습니다. 각 메시지는 끝에 줄바꿈을 포함합니다.

입력과 처리 결과는 [REQUEST], [PRE], [TRANS], [POST] 메시지로 따로 기록합니다. hdTrans는 줄마다 필터를 적용하므로 이 기록도 줄 단위이며, 규칙 메시지의 검색식과 치환식은 문장 전문이 아닙니다. JSON에서는 logs.join("")으로 로그 스트림을 만들 수 있습니다. 기본 번역에서는 로그를 수집하지 않습니다.

공개 Ehnd 3.20은 정규식 행에서 첫 구분자 하나를 생략하는 차이가 있고, hdTrans는 꿀도르 수정판의 통일된 형식을 사용합니다. 원본의 파일 저장 타임스탬프나 LOG_TIME 실행 시간 로그까지 재현하는 기능은 아닙니다.

다시 읽기

  • Ehnd: 시작할 때, /reload 명령, J2K_ReloadUserDict(사전만), 그리고 EHNDWATCH_SWITCH가 켜져 있으면 폴더 감시(watch.cpp: 500ms 타이머). 바뀐 파일 이름을 소문자로 보고 prefilter, postfilter, userdic, skiplayer가 들어 있으면 그 종류를, ehnd_conf.ini이면 설정을 다시 읽습니다(3.20 꿀도르 수정판은 전, 후처리 파일이 바뀌면 둘 다 다시 읽고 토글을 비웁니다).
  • hdTrans: Engine::Reload()가 사전 폴더와 Ehnd 폴더를 함께 다시 읽습니다. UserReload::Auto(C API와 J2KEngine.dll)는 번역을 시작할 때 Ehnd 폴더의 파일 목록과 크기, 시각을 1초에 한 번까지 확인해, 바뀌면 사전 폴더와 함께 백그라운드에서 다시 읽고 같은 순간에 바꿔 끼웁니다. 다시 읽기에 실패한 그 상태로는 다시 시도하지 않습니다.

hdTrans 필터 파일

사전 폴더의 .tsv 파일 첫 줄을 #! layer=prefilter 또는 #! layer=postfilter로 하면 Ehnd 파일 이름 없이 같은 행을 씁니다.

#! layer=prefilter
# 문장 부호 정리
‥|・{2,}	…	11110100	1
_TGL	_DECR:MY_SWITCH_	_OFF
_TGL	_CONN:MY_SWITCH_	ござる	です	20000000	0
  • 열, 접두, 정렬, 적용은 위의 필터 행과 같습니다. 다른 점은 #로 시작하는 줄도 주석이라는 것 하나입니다.
  • 일반 파일(머리글 key가 없는 파일)만 됩니다. SkipLayer 조건과 사용자 사전은 따로 없으니 _SkipLayer/_skip_sub 가드와 #! layer=user 열 파일을 씁니다.
  • 사전 폴더 순서, 폴더 안에서는 이름 순으로 읽고, Ehnd 폴더의 파일보다 먼저 읽습니다. 그래서 같은 차수이면 사전 폴더의 행이 먼저이고, 같은 토글을 선언하면 사전 폴더의 선언이 이깁니다.
  • 안 되는 줄은 불러오기를 실패시키지 않고 건너뛰며 FilterErrors()에 남습니다.

Ehnd와 다른 점

Ehnd hdTrans
요청 문자열 전체에 한 번 줄마다
Boost.Regex (Perl) PCRE2 (Perl), 컴파일되지 않는 패턴은 버리고 기록
CP932, CP949 변환 손실 없음
999자 넘는 줄은 둘로 읽음 나누지 않음
REGEX 필드가 없으면 초기화 안 된 값 0(리터럴)
빈 SRC 리터럴 행은 끝나지 않음 버리고 기록
너무 긴 UserDict 줄에서 파일 나머지를 버림 그 줄만 버리고 기록
UserDict.jk, 설정 스위치, 명령어 읽지 않음
아네모네 안에서 anedic.txt를 합침 Windows J2KEngine.dll만 합침, Ehnd 설정과 무관
후처리가 꺼지면 토글이 다음 요청으로 이어짐 줄마다 선언 상태
선언되지 않은 이름이 섞이면 토글 되돌리기가 어긋남 선언되지 않은 이름은 꺼짐으로 되돌림
ATTR 21바이트 넘침이 문법 코드로 감 의미 코드로 그대로