본문으로 건너뛰기

createAnalyzer

옵션 하나에 묶인, 재사용할 수 있는 분석기를 반환합니다. checkEntry는 키 하나를 모든 로케일에 걸쳐 약 2마이크로초에 비교합니다. 값의 사본을 따로 두지 않고 매 입력마다 검사해야 하는 편집기에 알맞습니다.

시그니처

typescript
function createAnalyzer(options?: Chki18nOptions): Chki18nAnalyzer;

interface Chki18nAnalyzer {
	readonly options: Chki18nResolvedOptions;
	readonly optionIssues: Chki18nIssue[];
	analyze: (input: Chki18nInput) => Chki18nResult;
	checkEntry: (entry: Chki18nEntry) => Chki18nIssue[];
}
dart
Chki18nAnalyzer createAnalyzer({Chki18nOptions? options});

class Chki18nAnalyzer {
  final Chki18nResolvedOptions options;
  final List<Chki18nIssue> optionIssues;

  Chki18nResult analyze(Chki18nInput input);
  List<Chki18nIssue> checkEntry(Chki18nEntry entry);
}
python
def create_analyzer(options: Options | None = None) -> Analyzer: ...


class Analyzer:
    options: ResolvedOptions
    option_issues: list[Issue]

    def analyze(self, data: Input) -> Result: ...
    def check_entry(self, entry: Entry) -> list[Issue]: ...

분석기를 재사용하는 이점

옵션과 활성화된 검사 집합, 보간 구분자는 분석기를 만들 때 한 번만 해석됩니다. analyzeTranslations를 반복 호출하면 매번 전부 다시 해석합니다. 한 번 검사할 때는 차이가 없지만, 매 입력마다 도는 검사에서는 이 해석 비용이 대부분을 차지합니다.

checkEntry

javascript
import { createAnalyzer } from 'chki18n';

const analyzer = createAnalyzer({ target: 'en' });

analyzer.checkEntry({
	key: 'desc.hello',
	values: { en: 'Hello {name}', ko: '안녕하세요' }
});
// [
//   {
//     code: 'NO_INTERPOLATION_KEY',
//     level: 'error',
//     locale: 'ko',
//     key: 'desc.hello',
//     interpolation: 'name',
//     message: 'The interpolation key `{name}` of the target language is missing from this value.'
//   }
// ]

analyzer.checkEntry({
	key: 'desc.hello',
	values: { en: 'Hello {name}', ko: '{name}님 안녕하세요' }
});
// []
dart
import 'package:chki18n/chki18n.dart';

final analyzer = createAnalyzer(options: const Chki18nOptions(target: 'en'));

analyzer.checkEntry(
  const Chki18nEntry(
    key: 'desc.hello',
    values: {'en': 'Hello {name}', 'ko': '안녕하세요'},
  ),
);
// [
//   Chki18nIssue(
//     code: Chki18nCheckCode.noInterpolationKey,
//     level: Chki18nLevel.error,
//     locale: 'ko',
//     key: 'desc.hello',
//     interpolation: 'name',
//     message: 'The interpolation key `{name}` of the target language is missing from this value.',
//   ),
// ]

analyzer.checkEntry(
  const Chki18nEntry(
    key: 'desc.hello',
    values: {'en': 'Hello {name}', 'ko': '{name}님 안녕하세요'},
  ),
);
// []
python
from chki18n import Entry, Options, create_analyzer

analyzer = create_analyzer(Options(target="en"))

analyzer.check_entry(Entry(key="desc.hello", values={"en": "Hello {name}", "ko": "안녕하세요"}))
# [
#     Issue(
#         code="NO_INTERPOLATION_KEY",
#         level="error",
#         locale="ko",
#         key="desc.hello",
#         interpolation="name",
#         message="The interpolation key `{name}` of the target language is missing from this value.",
#     )
# ]

analyzer.check_entry(
    Entry(key="desc.hello", values={"en": "Hello {name}", "ko": "{name}님 안녕하세요"})
)
# []

values로케일 -> 값 형태입니다. 기준 언어의 값도 같은 객체에서 읽으므로, 비교하려면 그 안에 함께 넣어야 합니다.

엔트리

typescript
interface Chki18nEntry {
	key: string;
	values: { [locale: string]: any };
	group?: string;
	locales?: string[];
}
dart
class Chki18nEntry {
  const Chki18nEntry({
    required String key,
    required Map<String, Object?> values,
    String? group,
    List<String>? locales,
  });
}
python
@dataclass(frozen=True, slots=True, kw_only=True)
class Entry:
    key: str
    values: dict[str, Any]
    group: str = ""
    locales: Sequence[str] | None = None

group은 모든 이슈에 그대로 실리므로, 번역 파일이 여러 개인 프로젝트에서 구분할 수 있습니다.

값이 아예 없는 로케일 보고하기

기본적으로 로케일 목록은 values의 키이므로, 아예 없는 언어는 비교되지 않습니다. 값이 없다는 사실 자체를 보고해야 한다면 locales를 전달하세요.

javascript
analyzer.checkEntry({ key: 'a', values: { en: 'Hello' }, locales: ['en', 'ko'] });
// [{ code: 'NO_KEY', locale: 'ko', … }]
dart
analyzer.checkEntry(
  const Chki18nEntry(key: 'a', values: {'en': 'Hello'}, locales: ['en', 'ko']),
);
// [Chki18nIssue(code: NO_KEY, locale: ko, …)]
python
analyzer.check_entry(Entry(key="a", values={"en": "Hello"}, locales=["en", "ko"]))
# [Issue(code='NO_KEY', locale='ko', …)]

