번역 라이브러리 개선
아래 예시와 같이, 번역파일의 특수문자 이스케이프 처리를 누락한 경우에 원본과 번역본의 형식 차이로 인해 병합 과정에서 에러가 발생했습니다.
messages.xlf<!-- 원본 번역파일 (messages.xlf) -->
<source>Dog & Cat</source>
<!-- 번역 파일 (messages.fr.xlf) -->
<source>Dog & Cat</source>
<!-- 특수문자 이스케이프 처리 누락-->
<target>Chien & Chat</target>
구현
처음에는 문자열을 정규식으로 치환하여 누락된 이스케이프를 보정하는 방법을 고려했습니다.
하지만 XML은 태그와 속성, 텍스트가 하나의 문자열 안에 함께 존재하기 때문에 문자열 단위의 치환으로 처리할 경우 XML 구조를 훼손할 가능성이 있었습니다.
따라서 파일 전체를 문자열로 처리하는 대신, DOMParser를 통해 XML을 DOM 트리로 변환한 뒤 보정이 필요한 텍스트 및 속성 노드만 순회하는 방식으로 구현했습니다.
builder.ts// Before
const translationSourceFile = await fs.readFile(sourcePath, 'utf8');
const translationTargetFile = await fs.readFile(targetPath, 'utf8');
// After
const translationSourceXml = await fs.readFile(sourcePath, 'utf8');
const translationSourceDoc = new DOMParser().parseFromString(translationSourceXml, "text/html");
const translationSourceFile = escapeParser(translationSourceDoc);
const translationTargetXml = await fs.readFile(targetPath, 'utf8');
const translationTargetDoc = new DOMParser().parseFromString(translationTargetXml, "text/html");
const translationTargetFile = escapeParser(translationTargetDoc);
parser.ts// TEXT_NODE, ATTRIBUTE_NODE에서 xmlEncoder 함수 호출
case node.ATTRIBUTE_NODE:
const attrNode = node as Attr;
return buf.push(' ', attrNode.name, '="', attrNode.value.replace(/[<&"]/g, _xmlEncoder), '"');
case node.TEXT_NODE:
const textNode = node as Text;
if (!options.beautify || partOfMixedContent || !containsOnlyWhiteSpace(textNode.data)) {
return buf.push(textNode.data.replace(/[<&]/g, _xmlEncoder));
}
return;
// xmlEncoder
function _xmlEncoder(c: string): string {
return c === '<' && '<' ||
c === '>' && '>' ||
c === '&' && '&' ||
c === '"' && '"' ||
'&#' + c.charCodeAt(0) + ';';
}
검증
원본과 번역본 모두 이스케이프 처리가 정상적으로 적용되는지 테스트 코드를 통해 검증했습니다.
builder.spec.tstest('extract-and-merge xlf 2.0', async () => {
// 원본 번역 파일
await fs.writeFile(
'builder-test/messages.xlf',
`<xliff version="2.0">
<file original="ng.template" id="ngi18n">
<unit id="ID1">
<segment>
<source>source & val</source>
</segment>
</unit>
</file>
</xliff>`,
'utf8',
);
// 번역 파일
await fs.writeFile(
'builder-test/messages.fr.xlf',
`<xliff version="2.0">
<file original="ng.template" id="ngi18n">
<unit id="ID1">
<segment>
<source>source & val</source>
<target>target & val</target> // 의도적으로 이스케이프 처리 누락
</segment>
</unit>
</file>
</xliff>`,
'utf8',
);
const run = await architect.scheduleTarget(
{
project: 'builder-test',
target: 'extract-i18n-merge',
},
{
format: 'xlf2',
targetFiles: ['messages.fr.xlf'],
outputPath: 'builder-test',
},
);
const result = await run.result;
expect(result.success).toBeTruthy();
await run.stop();
// 이스케이프가 보정된 최종 결과 검증
const targetContent = await fs.readFile(
'builder-test/messages.fr.xlf',
'utf8',
);
expect(targetContent).toContain(
'<target>target & val</target>',
);
});
결과
번역 과정에서 발생할 수 있는 특수문자 이스케이프 누락을 빌드 단계에서 보정하도록 변경하여, 해당 문제로 발생하던 병합 오류를 방지했습니다.