편집기의 그리드에는 보통 빈 칸까지 포함해 모든 언어의 셀이 있으므로 기본값이 반대로 되어 있습니다. 빈 셀은 EMPTY_VALUE로 보고합니다.

보고하지 않는 것

키 하나로 판단할 수 있는 검사만 수행합니다. DUPLICATE_VALUE는 로케일 전체를 한 번에 봐야 하므로 여기서는 보고되지 않습니다. 그런 코드는 CROSS_KEY_CHECK_CODES에 정리되어 있습니다.

그 외에는 같은 데이터를 전체 분석한 결과와 코드도 순서도 똑같습니다.

편집기 그리드 검사하기

이 함수를 만든 이유가 되는 구조입니다. 값은 애플리케이션이 소유하고, chki18n은 판정만 합니다.

javascript
import { createAnalyzer } from 'chki18n/core';

// 프로젝트를 열 때 한 번.
const analyzer = createAnalyzer({
	target: project.primaryLocale,
	interpolationPrefix: project.interpolationPrefix,
	interpolationSuffix: project.interpolationSuffix
});

// 수정할 때마다, 바뀐 행 하나에 대해서만.
function lintRow(row) {
	return analyzer.checkEntry({
		key: row.key,
		values: Object.fromEntries(row.cells.map((cell) => [cell.locale, cell.value])),
		locales: project.locales,
		group: row.group
	});
}
dart
import 'package:chki18n/core.dart';

// 프로젝트를 열 때 한 번.
final analyzer = createAnalyzer(
  options: Chki18nOptions(
    target: project.primaryLocale,
    interpolationPrefix: project.interpolationPrefix,
    interpolationSuffix: project.interpolationSuffix,
  ),
);

// 수정할 때마다, 바뀐 행 하나에 대해서만.
List<Chki18nIssue> lintRow(Row row) => analyzer.checkEntry(
  Chki18nEntry(
    key: row.key,
    values: {for (final cell in row.cells) cell.locale: cell.value},
    locales: project.locales,
    group: row.group,
  ),
);
python
from chki18n.core import Entry, Options, create_analyzer

# 프로젝트를 열 때 한 번.
analyzer = create_analyzer(
    Options(
        target=project.primary_locale,
        interpolation_prefix=project.interpolation_prefix,
        interpolation_suffix=project.interpolation_suffix,
    )
)


# 수정할 때마다, 바뀐 행 하나에 대해서만.
def lint_row(row):
    return analyzer.check_entry(
        Entry(
            key=row.key,
            values={cell.locale: cell.value for cell in row.cells},
            locales=project.locales,
            group=row.group,
        )
    )

chki18n 안에 데이터 사본이 남지 않으므로 양쪽을 맞출 일도 없습니다. 애플리케이션이 이미 값을 소유하고 있다면 세션보다 이쪽이 낫습니다.

analyze

analyzeTranslations가 수행하는 것과 같은 비교를, 이 분석기의 옵션으로 수행합니다.

javascript
const analyzer = createAnalyzer({ target: 'en', flattened: true });

analyzer.analyze({ groups: everything }); // 전체가 바뀌었을 때 전체 검사
analyzer.checkEntry({ key, values }); // 매 입력마다 키 하나
dart
final analyzer = createAnalyzer(
  options: const Chki18nOptions(target: 'en', flattened: true),
);

analyzer.analyze(Chki18nInput(groups: everything)); // 전체가 바뀌었을 때 전체 검사
analyzer.checkEntry(Chki18nEntry(key: key, values: values)); // 매 입력마다 키 하나
python
analyzer = create_analyzer(Options(target="en", flattened=True))

analyzer.analyze(Input(groups=everything))  # 전체가 바뀌었을 때 전체 검사
analyzer.check_entry(Entry(key=key, values=values))  # 매 입력마다 키 하나

프로젝트를 열 때 전체를 분석하고, 수정할 때마다 checkEntry를 부르는 방식이 일반적입니다.

optionsoptionIssues

javascript
analyzer.options; // 해석된 옵션 — target, enabledChecks, 구분자 등
analyzer.optionIssues; // 전달한 값 중 사용할 수 없었던 것, INVALID_OPTIONS로
dart
analyzer.options; // 해석된 옵션 — target, enabledChecks, 구분자 등
analyzer.optionIssues; // 전달한 값 중 사용할 수 없었던 것, INVALID_OPTIONS로
python
analyzer.options  # 해석된 옵션 — target, enabled_checks, 구분자 등
analyzer.option_issues  # 전달한 값 중 사용할 수 없었던 것, INVALID_OPTIONS로
optionIssuesoptionIssuesoption_issuesanalyze가 만드는 모든 결과에 다시 실립니다. 옵션에 오타가 있으면 결과마다 한 번씩 보고되므로 놓치지 않습니다.

성능

로케일 5개에 걸친 키 5,000개 기준입니다.

호출비용
analyze (평탄화된 입력)약 17ms
checkEntry약 2.2µs
session.checkKey약 0.9µs

checkKey가 더 빠른 이유는 세션이 값 집합을 전달받지 않고 직접 만들기 때문입니다. 어느 쪽이든 화면 한 프레임을 그리는 시간보다 훨씬 짧습니다.

키 하나를 비교하는 데 마이크로초 단위가 듭니다. 매 입력마다 호출하라고 만든 함수입니다. 전체 analyze는 키 수와 로케일 수의 곱에 비례하며, 파일 감시자에서 실행해도 될 만큼 빠릅니다.

Released under the MIT License