diff --git a/README.md b/README.md index 8633187..f33a987 100644 --- a/README.md +++ b/README.md @@ -67,22 +67,22 @@ npx @jeremyfellaz/maximus fix ```text Maximus audit -Target: /workspace/my-app - -Status: attention needed -Findings: 1 error, 2 warnings, 1 info -Fixes available: 1 - -Findings -- [error] Path alias target does not exist - file: packages/web/tsconfig.json - detail: @ui/* points to src/missing/* - hint: Update or remove the stale alias target. - -- [warn] Missing .env.example contract - file: .env - detail: Runtime env files exist, but .env.example is missing. - hint: Run `maximus fix` to create a blank contract file. +대상: /workspace/my-app + +상태: 조치 필요 +발견 항목: 오류 1개, 경고 2개, 정보 1개 +적용 가능한 수정: 1개 + +발견 항목 +- [오류] 경로 alias 대상이 존재하지 않음 + 파일: packages/web/tsconfig.json + 상세: @ui/*는 src/missing/*를 가리키지만 해석된 경로를 찾을 수 없습니다. + 힌트: editor와 build 해석을 깨기 전에 오래된 alias를 수정하거나 제거하세요. + +- [경고] .env.example 계약 파일 누락 + 파일: .env + 상세: 실행 env 파일은 있지만 .env.example이 없습니다. + 힌트: `maximus fix`를 실행해 빈 계약 파일을 생성하세요. ``` ## GitHub Action diff --git a/bin/maximus.js b/bin/maximus.js index 3fb1bdd..5249b8a 100755 --- a/bin/maximus.js +++ b/bin/maximus.js @@ -13,7 +13,7 @@ const require = createRequire(import.meta.url); const cliArgs = process.argv.slice(2); const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const frozenJsReferenceNote = - "Rust is the canonical Maximus runtime. The bundled JS reference is frozen and only kept as a temporary compatibility bridge for legacy-compatible commands."; + "Rust가 Maximus의 canonical runtime입니다. 포함된 JS reference는 frozen 상태이며 legacy 호환 명령을 위한 임시 compatibility bridge로만 유지됩니다."; try { const runtime = await resolveRuntime(cliArgs); @@ -28,7 +28,7 @@ try { } } catch (error) { const message = error instanceof Error ? error.message : String(error); - console.error(`Maximus failed: ${message}`); + console.error(`Maximus 실패: ${message}`); process.exitCode = process.exitCode || 1; } @@ -52,7 +52,7 @@ async function resolveRuntime(args) { } if (fallback.reason) { throw new Error( - `${fallback.reason} Build the Rust CLI with \`cargo build -p maximus-cli\`, or install a supported native runtime package before using this command.`, + `${fallback.reason} 이 명령을 사용하기 전에 \`cargo build -p maximus-cli\`로 Rust CLI를 빌드하거나 지원되는 native runtime package를 설치하세요.`, ); } @@ -109,10 +109,10 @@ function hasGlibcRuntime() { function formatUnsupportedPlatformMessage() { if (process.platform === "linux" && !hasGlibcRuntime()) { - return `Linux musl is not supported yet. Maximus currently ships prebuilt Rust binaries only for Linux glibc and macOS. ${frozenJsReferenceNote}`; + return `Linux musl은 아직 지원하지 않습니다. Maximus는 현재 Linux glibc와 macOS용 prebuilt Rust binary만 제공합니다. ${frozenJsReferenceNote}`; } - return `Unsupported platform ${process.platform}-${process.arch}. Maximus currently ships prebuilt Rust binaries only for darwin-arm64, darwin-x64, linux-arm64-gnu, and linux-x64-gnu. ${frozenJsReferenceNote}`; + return `지원하지 않는 플랫폼입니다: ${process.platform}-${process.arch}. Maximus는 현재 darwin-arm64, darwin-x64, linux-arm64-gnu, linux-x64-gnu용 prebuilt Rust binary만 제공합니다. ${frozenJsReferenceNote}`; } async function resolveInstalledBinary(packageName) { @@ -168,7 +168,7 @@ async function evaluateFrozenJsFallback(args) { if (parsed.unsupportedFlags.length > 0) { return { allowed: false, - reason: `A Rust runtime is required for options not supported by the frozen JS compatibility path: ${parsed.unsupportedFlags.join(", ")}. ${frozenJsReferenceNote}`, + reason: `frozen JS compatibility path에서 지원하지 않는 옵션에는 Rust runtime이 필요합니다: ${parsed.unsupportedFlags.join(", ")}. ${frozenJsReferenceNote}`, }; } @@ -176,7 +176,7 @@ async function evaluateFrozenJsFallback(args) { if (configPath) { return { allowed: false, - reason: `A Rust runtime is required when a Maximus config file is present (${configPath}). ${frozenJsReferenceNote}`, + reason: `Maximus config file이 있을 때는 Rust runtime이 필요합니다 (${configPath}). ${frozenJsReferenceNote}`, }; } @@ -322,10 +322,10 @@ async function isPlaceholderRuntime(binaryPath) { function formatMissingRuntimeMessage(platformPackage) { return [ - `No Rust runtime is available for ${platformPackage.label}.`, - `Expected optional dependency "${platformPackage.packageName}" to be installed or a local Cargo build to exist in target/debug or target/release.`, + `${platformPackage.label}에서 사용할 수 있는 Rust runtime이 없습니다.`, + `optional dependency "${platformPackage.packageName}"가 설치되어 있거나 target/debug 또는 target/release에 local Cargo build가 있어야 합니다.`, frozenJsReferenceNote, - "If you are developing inside the repository, build the Rust CLI with `cargo build -p maximus-cli` first.", + "repository 안에서 개발 중이라면 먼저 `cargo build -p maximus-cli`로 Rust CLI를 빌드하세요.", ].join(" "); } @@ -336,7 +336,7 @@ async function runBinary(command, args) { }); child.on("error", (error) => { - reject(new Error(`Failed to launch Rust CLI at "${command}": ${error.message}`)); + reject(new Error(`"${command}"에서 Rust CLI를 실행하지 못했습니다: ${error.message}`)); }); child.on("exit", (code, signal) => { @@ -362,15 +362,15 @@ function formatCompatHelp() { return [ "Maximus", "", - "Bring order to chaotic configs.", + "혼란스러운 설정을 정리합니다.", "", - "Usage", + "사용법", " maximus audit [path] [--json]", " maximus doctor [path] [--json]", " maximus fix [path] --dry-run [--json]", " maximus help", "", - "Rust is the canonical Maximus runtime. When no Rust runtime is available, the bundled JS compatibility path stays as frozen reference-only fallback for legacy-compatible commands without Maximus config files or Rust-only flags. `--only`, `--skip`, `--fail-on`, `--diff`, `--fix-id`, `--fix-prefix`, `--format`, and `--output` require the Rust runtime, and `fix` is only available with `--dry-run`.", + "Rust가 Maximus의 canonical runtime입니다. Rust runtime이 없을 때 포함된 JS compatibility path는 Maximus config file이나 Rust 전용 flag가 없는 legacy 호환 명령을 위한 frozen reference fallback으로만 동작합니다. `--only`, `--skip`, `--fail-on`, `--diff`, `--fix-id`, `--fix-prefix`, `--format`, `--output`에는 Rust runtime이 필요하고, `fix`는 `--dry-run`과 함께 사용할 때만 가능합니다.", ].join("\n"); } diff --git a/crates/maximus-cli/src/args.rs b/crates/maximus-cli/src/args.rs index 8c00e88..ad2bdb4 100644 --- a/crates/maximus-cli/src/args.rs +++ b/crates/maximus-cli/src/args.rs @@ -48,16 +48,16 @@ pub enum ArgsError { impl Display for ArgsError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - Self::EmptyValue(flag) => write!(f, "Option \"{flag}\" requires a non-empty value."), + Self::EmptyValue(flag) => write!(f, "\"{flag}\" 옵션에는 비어 있지 않은 값이 필요합니다."), Self::ConflictingValue(left, right) => write!( f, - "Option \"{left}\" cannot be combined with option \"{right}\"." + "\"{left}\" 옵션은 \"{right}\" 옵션과 함께 사용할 수 없습니다." ), Self::InvalidValue(flag, value, expected) => write!( f, - "Option \"{flag}\" received unsupported value \"{value}\". Use one of: {expected}." + "\"{flag}\" 옵션에 지원하지 않는 값 \"{value}\"가 전달되었습니다. 사용 가능한 값: {expected}." ), - Self::MissingValue(flag) => write!(f, "Option \"{flag}\" requires a value."), + Self::MissingValue(flag) => write!(f, "\"{flag}\" 옵션에는 값이 필요합니다."), } } } diff --git a/crates/maximus-cli/src/main.rs b/crates/maximus-cli/src/main.rs index 8df1ca5..27a6b47 100644 --- a/crates/maximus-cli/src/main.rs +++ b/crates/maximus-cli/src/main.rs @@ -3,6 +3,7 @@ mod exit_codes; mod fail_policy; mod report_diff; mod report_json; +mod report_ko; mod report_markdown; mod report_sarif; mod report_text; @@ -38,7 +39,7 @@ enum CliError { Args(ArgsError), Config(LoadConfigError), Io(io::Error), - Json(serde_json::Error), + Json, InvalidArguments(String), UnknownCommand(String), } @@ -47,16 +48,14 @@ impl Display for CliError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { Self::Args(error) => write!(f, "{error}"), - Self::Config(error) => write!(f, "{error}"), - Self::Io(error) => write!(f, "{error}"), - Self::Json(error) => write!(f, "{error}"), + Self::Config(error) => write!(f, "{}", format_config_error(error)), + Self::Io(error) => write!(f, "{}", format_io_error(error)), + Self::Json => write!(f, "JSON 출력을 생성할 수 없습니다."), Self::InvalidArguments(message) => write!(f, "{message}"), - Self::UnknownCommand(command) => { - write!( - f, - "Unknown command \"{command}\". Run \"maximus help\" for usage." - ) - } + Self::UnknownCommand(command) => write!( + f, + "알 수 없는 명령 \"{command}\"입니다. 사용법은 \"maximus help\"를 실행하세요." + ), } } } @@ -82,8 +81,63 @@ impl From for CliError { } impl From for CliError { - fn from(value: serde_json::Error) -> Self { - Self::Json(value) + fn from(_: serde_json::Error) -> Self { + Self::Json + } +} + +fn format_config_error(error: &LoadConfigError) -> String { + match error { + LoadConfigError::Io(error) => { + format!("설정 파일을 읽을 수 없습니다: {}", format_io_error(error)) + } + LoadConfigError::Parse(error) => format!( + "설정 파일 {}을 파싱할 수 없습니다. JSONC 문법을 확인하세요.", + error.label() + ), + } +} + +fn format_io_error(error: &io::Error) -> String { + let message = error.to_string(); + if let Some(rest) = message.strip_prefix("IO error for operation on ") { + if let Some((path, reason)) = rest.split_once(": ") { + return format!( + "{path}에서 I/O 작업을 수행할 수 없습니다: {}", + korean_io_reason(reason) + ); + } + } + + korean_io_reason_for_kind(error.kind()).to_string() +} + +fn korean_io_reason(reason: &str) -> &'static str { + if reason.contains("No such file or directory") { + "파일이나 디렉터리가 없습니다." + } else if reason.contains("Permission denied") { + "권한이 거부되었습니다." + } else if reason.contains("not a directory") { + "디렉터리가 아닙니다." + } else if reason.contains("Is a directory") { + "디렉터리입니다." + } else { + "운영체제 I/O 오류가 발생했습니다." + } +} + +fn korean_io_reason_for_kind(kind: io::ErrorKind) -> &'static str { + match kind { + io::ErrorKind::NotFound => "파일이나 디렉터리가 없습니다.", + io::ErrorKind::PermissionDenied => "권한이 거부되었습니다.", + io::ErrorKind::AlreadyExists => "이미 존재합니다.", + io::ErrorKind::InvalidInput => "입력값이 올바르지 않습니다.", + io::ErrorKind::InvalidData => "데이터가 올바르지 않습니다.", + io::ErrorKind::Unsupported => "지원하지 않는 작업입니다.", + io::ErrorKind::UnexpectedEof => "예상보다 일찍 파일 끝에 도달했습니다.", + io::ErrorKind::WriteZero => "파일에 쓸 수 없습니다.", + io::ErrorKind::BrokenPipe => "출력 대상 연결이 끊어졌습니다.", + _ => "I/O 작업을 완료할 수 없습니다.", } } @@ -91,7 +145,7 @@ fn main() { let exit_code = match run_cli(env::args_os().skip(1)) { Ok(exit_code) => exit_code, Err(error) => { - eprintln!("Maximus failed: {error}"); + eprintln!("Maximus 실패: {error}"); exit_codes::FAILURE } }; @@ -176,7 +230,7 @@ fn run_fix_command( if initial.planned_fixes.len() != initial.result.fixes.len() { return Err(CliError::Io(io::Error::new( io::ErrorKind::Unsupported, - "one or more fixes are not executable from the Rust runtime yet", + "하나 이상의 수정 항목은 아직 Rust runtime에서 실행할 수 없습니다", ))); } @@ -187,7 +241,7 @@ fn run_fix_command( let planned = select_planned_fixes(&initial.planned_fixes, &selector); if !selector.is_empty() && planned.is_empty() { return Err(CliError::InvalidArguments( - "No matching fixes for the requested selector.".to_string(), + "요청한 selector와 일치하는 수정 항목이 없습니다.".to_string(), )); } let selected_fixes = select_fix_plans(&initial.result.fixes, &selector); @@ -462,7 +516,7 @@ fn parse_fail_on_level(value: &str) -> Result { "info" => Ok(FailOnLevel::Info), "none" => Ok(FailOnLevel::None), _ => Err(CliError::InvalidArguments(format!( - "Unknown fail-on level \"{value}\". Use one of: error, warn, info, none." + "알 수 없는 fail-on level \"{value}\"입니다. 사용 가능한 값: error, warn, info, none." ))), } } @@ -473,7 +527,7 @@ fn validate_check_ids(source: &str, ids: &[String]) -> Result<(), CliError> { for id in ids { if !known_ids.contains(&id.as_str()) { return Err(CliError::InvalidArguments(format!( - "Unknown check id \"{id}\" in {source}. Use one of: {}.", + "{source}에 알 수 없는 check id \"{id}\"가 있습니다. 사용 가능한 값: {}.", known_ids.join(", ") ))); } @@ -490,19 +544,20 @@ fn validate_command_flags(command: Option<&str>, flags: &Flags) -> Result<(), Cl if uses_fix_only_flags && (flags.help || command != Some("fix")) { return Err(CliError::InvalidArguments( - "Options \"--diff\", \"--env-source-comments\", \"--fix-id\", and \"--fix-prefix\" are only available for \"fix\".".to_string(), + "\"--diff\", \"--env-source-comments\", \"--fix-id\", \"--fix-prefix\" 옵션은 \"fix\"에서만 사용할 수 있습니다.".to_string(), )); } if flags.diff && !flags.dry_run { return Err(CliError::InvalidArguments( - "Option \"--diff\" requires \"fix --dry-run\".".to_string(), + "\"--diff\" 옵션은 \"fix --dry-run\"이 필요합니다.".to_string(), )); } if command == Some("fix") && matches!(flags.output_format, OutputFormat::Sarif) { return Err(CliError::InvalidArguments( - "Option \"--format sarif\" is only available for \"audit\" and \"doctor\".".to_string(), + "\"--format sarif\" 옵션은 \"audit\"과 \"doctor\"에서만 사용할 수 있습니다." + .to_string(), )); } diff --git a/crates/maximus-cli/src/report_diff.rs b/crates/maximus-cli/src/report_diff.rs index 4a02ebf..24b37c8 100644 --- a/crates/maximus-cli/src/report_diff.rs +++ b/crates/maximus-cli/src/report_diff.rs @@ -4,11 +4,13 @@ use std::path::Path; use maximus_core::{FixFilePreview, PreviewedFix}; +use crate::report_ko as ko; + pub fn render_fix_preview(target_dir: &Path, previews: &[PreviewedFix]) -> String { let mut blocks = Vec::new(); for preview in previews { - blocks.push(format!("- {}", preview.title)); + blocks.push(format!("- {}", ko::fix_title(&preview.title))); for file_preview in &preview.previews { blocks.push(render_file_diff(target_dir, file_preview)); @@ -97,7 +99,10 @@ fn diff_lines(text: &str) -> Vec { return Vec::new(); } - let mut lines = text.split('\n').map(ToString::to_string).collect::>(); + let mut lines = text + .split('\n') + .map(ToString::to_string) + .collect::>(); if text.ends_with('\n') { lines.pop(); } @@ -152,6 +157,8 @@ mod tests { ); assert!(preview.contains("--- /dev/null")); + assert!(preview.contains("- .env.example 생성")); + assert!(preview.contains("- .env.example에 누락된 키 추가")); assert!(preview.contains("+++ .env.example")); assert!(preview.contains("+API_URL=")); assert!(preview.contains("@@ -1,1 +1,2 @@")); @@ -179,6 +186,8 @@ mod tests { assert!(preview.contains("--- .env.example")); assert!(preview.contains("+++ .env.example")); assert!(preview.contains("@@ -0,0 +1,1 @@")); + assert!(preview.contains("- .env.example에 누락된 키 추가")); + assert!(!preview.contains("Append missing keys to .env.example")); assert!(!preview.contains("/dev/null")); } } diff --git a/crates/maximus-cli/src/report_ko.rs b/crates/maximus-cli/src/report_ko.rs new file mode 100644 index 0000000..dbc1cd6 --- /dev/null +++ b/crates/maximus-cli/src/report_ko.rs @@ -0,0 +1,698 @@ +use std::borrow::Cow; + +use maximus_core::{Severity, StructureReport}; + +pub fn status_label(status: &str) -> Cow<'_, str> { + match status { + "clean" => Cow::Borrowed("정상"), + "attention needed" => Cow::Borrowed("조치 필요"), + "blocking issues" => Cow::Borrowed("차단 이슈 있음"), + _ => Cow::Borrowed(status), + } +} + +pub fn severity_label(severity: &Severity) -> &'static str { + match severity { + Severity::Error => "오류", + Severity::Warn => "경고", + Severity::Info => "정보", + } +} + +pub fn describe_structure(structure: &StructureReport) -> String { + let repo_type = if structure.is_monorepo { + "모노레포" + } else { + "단일 패키지" + }; + + format!( + "{repo_type}, 패키지 {}개, 설정 파일 {}개, env 폴더 {}개", + structure.package_count, structure.config_files, structure.env_directories + ) +} + +pub fn message(value: &str) -> Cow<'_, str> { + if let Some(translated) = dynamic_message(value) { + return Cow::Owned(translated); + } + + match value { + "Config file could not be parsed" => Cow::Borrowed("설정 파일을 파싱할 수 없음"), + "compilerOptions.paths must be an object" => { + Cow::Borrowed("compilerOptions.paths는 object여야 함") + } + "Path alias target does not exist" => Cow::Borrowed("경로 alias 대상이 존재하지 않음"), + "Include pattern does not match any files" => { + Cow::Borrowed("include pattern이 어떤 파일과도 일치하지 않음") + } + "Exclude pattern does not filter any included files" => { + Cow::Borrowed("exclude pattern이 포함 파일을 제외하지 않음") + } + "Output directory overlaps the TypeScript source root" => { + Cow::Borrowed("출력 디렉터리가 TypeScript source root와 겹침") + } + "Output directory is nested inside the TypeScript source root" => { + Cow::Borrowed("출력 디렉터리가 TypeScript source root 안에 있음") + } + "Output directory contains TypeScript input files" => { + Cow::Borrowed("출력 디렉터리에 TypeScript 입력 파일이 포함됨") + } + "Output directory contains the TypeScript source root" => { + Cow::Borrowed("출력 디렉터리가 TypeScript source root를 포함함") + } + "Missing .env.example contract" => Cow::Borrowed(".env.example 계약 파일 누락"), + "Missing example env file" => Cow::Borrowed("예시 env 파일 누락"), + "Invalid env syntax" => Cow::Borrowed("env 문법이 올바르지 않음"), + "Local env overrides detected" => Cow::Borrowed("local env override 감지됨"), + "Declared env contract is not satisfied locally" => { + Cow::Borrowed("선언된 env 계약이 로컬에서 충족되지 않음") + } + "pnpm-workspace.yaml could not be parsed" => { + Cow::Borrowed("pnpm-workspace.yaml을 파싱할 수 없음") + } + "pnpm-workspace.yaml does not declare any package patterns" => { + Cow::Borrowed("pnpm-workspace.yaml이 package pattern을 선언하지 않음") + } + "turbo.json could not be parsed" => Cow::Borrowed("turbo.json을 파싱할 수 없음"), + "turbo.json does not declare any workspace tasks" => { + Cow::Borrowed("turbo.json이 workspace task를 선언하지 않음") + } + "ESLint formatting rules may conflict with Prettier" => { + Cow::Borrowed("ESLint 서식 규칙이 Prettier와 충돌할 수 있음") + } + "Formatting-oriented ESLint rules were found, but no explicit Prettier bridge was detected." => { + Cow::Borrowed("서식 중심 ESLint 규칙이 발견됐지만 명시적인 Prettier 연결은 감지되지 않았습니다.") + } + "Consider eslint-config-prettier or plugin:prettier/recommended to reduce formatter churn." => { + Cow::Borrowed("포매터 변경 소음을 줄이려면 eslint-config-prettier 또는 plugin:prettier/recommended를 검토하세요.") + } + "ESLint and Prettier are configured separately" => { + Cow::Borrowed("ESLint와 Prettier가 별도로 설정됨") + } + "That can be fine, but teams often prefer an explicit integration strategy." => { + Cow::Borrowed("문제 없을 수도 있지만, 팀에서는 명시적인 통합 전략을 선호하는 경우가 많습니다.") + } + "Document which tool owns formatting and which tool owns code-quality rules." => { + Cow::Borrowed("서식은 어느 도구가 맡고 코드 품질 규칙은 어느 도구가 맡는지 문서화하세요.") + } + "Legacy and flat ESLint configs coexist" => { + Cow::Borrowed("legacy ESLint 설정과 flat 설정이 함께 존재함") + } + "This directory contains both legacy .eslintrc.* files and flat eslint.config.* files, so ESLint can resolve different rule sets depending on the entry point." => { + Cow::Borrowed("이 디렉터리에는 legacy .eslintrc.* 파일과 flat eslint.config.* 파일이 함께 있어 진입점에 따라 ESLint가 서로 다른 규칙 집합을 해석할 수 있습니다.") + } + "Migrate to eslint.config.* as the single source of truth, then remove the legacy .eslintrc.* files after the new config fully replaces them." => { + Cow::Borrowed("eslint.config.*를 단일 기준으로 마이그레이션한 뒤, 새 config가 완전히 대체하면 legacy .eslintrc.* 파일을 제거하세요.") + } + "EditorConfig and Prettier disagree" => { + Cow::Borrowed("EditorConfig와 Prettier 설정이 일치하지 않음") + } + "Ignore files disagree on generated artifact coverage" => { + Cow::Borrowed("ignore 파일들의 generated artifact 범위가 일치하지 않음") + } + "Multiple lockfiles are present in one directory" => { + Cow::Borrowed("한 디렉터리에 여러 lockfile이 존재함") + } + "Package tsconfig drifts from the shared base" => { + Cow::Borrowed("package tsconfig가 shared base와 달라짐") + } + "Jest and Vitest configs coexist" => Cow::Borrowed("Jest와 Vitest config가 함께 존재함"), + "Node engine support and GitHub Actions matrix are out of sync" => { + Cow::Borrowed("Node engine 지원 범위와 GitHub Actions matrix가 동기화되지 않음") + } + "Package ESM type conflicts with tsconfig module output" => { + Cow::Borrowed("package ESM type이 tsconfig module output과 충돌함") + } + "Package CommonJS type conflicts with tsconfig module output" => { + Cow::Borrowed("package CommonJS type이 tsconfig module output과 충돌함") + } + "Package entrypoint target is invalid" => Cow::Borrowed("package entrypoint 대상이 올바르지 않음"), + "Package entrypoint target does not exist" => { + Cow::Borrowed("package entrypoint 대상이 존재하지 않음") + } + "Package entrypoint key is invalid" => Cow::Borrowed("package entrypoint key가 올바르지 않음"), + "Package exports object is invalid" => Cow::Borrowed("package exports object가 올바르지 않음"), + "Package entrypoint target uses an incompatible file type" => { + Cow::Borrowed("package entrypoint 대상이 호환되지 않는 파일 타입을 사용함") + } + "package.json files entry is excluded by nested .npmignore" => { + Cow::Borrowed("package.json files 항목이 nested .npmignore에 의해 제외됨") + } + "references must be an array" => Cow::Borrowed("references는 array여야 함"), + "Each project reference entry must be an object with a path" => { + Cow::Borrowed("각 project reference 항목은 path가 있는 object여야 함") + } + "Each project reference entry must declare a string path" => { + Cow::Borrowed("각 project reference 항목은 string path를 선언해야 함") + } + "Project reference target does not exist" => { + Cow::Borrowed("project reference 대상이 존재하지 않음") + } + "Project reference target could not be read" => { + Cow::Borrowed("project reference 대상을 읽을 수 없음") + } + "Project reference target could not be parsed" => { + Cow::Borrowed("project reference 대상을 파싱할 수 없음") + } + "Project reference target must point to a tsconfig file" => { + Cow::Borrowed("project reference 대상은 tsconfig 파일을 가리켜야 함") + } + "Referenced project must enable composite" => { + Cow::Borrowed("참조된 project는 composite을 활성화해야 함") + } + "Configured typeRoots entry does not exist" => { + Cow::Borrowed("설정된 typeRoots 항목이 존재하지 않음") + } + "compilerOptions.types and typeRoots both narrow ambient type resolution" => { + Cow::Borrowed("compilerOptions.types와 typeRoots가 모두 ambient type 해석 범위를 좁힘") + } + "compilerOptions.typeRoots disables default @types discovery" => { + Cow::Borrowed("compilerOptions.typeRoots가 기본 @types discovery를 비활성화함") + } + "\"compilerOptions.types\" must be an array of package names" => { + Cow::Borrowed("\"compilerOptions.types\"는 package name array여야 함") + } + "\"compilerOptions.types\" contains a non-string package name" => { + Cow::Borrowed("\"compilerOptions.types\"에 string이 아닌 package name이 포함됨") + } + "\"compilerOptions.typeRoots\" must be an array of directory paths" => { + Cow::Borrowed("\"compilerOptions.typeRoots\"는 directory path array여야 함") + } + "\"compilerOptions.typeRoots\" contains a non-string path" => { + Cow::Borrowed("\"compilerOptions.typeRoots\"에 string이 아닌 path가 포함됨") + } + "\"files\" entries must point to explicit files" => { + Cow::Borrowed("\"files\" 항목은 명시적인 파일을 가리켜야 함") + } + "\"files\" entries must point to readable files" => { + Cow::Borrowed("\"files\" 항목은 읽을 수 있는 파일을 가리켜야 함") + } + "\"files\" entries must point to files" => Cow::Borrowed("\"files\" 항목은 파일을 가리켜야 함"), + "\"files\" entries must point to supported TypeScript input files" => { + Cow::Borrowed("\"files\" 항목은 지원되는 TypeScript 입력 파일을 가리켜야 함") + } + "\"files\" entries must point to existing files" => { + Cow::Borrowed("\"files\" 항목은 존재하는 파일을 가리켜야 함") + } + "Inherited tsconfig could not be found" => Cow::Borrowed("상속된 tsconfig를 찾을 수 없음"), + "Inherited tsconfig extends cycle detected" => { + Cow::Borrowed("상속된 tsconfig extends cycle이 감지됨") + } + "Inherited tsconfig could not be read" => Cow::Borrowed("상속된 tsconfig를 읽을 수 없음"), + "Inherited tsconfig could not be parsed" => Cow::Borrowed("상속된 tsconfig를 파싱할 수 없음"), + "Inherited config must point to a tsconfig file" => { + Cow::Borrowed("상속된 config는 tsconfig 파일을 가리켜야 함") + } + "Referenced project must set compilerOptions.composite to a boolean" => { + Cow::Borrowed("참조된 project는 compilerOptions.composite을 boolean으로 설정해야 함") + } + "Inherited tsconfig must set compilerOptions.composite to a boolean" => { + Cow::Borrowed("상속된 tsconfig는 compilerOptions.composite을 boolean으로 설정해야 함") + } + "Runtime env files exist, but .env.example is missing." => { + Cow::Borrowed("실행 env 파일은 있지만 .env.example이 없습니다.") + } + "The file uses a pnpm-workspace.yaml shape that this check does not understand." => { + Cow::Borrowed("이 파일은 이 check가 이해하지 못하는 pnpm-workspace.yaml 형태를 사용합니다.") + } + "Use a simple packages: block list such as packages: [\"apps/*\", \"packages/*\"]." => { + Cow::Borrowed("packages: [\"apps/*\", \"packages/*\"]처럼 단순한 packages: block list를 사용하세요.") + } + "No package globs were found under packages:, and this repo has at most one package file, so the workspace file looks like a placeholder." => { + Cow::Borrowed("packages: 아래에 package glob이 없고 이 repo에는 package file이 최대 1개라 workspace 파일이 placeholder처럼 보입니다.") + } + "No package globs were found under packages:, so workspace packages are not declared yet." => { + Cow::Borrowed("packages: 아래에 package glob이 없어 workspace package가 아직 선언되지 않았습니다.") + } + "Add a packages: block with one or more workspace globs, or remove the file until the repo actually needs a workspace definition." => { + Cow::Borrowed("하나 이상의 workspace glob이 있는 packages: block을 추가하거나, 실제 workspace 정의가 필요할 때까지 파일을 제거하세요.") + } + "The turbo.json file is not valid JSONC." => { + Cow::Borrowed("turbo.json 파일이 올바른 JSONC가 아닙니다.") + } + "Fix the syntax error or replace the placeholder file with a real Turbo config." => { + Cow::Borrowed("문법 오류를 고치거나 placeholder 파일을 실제 Turbo config로 바꾸세요.") + } + "The file does not contain any task definitions that would make the workspace config meaningful." => { + Cow::Borrowed("이 파일에는 workspace config를 의미 있게 만드는 task 정의가 없습니다.") + } + "Add a non-empty tasks or pipeline map, or remove the placeholder turbo.json until the workspace needs it." => { + Cow::Borrowed("비어 있지 않은 tasks 또는 pipeline map을 추가하거나, workspace가 필요할 때까지 placeholder turbo.json을 제거하세요.") + } + "Contract files should describe the interface, not ship concrete secrets." => { + Cow::Borrowed("계약 파일은 interface를 설명해야 하며 실제 secret을 담으면 안 됩니다.") + } + "A committed .env.example file is missing." => { + Cow::Borrowed("커밋된 .env.example 파일이 없습니다.") + } + "TypeScript expects alias keys mapped to arrays of target strings." => { + Cow::Borrowed("TypeScript는 alias key가 대상 string array에 매핑되기를 기대합니다.") + } + "Each path alias should map to at least one target string." => { + Cow::Borrowed("각 path alias는 최소 하나의 대상 string에 매핑되어야 합니다.") + } + "TypeScript path targets must be strings." => { + Cow::Borrowed("TypeScript path target은 string이어야 합니다.") + } + "Current config surface looks healthy. Keep shared rules centralized as the repo grows." => { + Cow::Borrowed("현재 설정 표면은 정상입니다. repo가 커져도 shared rule을 중앙에 유지하세요.") + } + "Introduce a shared tsconfig.base.json so packages inherit one source of truth." => { + Cow::Borrowed("package들이 단일 기준을 상속하도록 shared tsconfig.base.json을 도입하세요.") + } + "Reduce repo-wide ESLint entry points unless packages genuinely need different rule sets." => { + Cow::Borrowed("package별로 다른 규칙 집합이 꼭 필요하지 않다면 repo 전체 ESLint 진입점을 줄이세요.") + } + "Use .env.example files consistently so onboarding does not depend on tribal knowledge." => { + Cow::Borrowed(".env.example 파일을 일관되게 사용해 온보딩이 구두 지식에 의존하지 않게 하세요.") + } + "Use shell-style env syntax or move comments to their own line." => { + Cow::Borrowed("shell-style env 문법을 사용하거나 주석을 별도 줄로 옮기세요.") + } + "Protect concrete env files with an exact .gitignore entry before committing secrets." => { + Cow::Borrowed("secret을 커밋하기 전에 구체 env 파일을 정확한 .gitignore 항목으로 보호하세요.") + } + "Run \"maximus fix\" to create a blank contract file." => { + Cow::Borrowed("\"maximus fix\"를 실행해 빈 계약 파일을 생성하세요.") + } + "Replace the value with a blank or placeholder string before sharing the repo." => { + Cow::Borrowed("repo를 공유하기 전에 값을 빈 문자열이나 placeholder로 바꾸세요.") + } + "Make sure local-only overrides are intentional and documented in .env.example." => { + Cow::Borrowed("local-only override가 의도된 것이며 .env.example에 문서화되어 있는지 확인하세요.") + } + "If these are injected by CI, keep the contract documented. Otherwise add them to your local env files." => { + Cow::Borrowed("CI에서 주입되는 값이면 계약을 문서화하세요. 아니면 로컬 env 파일에 추가하세요.") + } + "Fix invalid JSONC syntax before relying on this config." => { + Cow::Borrowed("이 config를 신뢰하기 전에 올바르지 않은 JSONC 문법을 고치세요.") + } + "Remove legacy flags before they become upgrade blockers." => { + Cow::Borrowed("upgrade blocker가 되기 전에 legacy flag를 제거하세요.") + } + "Rewrite paths to the standard { alias: [targets] } shape." => { + Cow::Borrowed("paths를 표준 { alias: [targets] } 형태로 다시 작성하세요.") + } + "Add a valid target or remove the alias entry." => { + Cow::Borrowed("유효한 대상을 추가하거나 alias 항목을 제거하세요.") + } + "Replace non-string entries with valid path strings." => { + Cow::Borrowed("string이 아닌 항목을 유효한 path string으로 교체하세요.") + } + "Keep wildcard placement aligned so imports resolve predictably." => { + Cow::Borrowed("import가 예측 가능하게 해석되도록 wildcard 위치를 맞추세요.") + } + "Update or remove stale aliases before they break editor and build resolution." => { + Cow::Borrowed("editor와 build 해석을 깨기 전에 오래된 alias를 수정하거나 제거하세요.") + } + "Align both alias surfaces so runtime and editor resolution stay consistent." => { + Cow::Borrowed("runtime과 editor 해석이 일치하도록 두 alias 표면을 맞추세요.") + } + "This directory declares both Jest and Vitest configuration, so tests can run under different environments depending on the command." => { + Cow::Borrowed("이 디렉터리는 Jest와 Vitest config를 모두 선언하므로 명령에 따라 서로 다른 환경에서 test가 실행될 수 있습니다.") + } + "Pick one runner for this package, or document the split with separate config ownership and scripts." => { + Cow::Borrowed("이 package의 runner 하나를 선택하거나, 별도 config ownership과 script로 분리를 문서화하세요.") + } + "Align EditorConfig and Prettier so editor saves do not fight formatter output." => { + Cow::Borrowed( + "편집기 저장과 포매터 출력이 충돌하지 않도록 EditorConfig와 Prettier를 맞추세요.", + ) + } + "Keep one lockfile per directory so dependency resolution stays predictable. Separate package directories can each have their own lockfile." => { + Cow::Borrowed("dependency 해석이 예측 가능하도록 디렉터리마다 lockfile을 하나만 유지하세요. 별도 package 디렉터리는 각자 lockfile을 가질 수 있습니다.") + } + "Fix or remove empty include patterns before TypeScript silently skips expected inputs." => { + Cow::Borrowed("TypeScript가 예상 입력을 조용히 건너뛰기 전에 빈 include pattern을 수정하거나 제거하세요.") + } + "Remove or tighten exclude entries that do not change the effective TypeScript input set." => { + Cow::Borrowed("실제 TypeScript 입력 집합을 바꾸지 않는 exclude 항목을 제거하거나 좁히세요.") + } + "Next.js generates .next/types during development or build, so this include can be empty before .next exists." => { + Cow::Borrowed("Next.js는 개발 또는 build 중 .next/types를 생성하므로 .next가 생기기 전에는 이 include가 비어 있을 수 있습니다.") + } + "Move emit output outside the source root so build artifacts do not overwrite source files." => { + Cow::Borrowed("build artifact가 source file을 덮어쓰지 않도록 emit output을 source root 밖으로 옮기세요.") + } + "Move emit output outside any directory that currently contains TypeScript input files." => { + Cow::Borrowed("현재 TypeScript 입력 파일이 들어 있는 디렉터리 밖으로 emit output을 옮기세요.") + } + "Prefer an output directory that is completely separate from the TypeScript source root." => { + Cow::Borrowed("TypeScript source root와 완전히 분리된 output directory를 사용하세요.") + } + "Create .env.example with safe defaults." => { + Cow::Borrowed("안전한 기본값으로 .env.example을 생성하세요.") + } + "Update the alias to an existing directory." => { + Cow::Borrowed("alias를 존재하는 디렉터리로 수정하세요.") + } + "No extra work is needed." => Cow::Borrowed("추가 작업은 필요하지 않습니다."), + "Package scripts are tidy" => Cow::Borrowed("package script가 정리되어 있음"), + _ => Cow::Borrowed(value), + } +} + +pub fn fix_title(value: &str) -> Cow<'_, str> { + if let Some(rest) = value.strip_prefix("Create ") { + return Cow::Owned(format!("{rest} 생성")); + } + if let Some(rest) = value.strip_prefix("Append missing keys to ") { + return Cow::Owned(format!("{rest}에 누락된 키 추가")); + } + + message(value) +} + +fn dynamic_message(value: &str) -> Option { + quoted_pattern( + value, + "Concrete env file \"", + "\" is not protected by .gitignore", + ) + .map(|name| format!("구체 env 파일 \"{name}\"이 .gitignore로 보호되지 않음")) + .or_else(|| { + quoted_pattern(value, "Duplicate env key \"", "\"") + .map(|key| format!("중복 env key \"{key}\"")) + }) + .or_else(|| { + value + .strip_prefix("No concrete value was found for: ") + .and_then(|keys| keys.strip_suffix('.')) + .map(|keys| format!("다음 env key에 대한 구체 값을 찾을 수 없습니다: {keys}.")) + }) + .or_else(|| { + value + .strip_prefix("Missing keys: ") + .and_then(|keys| keys.strip_suffix('.')) + .map(|keys| format!("누락된 key: {keys}.")) + }) + .or_else(|| { + value + .strip_prefix("Run \"maximus fix\" to append the missing keys to ") + .and_then(|file| file.strip_suffix('.')) + .map(|file| format!("\"maximus fix\"를 실행해 {file}에 누락된 key를 추가하세요.")) + }) + .or_else(|| { + value + .strip_prefix("EditorConfig sets ") + .and_then(|rest| rest.split_once(", but Prettier sets ")) + .and_then(|(editor, prettier)| { + prettier + .strip_suffix('.') + .map(|prettier| (editor, prettier)) + }) + .map(|(editor, prettier)| { + format!("EditorConfig는 {editor}를 설정하지만 Prettier는 {prettier}를 설정합니다.") + }) + }) + .or_else(|| { + quoted_pattern(value, "Deprecated compiler option \"", "\"") + .map(|option| format!("deprecated compiler option \"{option}\" 사용 중")) + }) + .or_else(|| { + quoted_pattern(value, "Alias \"", "\" does not declare any targets") + .map(|alias| format!("alias \"{alias}\"가 대상을 선언하지 않음")) + }) + .or_else(|| { + quoted_pattern(value, "Alias \"", "\" contains a non-string target") + .map(|alias| format!("alias \"{alias}\"에 string이 아닌 대상이 포함됨")) + }) + .or_else(|| { + quoted_pattern(value, "Wildcard shape does not match for alias \"", "\"") + .map(|alias| format!("alias \"{alias}\"의 wildcard 형태가 일치하지 않음")) + }) + .or_else(|| { + quoted_pattern(value, "Path alias \"", "\" shadows a package import") + .map(|alias| format!("path alias \"{alias}\"가 package import를 shadow함")) + }) + .or_else(|| { + quoted_pattern(value, "Vite alias \"", "\" differs from tsconfig paths") + .map(|alias| format!("Vite alias \"{alias}\"가 tsconfig paths와 다름")) + }) + .or_else(|| { + quoted_pattern(value, "Vite alias \"", "\" is missing from tsconfig paths") + .map(|alias| format!("Vite alias \"{alias}\"가 tsconfig paths에 없음")) + }) + .or_else(|| { + quoted_pattern(value, "\"", "\" must be an array of strings") + .map(|field| format!("\"{field}\"는 string array여야 함")) + }) + .or_else(|| { + quoted_pattern(value, "\"", "\" contains a non-string pattern") + .map(|field| format!("\"{field}\"에 string이 아닌 pattern이 포함됨")) + }) + .or_else(|| { + value + .strip_suffix(" config is declared in multiple places") + .map(|label| format!("{label} 설정이 여러 위치에 선언됨")) + }) + .or_else(|| translate_duplicate_config_detail(value)) + .or_else(|| translate_single_config_hint(value)) + .or_else(|| translate_lockfiles_detail(value)) + .or_else(|| { + value + .strip_suffix(" is missing keys") + .map(|file| format!("{file}에 누락된 key가 있음")) + }) + .or_else(|| { + if let Some((left, _)) = value + .strip_prefix("Alias \"") + .and_then(|tail| tail.split_once("\" differs between tsconfig and package imports")) + { + return Some(format!( + "alias \"{}\"가 tsconfig와 package imports 사이에서 다름", + left + )); + } + if let Some((left, right)) = value + .strip_prefix("Path alias \"") + .and_then(|tail| tail.split_once("\" shadows \"")) + { + let shadowed = right.strip_suffix('"').unwrap_or(right); + return Some(format!( + "path alias \"{}\"가 \"{}\"를 shadow함", + left, shadowed + )); + } + None + }) + .or_else(|| translate_add_to_gitignore_detail(value)) + .or_else(|| translate_points_to_missing_path(value)) + .or_else(|| translate_points_to_path(value)) + .or_else(|| translate_output_dir_detail(value)) + .or_else(|| translate_tsconfig_declaration_detail(value)) + .or_else(|| translate_include_detail(value)) + .or_else(|| translate_exclude_detail(value)) +} + +#[cfg(test)] +mod tests { + use super::message; + + #[test] + fn translates_dynamic_missing_concrete_env_detail() { + assert_eq!( + message("No concrete value was found for: CI_ONLY.").as_ref(), + "다음 env key에 대한 구체 값을 찾을 수 없습니다: CI_ONLY." + ); + assert_eq!( + message("Missing keys: OTHER.").as_ref(), + "누락된 key: OTHER." + ); + assert_eq!( + message("Run \"maximus fix\" to append the missing keys to .env.example.").as_ref(), + "\"maximus fix\"를 실행해 .env.example에 누락된 key를 추가하세요." + ); + } + + #[test] + fn translates_workspace_runner_and_editorconfig_messages() { + assert_eq!( + message("pnpm-workspace.yaml does not declare any package patterns").as_ref(), + "pnpm-workspace.yaml이 package pattern을 선언하지 않음" + ); + assert_eq!( + message("No package globs were found under packages:, so workspace packages are not declared yet.").as_ref(), + "packages: 아래에 package glob이 없어 workspace package가 아직 선언되지 않았습니다." + ); + assert_eq!( + message("This directory declares both Jest and Vitest configuration, so tests can run under different environments depending on the command.").as_ref(), + "이 디렉터리는 Jest와 Vitest config를 모두 선언하므로 명령에 따라 서로 다른 환경에서 test가 실행될 수 있습니다." + ); + assert_eq!( + message("EditorConfig sets indent_style=tab, indent_size=4, end_of_line=crlf, but Prettier sets useTabs=false, tabWidth=2, endOfLine=lf.").as_ref(), + "EditorConfig는 indent_style=tab, indent_size=4, end_of_line=crlf를 설정하지만 Prettier는 useTabs=false, tabWidth=2, endOfLine=lf를 설정합니다." + ); + } + + #[test] + fn translates_duplicate_config_and_structure_messages() { + assert_eq!( + message("Found 2 ESLint config sources in .").as_ref(), + ".에서 ESLint 설정 출처 2개를 찾았습니다." + ); + assert_eq!( + message("Keep a single ESLint entry point per directory to avoid drift.").as_ref(), + "차이를 피하려면 디렉터리마다 ESLint 진입점을 하나만 유지하세요." + ); + assert_eq!( + message("Migrate to eslint.config.* as the single source of truth, then remove the legacy .eslintrc.* files after the new config fully replaces them.").as_ref(), + "eslint.config.*를 단일 기준으로 마이그레이션한 뒤, 새 config가 완전히 대체하면 legacy .eslintrc.* 파일을 제거하세요." + ); + assert_eq!( + message("Reduce repo-wide ESLint entry points unless packages genuinely need different rule sets.").as_ref(), + "package별로 다른 규칙 집합이 꼭 필요하지 않다면 repo 전체 ESLint 진입점을 줄이세요." + ); + assert_eq!( + message("Found 2 known lockfiles in .: package-lock.json, yarn.lock.").as_ref(), + ".에서 알려진 lockfile 2개를 찾았습니다: package-lock.json, yarn.lock." + ); + } +} + +fn quoted_pattern<'a>(value: &'a str, prefix: &str, suffix: &str) -> Option<&'a str> { + value.strip_prefix(prefix)?.strip_suffix(suffix) +} + +fn translate_add_to_gitignore_detail(value: &str) -> Option { + let rest = value.strip_prefix("Add \"")?; + let (pattern, target) = rest.split_once("\" to ")?; + let target = target.strip_suffix('.')?; + Some(format!("{target}에 \"{pattern}\"를 추가하세요.")) +} + +fn translate_points_to_missing_path(value: &str) -> Option { + let (alias, rest) = value.split_once(" points to ")?; + let (target, _) = rest.split_once(", but the resolved path was not found.")?; + Some(format!( + "{alias}는 {target}를 가리키지만 해석된 경로를 찾을 수 없습니다." + )) +} + +fn translate_points_to_path(value: &str) -> Option { + let (alias, rest) = value.split_once(" points to ")?; + let target = rest.strip_suffix('.')?; + Some(format!("{alias}는 {target}를 가리킵니다.")) +} + +fn translate_duplicate_config_detail(value: &str) -> Option { + let rest = value.strip_prefix("Found ")?; + let (count, rest) = rest.split_once(' ')?; + let (label, rest) = rest.split_once(" config sources in ")?; + let directory = rest.strip_suffix('.')?; + let directory = if directory.is_empty() { "." } else { directory }; + Some(format!( + "{directory}에서 {label} 설정 출처 {count}개를 찾았습니다." + )) +} + +fn translate_single_config_hint(value: &str) -> Option { + let rest = value.strip_prefix("Keep a single ")?; + let label = rest.strip_suffix(" entry point per directory to avoid drift.")?; + Some(format!( + "차이를 피하려면 디렉터리마다 {label} 진입점을 하나만 유지하세요." + )) +} + +fn translate_lockfiles_detail(value: &str) -> Option { + let rest = value.strip_prefix("Found ")?; + let (count, rest) = rest.split_once(" known lockfiles in ")?; + let (directory, files) = rest.split_once(": ")?; + let directory = if directory.is_empty() { "." } else { directory }; + let files = files.strip_suffix('.')?; + Some(format!( + "{directory}에서 알려진 lockfile {count}개를 찾았습니다: {files}." + )) +} + +fn translate_output_dir_detail(value: &str) -> Option { + if let Some(rest) = value.strip_prefix("outDir \"") { + if let Some((out_dir, source_root)) = rest.split_once("\" overlaps source root \"") { + let source_root = source_root.strip_suffix("\".")?; + return Some(format!( + "outDir \"{out_dir}\"이 source root \"{source_root}\"와 겹칩니다." + )); + } + if let Some((out_dir, source_root)) = rest.split_once("\" is nested inside source root \"") + { + let source_root = source_root.strip_suffix("\".")?; + return Some(format!( + "outDir \"{out_dir}\"이 source root \"{source_root}\" 안에 있습니다." + )); + } + if let Some((out_dir, input)) = rest.split_once("\" contains TypeScript input \"") { + let input = input.strip_suffix("\".")?; + return Some(format!( + "outDir \"{out_dir}\"에 TypeScript 입력 \"{input}\"이 포함됩니다." + )); + } + if let Some((out_dir, source_root)) = rest.split_once("\" contains source root \"") { + let source_root = source_root.strip_suffix("\".")?; + return Some(format!( + "outDir \"{out_dir}\"이 source root \"{source_root}\"를 포함합니다." + )); + } + } + + None +} + +fn translate_tsconfig_declaration_detail(value: &str) -> Option { + if let Some((path, rest)) = value.split_once(" declares ") { + if let Some((field, _)) = rest.split_once(", but TypeScript expects string patterns.") { + return Some(format!( + "{path}는 {field}를 선언하지만 TypeScript는 string pattern을 기대합니다." + )); + } + if let Some((field, _)) = + rest.split_once(", but TypeScript expects an array of string patterns.") + { + return Some(format!( + "{path}는 {field}를 선언하지만 TypeScript는 string pattern array를 기대합니다." + )); + } + if let Some((field, _)) = + rest.split_once(", but TypeScript files entries cannot use glob wildcards.") + { + return Some(format!( + "{path}는 {field}를 선언하지만 TypeScript files 항목에는 glob wildcard를 사용할 수 없습니다." + )); + } + if let Some((field, _)) = rest.split_once(", but that path resolves to a directory.") { + return Some(format!( + "{path}는 {field}를 선언하지만 해당 path는 directory로 해석됩니다." + )); + } + if let Some((field, _)) = + rest.split_once(", but that path does not resolve to an existing file.") + { + return Some(format!( + "{path}는 {field}를 선언하지만 해당 path는 존재하는 파일로 해석되지 않습니다." + )); + } + } + + None +} + +fn translate_include_detail(value: &str) -> Option { + let rest = value.strip_prefix("include pattern \"")?; + let (pattern, rest) = rest.split_once("\" matched ")?; + let (count, base_dir) = rest.split_once(" files under base dir ")?; + let base_dir = base_dir.strip_suffix('.')?; + Some(format!( + "include pattern \"{pattern}\"은 base dir {base_dir} 아래에서 파일 {count}개와 일치했습니다." + )) +} + +fn translate_exclude_detail(value: &str) -> Option { + let rest = value.strip_prefix("exclude pattern \"")?; + let (pattern, rest) = rest.split_once("\" removed ")?; + let (count, rest) = rest.split_once(" files from ")?; + let (included, base_dir) = rest.split_once(" included file(s) under base dir ")?; + let base_dir = base_dir.strip_suffix('.')?; + Some(format!( + "exclude pattern \"{pattern}\"은 base dir {base_dir} 아래 포함 파일 {included}개 중 {count}개를 제외했습니다." + )) +} diff --git a/crates/maximus-cli/src/report_markdown.rs b/crates/maximus-cli/src/report_markdown.rs index c7d4f4d..10ad012 100644 --- a/crates/maximus-cli/src/report_markdown.rs +++ b/crates/maximus-cli/src/report_markdown.rs @@ -2,16 +2,18 @@ use std::path::Path; -use maximus_core::{AppliedFix, AuditResult, FixPlan, Severity, StructureReport}; +use maximus_core::{AppliedFix, AuditResult, FixPlan}; + +use crate::report_ko as ko; pub fn format_audit_report(result: &AuditResult) -> String { let mut lines = vec![ "# Maximus audit".to_string(), String::new(), - format!("- Target: `{}`", display_path(&result.root_dir)), - format!("- Status: `{}`", result.summary.status), + format!("- 대상: `{}`", display_path(&result.root_dir)), + format!("- 상태: `{}`", ko::status_label(&result.summary.status)), format!( - "- Findings: `{}` error, `{}` warnings, `{}` info", + "- 발견 항목: 오류 `{}`개, 경고 `{}`개, 정보 `{}`개", result.summary.blocking_findings, result.summary.warning_findings, result.summary.info_findings @@ -19,27 +21,27 @@ pub fn format_audit_report(result: &AuditResult) -> String { ]; push_suppression_summary(&mut lines, result.summary.suppressed_by_config); lines.extend([ - format!("- Fixes available: `{}`", result.summary.fixes_available), + format!("- 적용 가능한 수정: `{}`개", result.summary.fixes_available), String::new(), - "## Structure".to_string(), - format!("{}.", describe_structure(&result.structure)), + "## 구조".to_string(), + format!("{}.", ko::describe_structure(&result.structure)), ]); if result.findings.is_empty() { lines.push(String::new()); - lines.push("## Findings".to_string()); - lines.push("- No config drift detected.".to_string()); + lines.push("## 발견 항목".to_string()); + lines.push("- 설정 차이가 감지되지 않았습니다.".to_string()); } else { lines.push(String::new()); - lines.push("## Findings".to_string()); + lines.push("## 발견 항목".to_string()); lines.extend(format_findings(result)); } if !result.structure.recommendations.is_empty() { lines.push(String::new()); - lines.push("## Recommendations".to_string()); + lines.push("## 권장 사항".to_string()); for recommendation in &result.structure.recommendations { - lines.push(format!("- {recommendation}")); + lines.push(format!("- {}", ko::message(recommendation))); } } @@ -50,14 +52,17 @@ pub fn format_doctor_report(result: &AuditResult) -> String { let mut lines = vec![ "# Maximus doctor".to_string(), String::new(), - format!("- Target: `{}`", display_path(&result.root_dir)), - format!("- Diagnosis: `{}`", result.summary.status), + format!("- 대상: `{}`", display_path(&result.root_dir)), + format!("- 진단: `{}`", ko::status_label(&result.summary.status)), ]; push_suppression_summary(&mut lines, result.summary.suppressed_by_config); lines.extend([ - format!("- Project shape: {}", describe_structure(&result.structure)), + format!( + "- 프로젝트 구조: {}", + ko::describe_structure(&result.structure) + ), String::new(), - "## Prescription".to_string(), + "## 처방".to_string(), ]); let manual_findings = result @@ -73,39 +78,39 @@ pub fn format_doctor_report(result: &AuditResult) -> String { if fixable_findings > 0 { lines.push(format!( - "- Run `maximus fix` to apply {} safe fix(es).", + "- 안전한 수정 {}개를 적용하려면 `maximus fix`를 실행하세요.", result.summary.fixes_available )); } else { - lines.push("- No automatic fixes are currently available.".to_string()); + lines.push("- 현재 적용 가능한 자동 수정이 없습니다.".to_string()); } if manual_findings > 0 { lines.push(format!( - "- Review {manual_findings} manual issue(s) in priority order below." + "- 아래 우선순위에 따라 수동 확인 항목 {manual_findings}개를 검토하세요." )); } else { - lines.push("- No manual follow-up is required right now.".to_string()); + lines.push("- 지금은 수동 후속 조치가 필요하지 않습니다.".to_string()); } if result.findings.is_empty() { lines.push(String::new()); - lines.push("## Findings".to_string()); - lines.push("- No config drift detected.".to_string()); + lines.push("## 발견 항목".to_string()); + lines.push("- 설정 차이가 감지되지 않았습니다.".to_string()); } else { lines.push(String::new()); - lines.push("## Top Priorities".to_string()); + lines.push("## 상위 우선순위".to_string()); lines.extend(format_top_priorities(result)); lines.push(String::new()); - lines.push("## Findings".to_string()); + lines.push("## 발견 항목".to_string()); lines.extend(format_findings(result)); } if !result.structure.recommendations.is_empty() { lines.push(String::new()); - lines.push("## Recommended Structure".to_string()); + lines.push("## 권장 구조".to_string()); for recommendation in &result.structure.recommendations { - lines.push(format!("- {recommendation}")); + lines.push(format!("- {}", ko::message(recommendation))); } } @@ -124,32 +129,32 @@ pub fn format_fix_result( let mut lines = vec![ "# Maximus fix".to_string(), String::new(), - format!("- Target: `{}`", display_path(target_dir)), + format!("- 대상: `{}`", display_path(target_dir)), ]; if dry_run { if let Some(selected_fixes) = selected_fixes.filter(|fixes| !fixes.is_empty()) { lines.push(format!( - "- Dry run: `{}` safe fix(es) selected.", + "- Dry run: 안전한 수정 `{}`개가 선택되었습니다.", selected_fixes.len() )); } else { lines.push(format!( - "- Dry run: `{}` safe fix(es) available.", + "- Dry run: 적용 가능한 안전한 수정 `{}`개가 있습니다.", initial.summary.fixes_available )); } } else { - lines.push(format!("- Applied: `{}` fix(es).", applied.len())); + lines.push(format!("- 적용됨: 수정 `{}`개.", applied.len())); } if !applied.is_empty() { lines.push(String::new()); - lines.push("## Changes".to_string()); + lines.push("## 변경 사항".to_string()); for fix in applied { - lines.push(format!("- {}", fix.title)); + lines.push(format!("- {}", ko::fix_title(&fix.title))); for file in &fix.files { - lines.push(format!(" - file: `{}`", display_path(file))); + lines.push(format!(" - 파일: `{}`", display_path(file))); } } } @@ -157,11 +162,11 @@ pub fn format_fix_result( if dry_run { if let Some(selected_fixes) = selected_fixes.filter(|fixes| !fixes.is_empty()) { lines.push(String::new()); - lines.push("## Planned Changes".to_string()); + lines.push("## 계획된 변경 사항".to_string()); for fix in selected_fixes { - lines.push(format!("- {}", fix.title)); + lines.push(format!("- {}", ko::fix_title(&fix.title))); for file in &fix.files { - lines.push(format!(" - file: `{}`", display_path(file))); + lines.push(format!(" - 파일: `{}`", display_path(file))); } } } @@ -169,7 +174,7 @@ pub fn format_fix_result( if let Some(preview_report) = preview_report.filter(|report| !report.is_empty()) { lines.push(String::new()); - lines.push("## Preview Diffs".to_string()); + lines.push("## 미리보기 diff".to_string()); lines.push("```diff".to_string()); lines.extend(preview_report.lines().map(ToOwned::to_owned)); lines.push("```".to_string()); @@ -177,7 +182,7 @@ pub fn format_fix_result( lines.push(String::new()); lines.push(format!( - "- Post-check: `{}` error, `{}` warnings, `{}` info", + "- 사후 점검: 오류 `{}`개, 경고 `{}`개, 정보 `{}`개", final_result.summary.blocking_findings, final_result.summary.warning_findings, final_result.summary.info_findings @@ -186,11 +191,11 @@ pub fn format_fix_result( if final_result.findings.is_empty() { lines.push(String::new()); - lines.push("## Remaining Findings".to_string()); - lines.push("- Project is currently clean.".to_string()); + lines.push("## 남은 발견 항목".to_string()); + lines.push("- 현재 프로젝트는 정상입니다.".to_string()); } else { lines.push(String::new()); - lines.push("## Remaining Findings".to_string()); + lines.push("## 남은 발견 항목".to_string()); lines.extend(format_findings(final_result)); } @@ -207,21 +212,21 @@ fn format_top_priorities(result: &AuditResult) -> Vec { let mut lines = vec![format!( "{}. **[{}]** {}", index + 1, - severity_label(&finding.severity), - finding.title + ko::severity_label(&finding.severity), + ko::message(&finding.title) )]; if let Some(file) = &finding.file { lines.push(format!( - " - file: `{}`", + " - 파일: `{}`", format_relative_file(&result.root_dir, file) )); } if !finding.hint.is_empty() { - lines.push(format!(" - next: {}", finding.hint)); + lines.push(format!(" - 다음: {}", ko::message(&finding.hint))); } else if !finding.detail.is_empty() { - lines.push(format!(" - next: {}", finding.detail)); + lines.push(format!(" - 다음: {}", ko::message(&finding.detail))); } lines @@ -235,23 +240,23 @@ fn format_findings(result: &AuditResult) -> Vec { for finding in &result.findings { lines.push(format!( "- **[{}]** {}", - severity_label(&finding.severity), - finding.title + ko::severity_label(&finding.severity), + ko::message(&finding.title) )); if let Some(file) = &finding.file { lines.push(format!( - " - file: `{}`", + " - 파일: `{}`", format_relative_file(&result.root_dir, file) )); } if !finding.detail.is_empty() { - lines.push(format!(" - detail: {}", finding.detail)); + lines.push(format!(" - 상세: {}", ko::message(&finding.detail))); } if !finding.hint.is_empty() { - lines.push(format!(" - hint: {}", finding.hint)); + lines.push(format!(" - 힌트: {}", ko::message(&finding.hint))); } } @@ -260,23 +265,10 @@ fn format_findings(result: &AuditResult) -> Vec { fn push_suppression_summary(lines: &mut Vec, suppressed_by_config: usize) { if suppressed_by_config > 0 { - lines.push(format!("- Suppressed by config: `{suppressed_by_config}`")); + lines.push(format!("- 설정으로 숨김: `{suppressed_by_config}`개")); } } -fn describe_structure(structure: &StructureReport) -> String { - let repo_type = if structure.is_monorepo { - "monorepo" - } else { - "single package" - }; - - format!( - "{repo_type}, {} package(s), {} config file(s), {} env folder(s)", - structure.package_count, structure.config_files, structure.env_directories - ) -} - fn format_relative_file(root_dir: &Path, file_path: &Path) -> String { relative_path_like_js(root_dir, file_path) .map(|value| { @@ -328,14 +320,6 @@ fn relative_path_like_js(root_dir: &Path, file_path: &Path) -> Option { Some(path_string(&relative)) } -fn severity_label(severity: &Severity) -> &'static str { - match severity { - Severity::Error => "error", - Severity::Warn => "warn", - Severity::Info => "info", - } -} - fn display_path(path: &Path) -> String { path_string(path) } @@ -358,9 +342,9 @@ mod tests { let report = format_audit_report(&result); assert!(report.contains("# Maximus audit")); - assert!(report.contains("## Structure")); - assert!(report.contains("## Findings")); - assert!(report.contains("- Target: `/tmp/project`")); + assert!(report.contains("## 구조")); + assert!(report.contains("## 발견 항목")); + assert!(report.contains("- 대상: `/tmp/project`")); } #[test] @@ -428,9 +412,9 @@ mod tests { let report = format_doctor_report(&result); - assert!(report.contains("## Top Priorities")); - assert!(report.contains("1. **[error]** First error")); - assert!(report.contains(" - file: `tsconfig.json`")); + assert!(report.contains("## 상위 우선순위")); + assert!(report.contains("1. **[오류]** First error")); + assert!(report.contains(" - 파일: `tsconfig.json`")); } #[test] @@ -447,7 +431,7 @@ mod tests { Some("--- /dev/null\n+++ .env.example\n@@ -0,0 +1,1 @@\n+API_URL="), ); - assert!(report.contains("## Preview Diffs")); + assert!(report.contains("## 미리보기 diff")); assert!(report.contains("```diff")); assert!(report.contains("+API_URL=")); } @@ -462,9 +446,9 @@ mod tests { let doctor_report = format_doctor_report(&result); let fix_report = format_fix_result(false, &root_dir, &result, &[], &result, None, None); - assert!(audit_report.contains("- Suppressed by config: `2`")); - assert!(doctor_report.contains("- Suppressed by config: `2`")); - assert!(fix_report.contains("- Suppressed by config: `2`")); + assert!(audit_report.contains("- 설정으로 숨김: `2`개")); + assert!(doctor_report.contains("- 설정으로 숨김: `2`개")); + assert!(fix_report.contains("- 설정으로 숨김: `2`개")); } fn sample_result(root_dir: PathBuf) -> AuditResult { diff --git a/crates/maximus-cli/src/report_text.rs b/crates/maximus-cli/src/report_text.rs index b53a5e0..bb94c0c 100644 --- a/crates/maximus-cli/src/report_text.rs +++ b/crates/maximus-cli/src/report_text.rs @@ -2,15 +2,17 @@ use std::path::Path; -use maximus_core::{AppliedFix, AuditResult, FixPlan, StructureReport}; +use maximus_core::{AppliedFix, AuditResult, FixPlan}; + +use crate::report_ko as ko; pub fn format_help() -> String { [ "Maximus", "", - "Bring order to chaotic configs.", + "혼란스러운 설정을 정리합니다.", "", - "Usage", + "사용법", " maximus audit [path] [--only ] [--skip ] [--fail-on ] [--format ] [--json] [--output ]", " maximus doctor [path] [--only ] [--skip ] [--fail-on ] [--format ] [--json] [--output ]", " maximus fix [path] [--only ] [--skip ] [--fail-on ] [--dry-run] [--diff] [--env-source-comments] [--fix-id ] [--fix-prefix ] [--format ] [--json] [--output ]", @@ -23,40 +25,43 @@ pub fn format_audit_report(result: &AuditResult) -> String { let mut lines = Vec::new(); lines.push("Maximus audit".to_string()); - lines.push(format!("Target: {}", display_path(&result.root_dir))); + lines.push(format!("대상: {}", display_path(&result.root_dir))); lines.push(String::new()); - lines.push(format!("Status: {}", result.summary.status)); lines.push(format!( - "Findings: {} error, {} warnings, {} info", + "상태: {}", + ko::status_label(&result.summary.status) + )); + lines.push(format!( + "발견 항목: 오류 {}개, 경고 {}개, 정보 {}개", result.summary.blocking_findings, result.summary.warning_findings, result.summary.info_findings )); push_suppression_summary(&mut lines, result.summary.suppressed_by_config); lines.push(format!( - "Fixes available: {}", + "적용 가능한 수정: {}개", result.summary.fixes_available )); lines.push(String::new()); lines.push(format!( - "Structure: {}", - describe_structure(&result.structure) + "구조: {}", + ko::describe_structure(&result.structure) )); if result.findings.is_empty() { lines.push(String::new()); - lines.push("No config drift detected.".to_string()); + lines.push("설정 차이가 감지되지 않았습니다.".to_string()); } else { lines.push(String::new()); - lines.push("Findings".to_string()); + lines.push("발견 항목".to_string()); lines.extend(format_findings(result)); } if !result.structure.recommendations.is_empty() { lines.push(String::new()); - lines.push("Recommendations".to_string()); + lines.push("권장 사항".to_string()); for recommendation in &result.structure.recommendations { - lines.push(format!("- {recommendation}")); + lines.push(format!("- {}", ko::message(recommendation))); } } @@ -77,51 +82,54 @@ pub fn format_doctor_report(result: &AuditResult) -> String { .count(); lines.push("Maximus doctor".to_string()); - lines.push(format!("Target: {}", display_path(&result.root_dir))); + lines.push(format!("대상: {}", display_path(&result.root_dir))); lines.push(String::new()); - lines.push(format!("Diagnosis: {}", result.summary.status)); + lines.push(format!( + "진단: {}", + ko::status_label(&result.summary.status) + )); push_suppression_summary(&mut lines, result.summary.suppressed_by_config); lines.push(format!( - "Project shape: {}", - describe_structure(&result.structure) + "프로젝트 구조: {}", + ko::describe_structure(&result.structure) )); lines.push(String::new()); - lines.push("Prescription".to_string()); + lines.push("처방".to_string()); if fixable_findings > 0 { lines.push(format!( - "- Run \"maximus fix\" to apply {} safe fix(es).", + "- 안전한 수정 {}개를 적용하려면 \"maximus fix\"를 실행하세요.", result.summary.fixes_available )); } else { - lines.push("- No automatic fixes are currently available.".to_string()); + lines.push("- 현재 적용 가능한 자동 수정이 없습니다.".to_string()); } if manual_findings > 0 { lines.push(format!( - "- Review {manual_findings} manual issue(s) in priority order below." + "- 아래 우선순위에 따라 수동 확인 항목 {manual_findings}개를 검토하세요." )); } else { - lines.push("- No manual follow-up is required right now.".to_string()); + lines.push("- 지금은 수동 후속 조치가 필요하지 않습니다.".to_string()); } if result.findings.is_empty() { lines.push(String::new()); - lines.push("No config drift detected.".to_string()); + lines.push("설정 차이가 감지되지 않았습니다.".to_string()); } else { lines.push(String::new()); - lines.push("Top 3 priorities".to_string()); + lines.push("상위 3개 우선순위".to_string()); lines.extend(format_top_priorities(result)); lines.push(String::new()); - lines.push("Findings".to_string()); + lines.push("발견 항목".to_string()); lines.extend(format_findings(result)); } if !result.structure.recommendations.is_empty() { lines.push(String::new()); - lines.push("Recommended structure".to_string()); + lines.push("권장 구조".to_string()); for recommendation in &result.structure.recommendations { - lines.push(format!("- {recommendation}")); + lines.push(format!("- {}", ko::message(recommendation))); } } @@ -138,21 +146,21 @@ fn format_top_priorities(result: &AuditResult) -> Vec { let mut lines = vec![format!( "{}. [{}] {}", index + 1, - severity_label(&finding.severity), - finding.title + ko::severity_label(&finding.severity), + ko::message(&finding.title) )]; if let Some(file) = &finding.file { lines.push(format!( - " file: {}", + " 파일: {}", format_relative_file(&result.root_dir, file) )); } if !finding.hint.is_empty() { - lines.push(format!(" next: {}", finding.hint)); + lines.push(format!(" 다음: {}", ko::message(&finding.hint))); } else if !finding.detail.is_empty() { - lines.push(format!(" next: {}", finding.detail)); + lines.push(format!(" 다음: {}", ko::message(&finding.detail))); } lines @@ -175,56 +183,56 @@ pub fn format_fix_result( let selected_fixes = selected_fixes.unwrap_or(&[]); lines.push("Maximus fix".to_string()); - lines.push(format!("Target: {}", display_path(target_dir))); + lines.push(format!("대상: {}", display_path(target_dir))); lines.push(String::new()); if dry_run { if should_show_selected_fixes { lines.push(format!( - "Dry run: {} safe fix(es) selected.", + "Dry run: 안전한 수정 {}개가 선택되었습니다.", selected_fixes.len() )); } else { lines.push(format!( - "Dry run: {} safe fix(es) available.", + "Dry run: 적용 가능한 안전한 수정 {}개가 있습니다.", initial.summary.fixes_available )); } } else { - lines.push(format!("Applied: {} fix(es).", applied.len())); + lines.push(format!("적용됨: 수정 {}개.", applied.len())); } if !applied.is_empty() { lines.push(String::new()); - lines.push("Changes".to_string()); + lines.push("변경 사항".to_string()); for fix in applied { - lines.push(format!("- {}", fix.title)); + lines.push(format!("- {}", ko::fix_title(&fix.title))); for file in &fix.files { - lines.push(format!(" file: {}", display_path(file))); + lines.push(format!(" 파일: {}", display_path(file))); } } } if dry_run && should_show_selected_fixes { lines.push(String::new()); - lines.push("Planned changes".to_string()); + lines.push("계획된 변경 사항".to_string()); for fix in selected_fixes { - lines.push(format!("- {}", fix.title)); + lines.push(format!("- {}", ko::fix_title(&fix.title))); for file in &fix.files { - lines.push(format!(" file: {}", display_path(file))); + lines.push(format!(" 파일: {}", display_path(file))); } } } if let Some(preview_report) = preview_report.filter(|report| !report.is_empty()) { lines.push(String::new()); - lines.push("Preview diffs".to_string()); + lines.push("미리보기 diff".to_string()); lines.extend(preview_report.lines().map(ToString::to_string)); } lines.push(String::new()); lines.push(format!( - "Post-check: {} error, {} warnings, {} info", + "사후 점검: 오류 {}개, 경고 {}개, 정보 {}개", result.summary.blocking_findings, result.summary.warning_findings, result.summary.info_findings @@ -233,10 +241,10 @@ pub fn format_fix_result( if result.findings.is_empty() { lines.push(String::new()); - lines.push("Project is currently clean.".to_string()); + lines.push("현재 프로젝트는 정상입니다.".to_string()); } else { lines.push(String::new()); - lines.push("Remaining findings".to_string()); + lines.push("남은 발견 항목".to_string()); lines.extend(format_findings(result)); } @@ -249,23 +257,23 @@ fn format_findings(result: &AuditResult) -> Vec { for finding in &result.findings { lines.push(format!( "- [{}] {}", - severity_label(&finding.severity), - finding.title + ko::severity_label(&finding.severity), + ko::message(&finding.title) )); if let Some(file) = &finding.file { lines.push(format!( - " file: {}", + " 파일: {}", format_relative_file(&result.root_dir, file) )); } if !finding.detail.is_empty() { - lines.push(format!(" detail: {}", finding.detail)); + lines.push(format!(" 상세: {}", ko::message(&finding.detail))); } if !finding.hint.is_empty() { - lines.push(format!(" hint: {}", finding.hint)); + lines.push(format!(" 힌트: {}", ko::message(&finding.hint))); } } @@ -274,7 +282,7 @@ fn format_findings(result: &AuditResult) -> Vec { fn push_suppression_summary(lines: &mut Vec, suppressed_by_config: usize) { if suppressed_by_config > 0 { - lines.push(format!("Suppressed by config: {suppressed_by_config}")); + lines.push(format!("설정으로 숨김: {suppressed_by_config}개")); } } @@ -290,27 +298,6 @@ fn format_relative_file(root_dir: &Path, file_path: &Path) -> String { .unwrap_or_else(|| display_path(file_path)) } -fn describe_structure(structure: &StructureReport) -> String { - let repo_type = if structure.is_monorepo { - "monorepo" - } else { - "single package" - }; - - format!( - "{repo_type}, {} package(s), {} config file(s), {} env folder(s)", - structure.package_count, structure.config_files, structure.env_directories - ) -} - -fn severity_label(severity: &maximus_core::Severity) -> &'static str { - match severity { - maximus_core::Severity::Error => "error", - maximus_core::Severity::Warn => "warn", - maximus_core::Severity::Info => "info", - } -} - fn display_path(path: &Path) -> String { path_string(path) } @@ -376,9 +363,9 @@ mod tests { [ "Maximus", "", - "Bring order to chaotic configs.", + "혼란스러운 설정을 정리합니다.", "", - "Usage", + "사용법", " maximus audit [path] [--only ] [--skip ] [--fail-on ] [--format ] [--json] [--output ]", " maximus doctor [path] [--only ] [--skip ] [--fail-on ] [--format ] [--json] [--output ]", " maximus fix [path] [--only ] [--skip ] [--fail-on ] [--dry-run] [--diff] [--env-source-comments] [--fix-id ] [--fix-prefix ] [--format ] [--json] [--output ]", @@ -397,18 +384,18 @@ mod tests { format_audit_report(&result), [ "Maximus audit", - "Target: /tmp/project", + "대상: /tmp/project", "", - "Status: clean", - "Findings: 0 error, 0 warnings, 0 info", - "Fixes available: 0", + "상태: 정상", + "발견 항목: 오류 0개, 경고 0개, 정보 0개", + "적용 가능한 수정: 0개", "", - "Structure: single package, 1 package(s), 1 config file(s), 0 env folder(s)", + "구조: 단일 패키지, 패키지 1개, 설정 파일 1개, env 폴더 0개", "", - "No config drift detected.", + "설정 차이가 감지되지 않았습니다.", "", - "Recommendations", - "- Current config surface looks healthy. Keep shared rules centralized as the repo grows.", + "권장 사항", + "- 현재 설정 표면은 정상입니다. repo가 커져도 shared rule을 중앙에 유지하세요.", ] .join("\n") ); @@ -417,19 +404,19 @@ mod tests { format_doctor_report(&result), [ "Maximus doctor", - "Target: /tmp/project", + "대상: /tmp/project", "", - "Diagnosis: clean", - "Project shape: single package, 1 package(s), 1 config file(s), 0 env folder(s)", + "진단: 정상", + "프로젝트 구조: 단일 패키지, 패키지 1개, 설정 파일 1개, env 폴더 0개", "", - "Prescription", - "- No automatic fixes are currently available.", - "- No manual follow-up is required right now.", + "처방", + "- 현재 적용 가능한 자동 수정이 없습니다.", + "- 지금은 수동 후속 조치가 필요하지 않습니다.", "", - "No config drift detected.", + "설정 차이가 감지되지 않았습니다.", "", - "Recommended structure", - "- Current config surface looks healthy. Keep shared rules centralized as the repo grows.", + "권장 구조", + "- 현재 설정 표면은 정상입니다. repo가 커져도 shared rule을 중앙에 유지하세요.", ] .join("\n") ); @@ -446,13 +433,13 @@ mod tests { ), [ "Maximus fix", - "Target: /tmp/project", + "대상: /tmp/project", "", - "Dry run: 0 safe fix(es) available.", + "Dry run: 적용 가능한 안전한 수정 0개가 있습니다.", "", - "Post-check: 0 error, 0 warnings, 0 info", + "사후 점검: 오류 0개, 경고 0개, 정보 0개", "", - "Project is currently clean.", + "현재 프로젝트는 정상입니다.", ] .join("\n") ); @@ -551,22 +538,22 @@ mod tests { assert!(report.contains( [ - "Top 3 priorities", - "1. [error] First error", - " file: tsconfig.json", - " next: fix the first issue", - "2. [warn] Second warning", - " file: .env", - " next: run maximus fix", - "3. [info] Third note", - " file: packages/app/tsconfig.json", - " next: third detail", + "상위 3개 우선순위", + "1. [오류] First error", + " 파일: tsconfig.json", + " 다음: fix the first issue", + "2. [경고] Second warning", + " 파일: .env", + " 다음: run maximus fix", + "3. [정보] Third note", + " 파일: packages/app/tsconfig.json", + " 다음: third detail", ] .join("\n") .as_str() )); - assert!(report.contains("- [info] Fourth note")); - assert!(!report.contains("4. [info] Fourth note")); + assert!(report.contains("- [정보] Fourth note")); + assert!(!report.contains("4. [정보] Fourth note")); } #[cfg(windows)] diff --git a/crates/maximus-cli/tests/cli_help.rs b/crates/maximus-cli/tests/cli_help.rs index 3aace48..650ba72 100644 --- a/crates/maximus-cli/tests/cli_help.rs +++ b/crates/maximus-cli/tests/cli_help.rs @@ -19,9 +19,9 @@ fn no_args_prints_help() { [ "Maximus", "", - "Bring order to chaotic configs.", + "혼란스러운 설정을 정리합니다.", "", - "Usage", + "사용법", " maximus audit [path] [--only ] [--skip ] [--fail-on ] [--format ] [--json] [--output ]", " maximus doctor [path] [--only ] [--skip ] [--fail-on ] [--format ] [--json] [--output ]", " maximus fix [path] [--only ] [--skip ] [--fail-on ] [--dry-run] [--diff] [--env-source-comments] [--fix-id ] [--fix-prefix ] [--format ] [--json] [--output ]", @@ -83,7 +83,7 @@ fn audit_format_markdown_routes_to_markdown_report() { assert!(output.status.success()); let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8"); assert!(stdout.starts_with("# Maximus audit\n")); - assert!(stdout.contains("- Status: `clean`")); + assert!(stdout.contains("- 상태: `정상`")); } #[test] @@ -229,25 +229,87 @@ fn doctor_text_uses_expected_sections() { String::from_utf8(output.stdout).expect("stdout should be utf8"), [ "Maximus doctor", - &format!("Target: {}", fixture.to_string_lossy()), + &format!("대상: {}", fixture.to_string_lossy()), "", - "Diagnosis: clean", - "Project shape: single package, 1 package(s), 1 config file(s), 0 env folder(s)", + "진단: 정상", + "프로젝트 구조: 단일 패키지, 패키지 1개, 설정 파일 1개, env 폴더 0개", "", - "Prescription", - "- No automatic fixes are currently available.", - "- No manual follow-up is required right now.", + "처방", + "- 현재 적용 가능한 자동 수정이 없습니다.", + "- 지금은 수동 후속 조치가 필요하지 않습니다.", "", - "No config drift detected.", + "설정 차이가 감지되지 않았습니다.", "", - "Recommended structure", - "- Current config surface looks healthy. Keep shared rules centralized as the repo grows.", + "권장 구조", + "- 현재 설정 표면은 정상입니다. repo가 커져도 shared rule을 중앙에 유지하세요.", "", ] .join("\n") ); } +#[test] +fn audit_text_translates_workspace_runner_and_editorconfig_findings() { + let workspace = fixture_path_for("workspace-config/empty"); + let workspace_output = maximus_bin() + .args(["audit", workspace.to_string_lossy().as_ref()]) + .output() + .expect("workspace audit should run"); + let workspace_stdout = String::from_utf8(workspace_output.stdout).expect("stdout is utf8"); + assert!(workspace_output.stderr.is_empty()); + assert!(workspace_stdout.contains("pnpm-workspace.yaml이 package pattern을 선언하지 않음")); + assert!(workspace_stdout.contains("workspace 파일이 placeholder처럼 보입니다")); + assert!(!workspace_stdout.contains("pnpm-workspace.yaml does not declare any package patterns")); + assert!(!workspace_stdout.contains("No package globs were found")); + + let test_runner = fixture_path_for("test-runners/dual-config"); + let runner_output = maximus_bin() + .args(["audit", test_runner.to_string_lossy().as_ref()]) + .output() + .expect("test runner audit should run"); + let runner_stdout = String::from_utf8(runner_output.stdout).expect("stdout is utf8"); + assert!(runner_output.stderr.is_empty()); + assert!(runner_stdout.contains("Jest와 Vitest config가 함께 존재함")); + assert!(runner_stdout.contains("명령에 따라 서로 다른 환경에서 test가 실행될 수 있습니다")); + assert!(!runner_stdout.contains("This directory declares both Jest and Vitest")); + + let editorconfig = fixture_path_for("editorconfig-prettier/conflict"); + let editorconfig_output = maximus_bin() + .args(["audit", editorconfig.to_string_lossy().as_ref()]) + .output() + .expect("EditorConfig audit should run"); + let editorconfig_stdout = + String::from_utf8(editorconfig_output.stdout).expect("stdout is utf8"); + assert!(editorconfig_output.stderr.is_empty()); + assert!(editorconfig_stdout.contains("EditorConfig와 Prettier 설정이 일치하지 않음")); + assert!(editorconfig_stdout + .contains("EditorConfig는 indent_style=tab, indent_size=4, end_of_line=crlf를 설정하지만")); + assert!(editorconfig_stdout.contains("편집기 저장과 포매터 출력이 충돌하지 않도록")); + assert!(!editorconfig_stdout.contains("EditorConfig sets")); + assert!(!editorconfig_stdout.contains("formatter output")); +} + +#[test] +fn audit_text_translates_duplicate_config_and_structure_guidance() { + let fixture = fixture_path_for("eslint-migration-guidance"); + let output = maximus_bin() + .args(["audit", fixture.to_string_lossy().as_ref()]) + .output() + .expect("duplicate config audit should run"); + + let stdout = String::from_utf8(output.stdout).expect("stdout is utf8"); + assert!(output.stderr.is_empty()); + assert!(stdout.contains("ESLint 설정이 여러 위치에 선언됨")); + assert!(stdout.contains("ESLint 설정 출처 2개를 찾았습니다")); + assert!(stdout.contains("legacy ESLint 설정과 flat 설정이 함께 존재함")); + assert!(stdout.contains("eslint.config.*를 단일 기준으로 마이그레이션")); + assert!(stdout.contains("repo 전체 ESLint 진입점을 줄이세요")); + assert!(!stdout.contains("Found 2 ESLint config sources")); + assert!(!stdout.contains("Keep a single ESLint")); + assert!(!stdout.contains("Migrate to eslint.config")); + assert!(!stdout.contains("Reduce repo-wide ESLint")); +} + #[test] fn fix_dry_run_json_keeps_js_top_level_contract() { let fixture = fixture_path(); @@ -289,7 +351,7 @@ fn fix_format_sarif_fails_closed() { assert_eq!(output.status.code(), Some(2)); assert_eq!( String::from_utf8(output.stderr).expect("stderr should be utf8"), - "Maximus failed: Option \"--format sarif\" is only available for \"audit\" and \"doctor\".\n" + "Maximus 실패: \"--format sarif\" 옵션은 \"audit\"과 \"doctor\"에서만 사용할 수 있습니다.\n" ); } @@ -303,7 +365,7 @@ fn unknown_command_uses_prefixed_stderr_and_exit_code_two() { assert_eq!(output.status.code(), Some(2)); assert_eq!( String::from_utf8(output.stderr).expect("stderr should be utf8"), - "Maximus failed: Unknown command \"foobar\". Run \"maximus help\" for usage.\n" + "Maximus 실패: 알 수 없는 명령 \"foobar\"입니다. 사용법은 \"maximus help\"를 실행하세요.\n" ); } @@ -322,7 +384,7 @@ fn unknown_command_does_not_load_broken_config() { assert_eq!(output.status.code(), Some(2)); assert_eq!( String::from_utf8(output.stderr).expect("stderr should be utf8"), - "Maximus failed: Unknown command \"foobar\". Run \"maximus help\" for usage.\n" + "Maximus 실패: 알 수 없는 명령 \"foobar\"입니다. 사용법은 \"maximus help\"를 실행하세요.\n" ); } @@ -336,7 +398,10 @@ fn missing_directory_uses_prefixed_stderr_and_exit_code_two() { assert_eq!(output.status.code(), Some(2)); let stderr = String::from_utf8(output.stderr).expect("stderr should be utf8"); - assert!(stderr.starts_with("Maximus failed: ")); + assert!(stderr.starts_with("Maximus 실패: ")); + assert!(stderr.contains("파일이나 디렉터리가 없습니다")); + assert!(!stderr.contains("IO error for operation")); + assert!(!stderr.contains("No such file or directory")); } #[cfg(unix)] diff --git a/crates/maximus-cli/tests/config_and_filtering.rs b/crates/maximus-cli/tests/config_and_filtering.rs index 0bc5800..52529b0 100644 --- a/crates/maximus-cli/tests/config_and_filtering.rs +++ b/crates/maximus-cli/tests/config_and_filtering.rs @@ -718,7 +718,7 @@ fn config_suppression_text_report_shows_nonzero_suppressed_count() { assert_eq!(output.status.code(), Some(0), "{output:?}"); let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8"); assert!( - stdout.contains("Suppressed by config: 1"), + stdout.contains("설정으로 숨김: 1개"), "text report should show nonzero suppressed count: {stdout}" ); } @@ -1592,7 +1592,7 @@ fn broken_config_is_reported_as_cli_error() { assert_eq!(output.status.code(), Some(2)); let stderr = String::from_utf8(output.stderr.clone()).expect("stderr should be utf8"); - assert!(stderr.contains("Maximus failed:")); + assert!(stderr.contains("Maximus 실패:")); assert!(stderr.contains(&config_path.to_string_lossy().to_string())); } @@ -1741,7 +1741,9 @@ fn empty_cli_check_filters_are_reported_as_cli_errors() { assert_eq!(output.status.code(), Some(2), "{output:?}"); let stderr = String::from_utf8(output.stderr).expect("stderr should be utf8"); - assert!(stderr.contains(&format!("Option \"{flag}\" requires a non-empty value."))); + assert!(stderr.contains(&format!( + "\"{flag}\" 옵션에는 비어 있지 않은 값이 필요합니다." + ))); } } @@ -1751,16 +1753,16 @@ fn cli_filters_do_not_hide_invalid_config_check_ids() { write_mixed_fixture(fixture.path()); for (config_body, cli_args, expected_fragment) in [ - ( - r#"{ "checks": { "only": ["not-a-real-check"] } }"#, - vec!["--skip", "env"], - "Unknown check id \"not-a-real-check\" in only.", - ), - ( - r#"{ "checks": { "skip": ["not-a-real-check"] } }"#, - vec!["--only", "env"], - "Unknown check id \"not-a-real-check\" in skip.", - ), + ( + r#"{ "checks": { "only": ["not-a-real-check"] } }"#, + vec!["--skip", "env"], + "only에 알 수 없는 check id \"not-a-real-check\"가 있습니다.", + ), + ( + r#"{ "checks": { "skip": ["not-a-real-check"] } }"#, + vec!["--only", "env"], + "skip에 알 수 없는 check id \"not-a-real-check\"가 있습니다.", + ), ] { write(fixture.path().join("maximus.config.json"), config_body); diff --git a/crates/maximus-cli/tests/fix_selection_and_diff.rs b/crates/maximus-cli/tests/fix_selection_and_diff.rs index f9885d8..fc53c77 100644 --- a/crates/maximus-cli/tests/fix_selection_and_diff.rs +++ b/crates/maximus-cli/tests/fix_selection_and_diff.rs @@ -35,8 +35,8 @@ fn fix_id_applies_only_the_selected_fix() { assert!(!root.join("packages/app/.env.example").exists()); let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8"); - assert!(stdout.contains("Applied: 1 fix(es).")); - assert!(stdout.contains("Create .env.example")); + assert!(stdout.contains("적용됨: 수정 1개.")); + assert!(stdout.contains(".env.example 생성")); } #[test] @@ -98,7 +98,7 @@ fn fix_command_errors_when_selector_matches_nothing() { assert_eq!(output.status.code(), Some(2)); assert_eq!( String::from_utf8(output.stderr).expect("stderr should be utf8"), - "Maximus failed: No matching fixes for the requested selector.\n" + "Maximus 실패: 요청한 selector와 일치하는 수정 항목이 없습니다.\n" ); } @@ -122,7 +122,7 @@ fn fix_id_requires_a_real_value_instead_of_another_flag() { assert_eq!(output.status.code(), Some(2)); assert_eq!( String::from_utf8(output.stderr).expect("stderr should be utf8"), - "Maximus failed: Option \"--fix-id\" requires a value.\n" + "Maximus 실패: \"--fix-id\" 옵션에는 값이 필요합니다.\n" ); } @@ -146,7 +146,7 @@ fn fix_prefix_requires_a_real_value_instead_of_another_flag() { assert_eq!(output.status.code(), Some(2)); assert_eq!( String::from_utf8(output.stderr).expect("stderr should be utf8"), - "Maximus failed: Option \"--fix-prefix\" requires a value.\n" + "Maximus 실패: \"--fix-prefix\" 옵션에는 값이 필요합니다.\n" ); } @@ -165,7 +165,7 @@ fn diff_requires_dry_run() { assert_eq!(output.status.code(), Some(2)); assert_eq!( String::from_utf8(output.stderr).expect("stderr should be utf8"), - "Maximus failed: Option \"--diff\" requires \"fix --dry-run\".\n" + "Maximus 실패: \"--diff\" 옵션은 \"fix --dry-run\"이 필요합니다.\n" ); } @@ -179,7 +179,7 @@ fn fix_only_flags_are_rejected_without_fix_command() { assert_eq!(output.status.code(), Some(2)); assert_eq!( String::from_utf8(output.stderr).expect("stderr should be utf8"), - "Maximus failed: Options \"--diff\", \"--env-source-comments\", \"--fix-id\", and \"--fix-prefix\" are only available for \"fix\".\n" + "Maximus 실패: \"--diff\", \"--env-source-comments\", \"--fix-id\", \"--fix-prefix\" 옵션은 \"fix\"에서만 사용할 수 있습니다.\n" ); } @@ -194,7 +194,7 @@ fn fix_only_flags_are_rejected_for_help_command() { assert!(output.stderr.is_empty()); assert!(String::from_utf8(output.stdout) .expect("stdout should be utf8") - .contains("Usage\n maximus audit [path]")); + .contains("사용법\n maximus audit [path]")); } #[test] @@ -208,7 +208,7 @@ fn fix_only_flags_are_rejected_for_fix_help_command() { assert!(output.stderr.is_empty()); assert!(String::from_utf8(output.stdout) .expect("stdout should be utf8") - .contains("Usage\n maximus audit [path]")); + .contains("사용법\n maximus audit [path]")); } #[test] @@ -222,7 +222,7 @@ fn fix_only_flags_are_rejected_when_help_flag_precedes_fix_command() { assert!(output.stderr.is_empty()); assert!(String::from_utf8(output.stdout) .expect("stdout should be utf8") - .contains("Usage\n maximus audit [path]")); + .contains("사용법\n maximus audit [path]")); } #[test] @@ -245,7 +245,7 @@ fn audit_rejects_fix_only_flags() { assert_eq!(output.status.code(), Some(2)); assert_eq!( String::from_utf8(output.stderr).expect("stderr should be utf8"), - "Maximus failed: Options \"--diff\", \"--env-source-comments\", \"--fix-id\", and \"--fix-prefix\" are only available for \"fix\".\n" + "Maximus 실패: \"--diff\", \"--env-source-comments\", \"--fix-id\", \"--fix-prefix\" 옵션은 \"fix\"에서만 사용할 수 있습니다.\n" ); } @@ -273,7 +273,7 @@ fn diff_preview_shows_create_diff_without_writing_files() { assert!(!root.join(".env.example").exists()); let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8"); - assert!(stdout.contains("Preview diffs")); + assert!(stdout.contains("미리보기 diff")); assert!(stdout.contains("--- /dev/null")); assert!(stdout.contains("+++ .env.example")); assert!(stdout.contains("+API_URL=")); @@ -412,7 +412,8 @@ fn diff_preview_treats_existing_empty_env_example_as_update() { ); let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8"); - assert!(stdout.contains("Append missing keys to .env.example")); + assert!(stdout.contains(".env.example에 누락된 키 추가")); + assert!(!stdout.contains("Append missing keys to .env.example")); assert!(stdout.contains("--- .env.example")); assert!(stdout.contains("+++ .env.example")); assert!(stdout.contains("@@ -0,0 +1,1 @@")); diff --git a/crates/maximus-cli/tests/output_file.rs b/crates/maximus-cli/tests/output_file.rs index 81e4313..5c39b20 100644 --- a/crates/maximus-cli/tests/output_file.rs +++ b/crates/maximus-cli/tests/output_file.rs @@ -122,7 +122,7 @@ fn fix_output_file_write_error_fails_before_applying_mutations() { assert_eq!(output.status.code(), Some(2), "{output:?}"); assert!(output.stdout.is_empty(), "{output:?}"); assert!( - String::from_utf8_lossy(&output.stderr).contains("Maximus failed:"), + String::from_utf8_lossy(&output.stderr).contains("Maximus 실패:"), "{output:?}" ); assert!( diff --git a/scripts/run-packed-wrapper-smoke.mjs b/scripts/run-packed-wrapper-smoke.mjs index 958e1d3..f348fd5 100644 --- a/scripts/run-packed-wrapper-smoke.mjs +++ b/scripts/run-packed-wrapper-smoke.mjs @@ -355,12 +355,12 @@ async function runFallbackBlockingScenarios(installRoot) { await assertWrapperFails( ["audit", configFixture], installRoot, - /A Rust runtime is required when a Maximus config file is present/, + /Maximus config file이 있을 때는 Rust runtime이 필요합니다/, ); await assertWrapperFails( ["audit", fixtureDir, "--only", "env"], installRoot, - /A Rust runtime is required for options not supported by the frozen JS compatibility path/, + /frozen JS compatibility path에서 지원하지 않는 옵션에는 Rust runtime이 필요합니다/, ); await assertWrapperFails( ["fix", fixtureDir], diff --git a/src/cli.js b/src/cli.js index 3f3150c..d2d59c8 100644 --- a/src/cli.js +++ b/src/cli.js @@ -65,7 +65,7 @@ export async function runCli(argv = []) { return; } - throw new Error(`Unknown command "${command}". Run "maximus help" for usage.`); + throw new Error(`알 수 없는 명령 "${command}"입니다. 사용법은 "maximus help"를 실행하세요.`); } function parseArgs(argv) { diff --git a/src/core/format-report.js b/src/core/format-report.js index c3eef6f..b1a2989 100644 --- a/src/core/format-report.js +++ b/src/core/format-report.js @@ -4,12 +4,12 @@ export function formatHelp() { return [ "Maximus", "", - "Bring order to chaotic configs.", + "혼란스러운 설정을 정리합니다.", "", - "Usage", - " maximus audit [path] [--json]", - " maximus doctor [path] [--json]", - " maximus fix [path] [--dry-run] [--json]", + "사용법", + " maximus audit [path] [--only ] [--skip ] [--fail-on ] [--format ] [--json] [--output ]", + " maximus doctor [path] [--only ] [--skip ] [--fail-on ] [--format ] [--json] [--output ]", + " maximus fix [path] [--only ] [--skip ] [--fail-on ] [--dry-run] [--diff] [--env-source-comments] [--fix-id ] [--fix-prefix ] [--format ] [--json] [--output ]", " maximus help", ].join("\n"); } @@ -18,30 +18,30 @@ export function formatAuditReport(result) { const lines = []; lines.push("Maximus audit"); - lines.push(`Target: ${result.rootDir}`); + lines.push(`대상: ${result.rootDir}`); lines.push(""); - lines.push(`Status: ${result.summary.status}`); + lines.push(`상태: ${translateStatus(result.summary.status)}`); lines.push( - `Findings: ${result.summary.blockingFindings} error, ${result.summary.warningFindings} warnings, ${result.summary.infoFindings} info`, + `발견 항목: 오류 ${result.summary.blockingFindings}개, 경고 ${result.summary.warningFindings}개, 정보 ${result.summary.infoFindings}개`, ); - lines.push(`Fixes available: ${result.summary.fixesAvailable}`); + lines.push(`적용 가능한 수정: ${result.summary.fixesAvailable}개`); lines.push(""); - lines.push(`Structure: ${describeStructure(result.structure)}`); + lines.push(`구조: ${describeStructure(result.structure)}`); if (result.findings.length === 0) { lines.push(""); - lines.push("No config drift detected."); + lines.push("설정 차이가 감지되지 않았습니다."); } else { lines.push(""); - lines.push("Findings"); + lines.push("발견 항목"); lines.push(...formatFindings(result)); } if (result.structure.recommendations.length > 0) { lines.push(""); - lines.push("Recommendations"); + lines.push("권장 사항"); for (const recommendation of result.structure.recommendations) { - lines.push(`- ${recommendation}`); + lines.push(`- ${translateMessage(recommendation)}`); } } @@ -54,42 +54,42 @@ export function formatDoctorReport(result) { const fixableFindings = result.findings.filter((finding) => finding.fixable); lines.push("Maximus doctor"); - lines.push(`Target: ${result.rootDir}`); + lines.push(`대상: ${result.rootDir}`); lines.push(""); - lines.push(`Diagnosis: ${result.summary.status}`); - lines.push(`Project shape: ${describeStructure(result.structure)}`); + lines.push(`진단: ${translateStatus(result.summary.status)}`); + lines.push(`프로젝트 구조: ${describeStructure(result.structure)}`); lines.push(""); - lines.push("Prescription"); + lines.push("처방"); if (fixableFindings.length > 0) { - lines.push(`- Run "maximus fix" to apply ${result.summary.fixesAvailable} safe fix(es).`); + lines.push(`- 안전한 수정 ${result.summary.fixesAvailable}개를 적용하려면 "maximus fix"를 실행하세요.`); } else { - lines.push("- No automatic fixes are currently available."); + lines.push("- 현재 적용 가능한 자동 수정이 없습니다."); } if (manualFindings.length > 0) { - lines.push(`- Review ${manualFindings.length} manual issue(s) in priority order below.`); + lines.push(`- 아래 우선순위에 따라 수동 확인 항목 ${manualFindings.length}개를 검토하세요.`); } else { - lines.push("- No manual follow-up is required right now."); + lines.push("- 지금은 수동 후속 조치가 필요하지 않습니다."); } if (result.findings.length === 0) { lines.push(""); - lines.push("No config drift detected."); + lines.push("설정 차이가 감지되지 않았습니다."); } else { lines.push(""); - lines.push("Top 3 priorities"); + lines.push("상위 3개 우선순위"); lines.push(...formatTopPriorities(result)); lines.push(""); - lines.push("Findings"); + lines.push("발견 항목"); lines.push(...formatFindings(result)); } if (result.structure.recommendations.length > 0) { lines.push(""); - lines.push("Recommended structure"); + lines.push("권장 구조"); for (const recommendation of result.structure.recommendations) { - lines.push(`- ${recommendation}`); + lines.push(`- ${translateMessage(recommendation)}`); } } @@ -101,38 +101,38 @@ export function formatFixResult({ dryRun, targetDir, initial, applied, final }) const result = dryRun ? initial : final; lines.push("Maximus fix"); - lines.push(`Target: ${targetDir}`); + lines.push(`대상: ${targetDir}`); lines.push(""); if (dryRun) { - lines.push(`Dry run: ${initial.summary.fixesAvailable} safe fix(es) available.`); + lines.push(`Dry run: 적용 가능한 안전한 수정 ${initial.summary.fixesAvailable}개가 있습니다.`); } else { - lines.push(`Applied: ${applied.length} fix(es).`); + lines.push(`적용됨: 수정 ${applied.length}개.`); } if (applied.length > 0) { lines.push(""); - lines.push("Changes"); + lines.push("변경 사항"); for (const fix of applied) { - lines.push(`- ${fix.title}`); + lines.push(`- ${translateFixTitle(fix.title)}`); for (const file of fix.files) { - lines.push(` file: ${file}`); + lines.push(` 파일: ${file}`); } } } lines.push(""); lines.push( - `Post-check: ${result.summary.blockingFindings} error, ${result.summary.warningFindings} warnings, ${result.summary.infoFindings} info`, + `사후 점검: 오류 ${result.summary.blockingFindings}개, 경고 ${result.summary.warningFindings}개, 정보 ${result.summary.infoFindings}개`, ); if (result.findings.length > 0) { lines.push(""); - lines.push("Remaining findings"); + lines.push("남은 발견 항목"); lines.push(...formatFindings(result)); } else { lines.push(""); - lines.push("Project is currently clean."); + lines.push("현재 프로젝트는 정상입니다."); } return lines.join("\n"); @@ -141,18 +141,18 @@ export function formatFixResult({ dryRun, targetDir, initial, applied, final }) function formatFindings(result) { return result.findings.flatMap((finding) => { const lines = []; - lines.push(`- [${finding.severity}] ${finding.title}`); + lines.push(`- [${translateSeverity(finding.severity)}] ${translateMessage(finding.title)}`); if (finding.file) { - lines.push(` file: ${formatFile(result.rootDir, finding.file)}`); + lines.push(` 파일: ${formatFile(result.rootDir, finding.file)}`); } if (finding.detail) { - lines.push(` detail: ${finding.detail}`); + lines.push(` 상세: ${translateMessage(finding.detail)}`); } if (finding.hint) { - lines.push(` hint: ${finding.hint}`); + lines.push(` 힌트: ${translateMessage(finding.hint)}`); } return lines; @@ -161,16 +161,18 @@ function formatFindings(result) { function formatTopPriorities(result) { return result.findings.slice(0, 3).flatMap((finding, index) => { - const lines = [`${index + 1}. [${finding.severity}] ${finding.title}`]; + const lines = [ + `${index + 1}. [${translateSeverity(finding.severity)}] ${translateMessage(finding.title)}`, + ]; if (finding.file) { - lines.push(` file: ${formatFile(result.rootDir, finding.file)}`); + lines.push(` 파일: ${formatFile(result.rootDir, finding.file)}`); } if (finding.hint) { - lines.push(` next: ${finding.hint}`); + lines.push(` 다음: ${translateMessage(finding.hint)}`); } else if (finding.detail) { - lines.push(` next: ${finding.detail}`); + lines.push(` 다음: ${translateMessage(finding.detail)}`); } return lines; @@ -182,6 +184,512 @@ function formatFile(rootDir, filePath) { } function describeStructure(structure) { - const repoType = structure.isMonorepo ? "monorepo" : "single package"; - return `${repoType}, ${structure.packageCount} package(s), ${structure.configFiles} config file(s), ${structure.envDirectories} env folder(s)`; + const repoType = structure.isMonorepo ? "모노레포" : "단일 패키지"; + return `${repoType}, 패키지 ${structure.packageCount}개, 설정 파일 ${structure.configFiles}개, env 폴더 ${structure.envDirectories}개`; } + +function translateStatus(status) { + return ( + { + clean: "정상", + "attention needed": "조치 필요", + "blocking issues": "차단 이슈 있음", + }[status] ?? status + ); +} + +function translateSeverity(severity) { + return ( + { + error: "오류", + warn: "경고", + info: "정보", + }[severity] ?? severity + ); +} + +function translateFixTitle(value) { + if (value.startsWith("Create ")) { + return `${value.slice("Create ".length)} 생성`; + } + if (value.startsWith("Append missing keys to ")) { + return `${value.slice("Append missing keys to ".length)}에 누락된 키 추가`; + } + return translateMessage(value); +} + +function translateMessage(value) { + const dynamic = translateDynamicMessage(value); + if (dynamic) { + return dynamic; + } + + return MESSAGE_TRANSLATIONS.get(value) ?? value; +} + +function translateDynamicMessage(value) { + const concreteEnv = value.match(/^Concrete env file "(.+)" is not protected by \.gitignore$/); + if (concreteEnv) { + return `구체 env 파일 "${concreteEnv[1]}"이 .gitignore로 보호되지 않음`; + } + + const duplicateEnv = value.match(/^Duplicate env key "(.+)"$/); + if (duplicateEnv) { + return `중복 env key "${duplicateEnv[1]}"`; + } + + const missingConcreteEnv = value.match(/^No concrete value was found for: (.+)\.$/); + if (missingConcreteEnv) { + return `다음 env key에 대한 구체 값을 찾을 수 없습니다: ${missingConcreteEnv[1]}.`; + } + + const missingEnvKeys = value.match(/^Missing keys: (.+)\.$/); + if (missingEnvKeys) { + return `누락된 key: ${missingEnvKeys[1]}.`; + } + + const appendMissingKeys = value.match(/^Run "maximus fix" to append the missing keys to (.+)\.$/); + if (appendMissingKeys) { + return `"maximus fix"를 실행해 ${appendMissingKeys[1]}에 누락된 key를 추가하세요.`; + } + + const editorconfigPrettier = value.match(/^EditorConfig sets (.+), but Prettier sets (.+)\.$/); + if (editorconfigPrettier) { + return `EditorConfig는 ${editorconfigPrettier[1]}를 설정하지만 Prettier는 ${editorconfigPrettier[2]}를 설정합니다.`; + } + + const duplicateConfigDetail = value.match(/^Found (\d+) (.+) config sources in (.*)\.$/); + if (duplicateConfigDetail) { + const directory = duplicateConfigDetail[3] || "."; + return `${directory}에서 ${duplicateConfigDetail[2]} 설정 출처 ${duplicateConfigDetail[1]}개를 찾았습니다.`; + } + + const singleConfigHint = value.match(/^Keep a single (.+) entry point per directory to avoid drift\.$/); + if (singleConfigHint) { + return `차이를 피하려면 디렉터리마다 ${singleConfigHint[1]} 진입점을 하나만 유지하세요.`; + } + + const lockfilesDetail = value.match(/^Found (\d+) known lockfiles in (.+): (.+)\.$/); + if (lockfilesDetail) { + return `${lockfilesDetail[2]}에서 알려진 lockfile ${lockfilesDetail[1]}개를 찾았습니다: ${lockfilesDetail[3]}.`; + } + + const deprecatedOption = value.match(/^Deprecated compiler option "(.+)"$/); + if (deprecatedOption) { + return `deprecated compiler option "${deprecatedOption[1]}" 사용 중`; + } + + const aliasNoTargets = value.match(/^Alias "(.+)" does not declare any targets$/); + if (aliasNoTargets) { + return `alias "${aliasNoTargets[1]}"가 대상을 선언하지 않음`; + } + + const aliasNonString = value.match(/^Alias "(.+)" contains a non-string target$/); + if (aliasNonString) { + return `alias "${aliasNonString[1]}"에 string이 아닌 대상이 포함됨`; + } + + const wildcard = value.match(/^Wildcard shape does not match for alias "(.+)"$/); + if (wildcard) { + return `alias "${wildcard[1]}"의 wildcard 형태가 일치하지 않음`; + } + + const aliasDiffers = value.match(/^Alias "(.+)" differs between tsconfig and package imports$/); + if (aliasDiffers) { + return `alias "${aliasDiffers[1]}"가 tsconfig와 package imports 사이에서 다름`; + } + + const packageShadow = value.match(/^Path alias "(.+)" shadows a package import$/); + if (packageShadow) { + return `path alias "${packageShadow[1]}"가 package import를 shadow함`; + } + + const aliasShadow = value.match(/^Path alias "(.+)" shadows "(.+)"$/); + if (aliasShadow) { + return `path alias "${aliasShadow[1]}"가 "${aliasShadow[2]}"를 shadow함`; + } + + const viteDiffers = value.match(/^Vite alias "(.+)" differs from tsconfig paths$/); + if (viteDiffers) { + return `Vite alias "${viteDiffers[1]}"가 tsconfig paths와 다름`; + } + + const viteMissing = value.match(/^Vite alias "(.+)" is missing from tsconfig paths$/); + if (viteMissing) { + return `Vite alias "${viteMissing[1]}"가 tsconfig paths에 없음`; + } + + const stringArray = value.match(/^"(.+)" must be an array of strings$/); + if (stringArray) { + return `"${stringArray[1]}"는 string array여야 함`; + } + + const nonStringPattern = value.match(/^"(.+)" contains a non-string pattern$/); + if (nonStringPattern) { + return `"${nonStringPattern[1]}"에 string이 아닌 pattern이 포함됨`; + } + + const multiConfig = value.match(/^(.+) config is declared in multiple places$/); + if (multiConfig) { + return `${multiConfig[1]} 설정이 여러 위치에 선언됨`; + } + + const missingKeys = value.match(/^(.+) is missing keys$/); + if (missingKeys) { + return `${missingKeys[1]}에 누락된 key가 있음`; + } + + const addToGitignore = value.match(/^Add "(.+)" to (.+)\.$/); + if (addToGitignore) { + return `${addToGitignore[2]}에 "${addToGitignore[1]}"를 추가하세요.`; + } + + const missingPath = value.match(/^(.+) points to (.+), but the resolved path was not found\.$/); + if (missingPath) { + return `${missingPath[1]}는 ${missingPath[2]}를 가리키지만 해석된 경로를 찾을 수 없습니다.`; + } + + const pointsToPath = value.match(/^(.+) points to (.+)\.$/); + if (pointsToPath) { + return `${pointsToPath[1]}는 ${pointsToPath[2]}를 가리킵니다.`; + } + + const outDirOverlaps = value.match(/^outDir "(.+)" overlaps source root "(.+)"\.$/); + if (outDirOverlaps) { + return `outDir "${outDirOverlaps[1]}"이 source root "${outDirOverlaps[2]}"와 겹칩니다.`; + } + + const outDirNested = value.match(/^outDir "(.+)" is nested inside source root "(.+)"\.$/); + if (outDirNested) { + return `outDir "${outDirNested[1]}"이 source root "${outDirNested[2]}" 안에 있습니다.`; + } + + const outDirContainsInput = value.match(/^outDir "(.+)" contains TypeScript input "(.+)"\.$/); + if (outDirContainsInput) { + return `outDir "${outDirContainsInput[1]}"에 TypeScript 입력 "${outDirContainsInput[2]}"이 포함됩니다.`; + } + + const outDirContainsRoot = value.match(/^outDir "(.+)" contains source root "(.+)"\.$/); + if (outDirContainsRoot) { + return `outDir "${outDirContainsRoot[1]}"이 source root "${outDirContainsRoot[2]}"를 포함합니다.`; + } + + const expectsStringPatterns = value.match( + /^(.+) declares (.+), but TypeScript expects string patterns\.$/, + ); + if (expectsStringPatterns) { + return `${expectsStringPatterns[1]}는 ${expectsStringPatterns[2]}를 선언하지만 TypeScript는 string pattern을 기대합니다.`; + } + + const expectsPatternArray = value.match( + /^(.+) declares (.+), but TypeScript expects an array of string patterns\.$/, + ); + if (expectsPatternArray) { + return `${expectsPatternArray[1]}는 ${expectsPatternArray[2]}를 선언하지만 TypeScript는 string pattern array를 기대합니다.`; + } + + const filesGlob = value.match( + /^(.+) declares (.+), but TypeScript files entries cannot use glob wildcards\.$/, + ); + if (filesGlob) { + return `${filesGlob[1]}는 ${filesGlob[2]}를 선언하지만 TypeScript files 항목에는 glob wildcard를 사용할 수 없습니다.`; + } + + const filesDirectory = value.match(/^(.+) declares (.+), but that path resolves to a directory\.$/); + if (filesDirectory) { + return `${filesDirectory[1]}는 ${filesDirectory[2]}를 선언하지만 해당 path는 directory로 해석됩니다.`; + } + + const filesMissing = value.match( + /^(.+) declares (.+), but that path does not resolve to an existing file\.$/, + ); + if (filesMissing) { + return `${filesMissing[1]}는 ${filesMissing[2]}를 선언하지만 해당 path는 존재하는 파일로 해석되지 않습니다.`; + } + + const includePattern = value.match( + /^include pattern "(.+)" matched (\d+) files under base dir (.+)\.$/, + ); + if (includePattern) { + return `include pattern "${includePattern[1]}"은 base dir ${includePattern[3]} 아래에서 파일 ${includePattern[2]}개와 일치했습니다.`; + } + + const excludePattern = value.match( + /^exclude pattern "(.+)" removed (\d+) files from (\d+) included file\(s\) under base dir (.+)\.$/, + ); + if (excludePattern) { + return `exclude pattern "${excludePattern[1]}"은 base dir ${excludePattern[4]} 아래 포함 파일 ${excludePattern[3]}개 중 ${excludePattern[2]}개를 제외했습니다.`; + } + + return null; +} + +const MESSAGE_TRANSLATIONS = new Map([ + ["Config file could not be parsed", "설정 파일을 파싱할 수 없음"], + ["compilerOptions.paths must be an object", "compilerOptions.paths는 object여야 함"], + ["Path alias target does not exist", "경로 alias 대상이 존재하지 않음"], + ["Include pattern does not match any files", "include pattern이 어떤 파일과도 일치하지 않음"], + ["Exclude pattern does not filter any included files", "exclude pattern이 포함 파일을 제외하지 않음"], + ["Output directory overlaps the TypeScript source root", "출력 디렉터리가 TypeScript source root와 겹침"], + ["Output directory is nested inside the TypeScript source root", "출력 디렉터리가 TypeScript source root 안에 있음"], + ["Output directory contains TypeScript input files", "출력 디렉터리에 TypeScript 입력 파일이 포함됨"], + ["Output directory contains the TypeScript source root", "출력 디렉터리가 TypeScript source root를 포함함"], + ["Missing .env.example contract", ".env.example 계약 파일 누락"], + ["Missing example env file", "예시 env 파일 누락"], + ["Invalid env syntax", "env 문법이 올바르지 않음"], + ["Local env overrides detected", "local env override 감지됨"], + ["Declared env contract is not satisfied locally", "선언된 env 계약이 로컬에서 충족되지 않음"], + ["pnpm-workspace.yaml could not be parsed", "pnpm-workspace.yaml을 파싱할 수 없음"], + [ + "pnpm-workspace.yaml does not declare any package patterns", + "pnpm-workspace.yaml이 package pattern을 선언하지 않음", + ], + ["turbo.json could not be parsed", "turbo.json을 파싱할 수 없음"], + ["turbo.json does not declare any workspace tasks", "turbo.json이 workspace task를 선언하지 않음"], + ["ESLint formatting rules may conflict with Prettier", "ESLint 서식 규칙이 Prettier와 충돌할 수 있음"], + [ + "Formatting-oriented ESLint rules were found, but no explicit Prettier bridge was detected.", + "서식 중심 ESLint 규칙이 발견됐지만 명시적인 Prettier 연결은 감지되지 않았습니다.", + ], + [ + "Consider eslint-config-prettier or plugin:prettier/recommended to reduce formatter churn.", + "포매터 변경 소음을 줄이려면 eslint-config-prettier 또는 plugin:prettier/recommended를 검토하세요.", + ], + ["ESLint and Prettier are configured separately", "ESLint와 Prettier가 별도로 설정됨"], + [ + "That can be fine, but teams often prefer an explicit integration strategy.", + "문제 없을 수도 있지만, 팀에서는 명시적인 통합 전략을 선호하는 경우가 많습니다.", + ], + [ + "Document which tool owns formatting and which tool owns code-quality rules.", + "서식은 어느 도구가 맡고 코드 품질 규칙은 어느 도구가 맡는지 문서화하세요.", + ], + ["Legacy and flat ESLint configs coexist", "legacy ESLint 설정과 flat 설정이 함께 존재함"], + [ + "This directory contains both legacy .eslintrc.* files and flat eslint.config.* files, so ESLint can resolve different rule sets depending on the entry point.", + "이 디렉터리에는 legacy .eslintrc.* 파일과 flat eslint.config.* 파일이 함께 있어 진입점에 따라 ESLint가 서로 다른 규칙 집합을 해석할 수 있습니다.", + ], + [ + "Migrate to eslint.config.* as the single source of truth, then remove the legacy .eslintrc.* files after the new config fully replaces them.", + "eslint.config.*를 단일 기준으로 마이그레이션한 뒤, 새 config가 완전히 대체하면 legacy .eslintrc.* 파일을 제거하세요.", + ], + ["EditorConfig and Prettier disagree", "EditorConfig와 Prettier 설정이 일치하지 않음"], + ["Ignore files disagree on generated artifact coverage", "ignore 파일들의 generated artifact 범위가 일치하지 않음"], + ["Multiple lockfiles are present in one directory", "한 디렉터리에 여러 lockfile이 존재함"], + ["Package tsconfig drifts from the shared base", "package tsconfig가 shared base와 달라짐"], + ["Jest and Vitest configs coexist", "Jest와 Vitest config가 함께 존재함"], + [ + "Node engine support and GitHub Actions matrix are out of sync", + "Node engine 지원 범위와 GitHub Actions matrix가 동기화되지 않음", + ], + [ + "Package ESM type conflicts with tsconfig module output", + "package ESM type이 tsconfig module output과 충돌함", + ], + [ + "Package CommonJS type conflicts with tsconfig module output", + "package CommonJS type이 tsconfig module output과 충돌함", + ], + ["Package entrypoint target is invalid", "package entrypoint 대상이 올바르지 않음"], + ["Package entrypoint target does not exist", "package entrypoint 대상이 존재하지 않음"], + ["Package entrypoint key is invalid", "package entrypoint key가 올바르지 않음"], + ["Package exports object is invalid", "package exports object가 올바르지 않음"], + [ + "Package entrypoint target uses an incompatible file type", + "package entrypoint 대상이 호환되지 않는 파일 타입을 사용함", + ], + [ + "package.json files entry is excluded by nested .npmignore", + "package.json files 항목이 nested .npmignore에 의해 제외됨", + ], + ["references must be an array", "references는 array여야 함"], + [ + "Each project reference entry must be an object with a path", + "각 project reference 항목은 path가 있는 object여야 함", + ], + [ + "Each project reference entry must declare a string path", + "각 project reference 항목은 string path를 선언해야 함", + ], + ["Project reference target does not exist", "project reference 대상이 존재하지 않음"], + ["Project reference target could not be read", "project reference 대상을 읽을 수 없음"], + ["Project reference target could not be parsed", "project reference 대상을 파싱할 수 없음"], + [ + "Project reference target must point to a tsconfig file", + "project reference 대상은 tsconfig 파일을 가리켜야 함", + ], + ["Referenced project must enable composite", "참조된 project는 composite을 활성화해야 함"], + ["Configured typeRoots entry does not exist", "설정된 typeRoots 항목이 존재하지 않음"], + [ + "compilerOptions.types and typeRoots both narrow ambient type resolution", + "compilerOptions.types와 typeRoots가 모두 ambient type 해석 범위를 좁힘", + ], + [ + "compilerOptions.typeRoots disables default @types discovery", + "compilerOptions.typeRoots가 기본 @types discovery를 비활성화함", + ], + ["\"compilerOptions.types\" must be an array of package names", "\"compilerOptions.types\"는 package name array여야 함"], + [ + "\"compilerOptions.types\" contains a non-string package name", + "\"compilerOptions.types\"에 string이 아닌 package name이 포함됨", + ], + [ + "\"compilerOptions.typeRoots\" must be an array of directory paths", + "\"compilerOptions.typeRoots\"는 directory path array여야 함", + ], + [ + "\"compilerOptions.typeRoots\" contains a non-string path", + "\"compilerOptions.typeRoots\"에 string이 아닌 path가 포함됨", + ], + ["\"files\" entries must point to explicit files", "\"files\" 항목은 명시적인 파일을 가리켜야 함"], + ["\"files\" entries must point to readable files", "\"files\" 항목은 읽을 수 있는 파일을 가리켜야 함"], + ["\"files\" entries must point to files", "\"files\" 항목은 파일을 가리켜야 함"], + [ + "\"files\" entries must point to supported TypeScript input files", + "\"files\" 항목은 지원되는 TypeScript 입력 파일을 가리켜야 함", + ], + ["\"files\" entries must point to existing files", "\"files\" 항목은 존재하는 파일을 가리켜야 함"], + ["Inherited tsconfig could not be found", "상속된 tsconfig를 찾을 수 없음"], + ["Inherited tsconfig extends cycle detected", "상속된 tsconfig extends cycle이 감지됨"], + ["Inherited tsconfig could not be read", "상속된 tsconfig를 읽을 수 없음"], + ["Inherited tsconfig could not be parsed", "상속된 tsconfig를 파싱할 수 없음"], + ["Inherited config must point to a tsconfig file", "상속된 config는 tsconfig 파일을 가리켜야 함"], + [ + "Referenced project must set compilerOptions.composite to a boolean", + "참조된 project는 compilerOptions.composite을 boolean으로 설정해야 함", + ], + [ + "Inherited tsconfig must set compilerOptions.composite to a boolean", + "상속된 tsconfig는 compilerOptions.composite을 boolean으로 설정해야 함", + ], + ["Runtime env files exist, but .env.example is missing.", "실행 env 파일은 있지만 .env.example이 없습니다."], + [ + "The file uses a pnpm-workspace.yaml shape that this check does not understand.", + "이 파일은 이 check가 이해하지 못하는 pnpm-workspace.yaml 형태를 사용합니다.", + ], + [ + 'Use a simple packages: block list such as packages: ["apps/*", "packages/*"].', + 'packages: ["apps/*", "packages/*"]처럼 단순한 packages: block list를 사용하세요.', + ], + [ + "No package globs were found under packages:, and this repo has at most one package file, so the workspace file looks like a placeholder.", + "packages: 아래에 package glob이 없고 이 repo에는 package file이 최대 1개라 workspace 파일이 placeholder처럼 보입니다.", + ], + [ + "No package globs were found under packages:, so workspace packages are not declared yet.", + "packages: 아래에 package glob이 없어 workspace package가 아직 선언되지 않았습니다.", + ], + [ + "Add a packages: block with one or more workspace globs, or remove the file until the repo actually needs a workspace definition.", + "하나 이상의 workspace glob이 있는 packages: block을 추가하거나, 실제 workspace 정의가 필요할 때까지 파일을 제거하세요.", + ], + ["The turbo.json file is not valid JSONC.", "turbo.json 파일이 올바른 JSONC가 아닙니다."], + [ + "Fix the syntax error or replace the placeholder file with a real Turbo config.", + "문법 오류를 고치거나 placeholder 파일을 실제 Turbo config로 바꾸세요.", + ], + [ + "The file does not contain any task definitions that would make the workspace config meaningful.", + "이 파일에는 workspace config를 의미 있게 만드는 task 정의가 없습니다.", + ], + [ + "Add a non-empty tasks or pipeline map, or remove the placeholder turbo.json until the workspace needs it.", + "비어 있지 않은 tasks 또는 pipeline map을 추가하거나, workspace가 필요할 때까지 placeholder turbo.json을 제거하세요.", + ], + ["A committed .env.example file is missing.", "커밋된 .env.example 파일이 없습니다."], + [ + "Current config surface looks healthy. Keep shared rules centralized as the repo grows.", + "현재 설정 표면은 정상입니다. repo가 커져도 shared rule을 중앙에 유지하세요.", + ], + [ + "Introduce a shared tsconfig.base.json so packages inherit one source of truth.", + "package들이 단일 기준을 상속하도록 shared tsconfig.base.json을 도입하세요.", + ], + [ + "Reduce repo-wide ESLint entry points unless packages genuinely need different rule sets.", + "package별로 다른 규칙 집합이 꼭 필요하지 않다면 repo 전체 ESLint 진입점을 줄이세요.", + ], + [ + "Use .env.example files consistently so onboarding does not depend on tribal knowledge.", + ".env.example 파일을 일관되게 사용해 온보딩이 구두 지식에 의존하지 않게 하세요.", + ], + [ + "Use shell-style env syntax or move comments to their own line.", + "shell-style env 문법을 사용하거나 주석을 별도 줄로 옮기세요.", + ], + [ + "Protect concrete env files with an exact .gitignore entry before committing secrets.", + "secret을 커밋하기 전에 구체 env 파일을 정확한 .gitignore 항목으로 보호하세요.", + ], + ["Run \"maximus fix\" to create a blank contract file.", "\"maximus fix\"를 실행해 빈 계약 파일을 생성하세요."], + [ + "Replace the value with a blank or placeholder string before sharing the repo.", + "repo를 공유하기 전에 값을 빈 문자열이나 placeholder로 바꾸세요.", + ], + [ + "Make sure local-only overrides are intentional and documented in .env.example.", + "local-only override가 의도된 것이며 .env.example에 문서화되어 있는지 확인하세요.", + ], + [ + "If these are injected by CI, keep the contract documented. Otherwise add them to your local env files.", + "CI에서 주입되는 값이면 계약을 문서화하세요. 아니면 로컬 env 파일에 추가하세요.", + ], + ["Fix invalid JSONC syntax before relying on this config.", "이 config를 신뢰하기 전에 올바르지 않은 JSONC 문법을 고치세요."], + ["Remove legacy flags before they become upgrade blockers.", "upgrade blocker가 되기 전에 legacy flag를 제거하세요."], + ["Rewrite paths to the standard { alias: [targets] } shape.", "paths를 표준 { alias: [targets] } 형태로 다시 작성하세요."], + ["Add a valid target or remove the alias entry.", "유효한 대상을 추가하거나 alias 항목을 제거하세요."], + ["Replace non-string entries with valid path strings.", "string이 아닌 항목을 유효한 path string으로 교체하세요."], + ["Keep wildcard placement aligned so imports resolve predictably.", "import가 예측 가능하게 해석되도록 wildcard 위치를 맞추세요."], + [ + "Update or remove stale aliases before they break editor and build resolution.", + "editor와 build 해석을 깨기 전에 오래된 alias를 수정하거나 제거하세요.", + ], + [ + "Align both alias surfaces so runtime and editor resolution stay consistent.", + "runtime과 editor 해석이 일치하도록 두 alias 표면을 맞추세요.", + ], + [ + "This directory declares both Jest and Vitest configuration, so tests can run under different environments depending on the command.", + "이 디렉터리는 Jest와 Vitest config를 모두 선언하므로 명령에 따라 서로 다른 환경에서 test가 실행될 수 있습니다.", + ], + [ + "Pick one runner for this package, or document the split with separate config ownership and scripts.", + "이 package의 runner 하나를 선택하거나, 별도 config ownership과 script로 분리를 문서화하세요.", + ], + [ + "Align EditorConfig and Prettier so editor saves do not fight formatter output.", + "편집기 저장과 포매터 출력이 충돌하지 않도록 EditorConfig와 Prettier를 맞추세요.", + ], + [ + "Keep one lockfile per directory so dependency resolution stays predictable. Separate package directories can each have their own lockfile.", + "dependency 해석이 예측 가능하도록 디렉터리마다 lockfile을 하나만 유지하세요. 별도 package 디렉터리는 각자 lockfile을 가질 수 있습니다.", + ], + [ + "Fix or remove empty include patterns before TypeScript silently skips expected inputs.", + "TypeScript가 예상 입력을 조용히 건너뛰기 전에 빈 include pattern을 수정하거나 제거하세요.", + ], + [ + "Remove or tighten exclude entries that do not change the effective TypeScript input set.", + "실제 TypeScript 입력 집합을 바꾸지 않는 exclude 항목을 제거하거나 좁히세요.", + ], + [ + "Next.js generates .next/types during development or build, so this include can be empty before .next exists.", + "Next.js는 개발 또는 build 중 .next/types를 생성하므로 .next가 생기기 전에는 이 include가 비어 있을 수 있습니다.", + ], + [ + "Move emit output outside the source root so build artifacts do not overwrite source files.", + "build artifact가 source file을 덮어쓰지 않도록 emit output을 source root 밖으로 옮기세요.", + ], + [ + "Move emit output outside any directory that currently contains TypeScript input files.", + "현재 TypeScript 입력 파일이 들어 있는 디렉터리 밖으로 emit output을 옮기세요.", + ], + [ + "Prefer an output directory that is completely separate from the TypeScript source root.", + "TypeScript source root와 완전히 분리된 output directory를 사용하세요.", + ], + ["Create .env.example with safe defaults.", "안전한 기본값으로 .env.example을 생성하세요."], + ["Update the alias to an existing directory.", "alias를 존재하는 디렉터리로 수정하세요."], + ["No extra work is needed.", "추가 작업은 필요하지 않습니다."], + ["Package scripts are tidy", "package script가 정리되어 있음"], +]); diff --git a/test/doctor-report-contract.test.js b/test/doctor-report-contract.test.js index 69f13b3..5b27cd2 100644 --- a/test/doctor-report-contract.test.js +++ b/test/doctor-report-contract.test.js @@ -3,7 +3,7 @@ import test from "node:test"; import { formatDoctorReport } from "../src/core/format-report.js"; -test("JS doctor formatter includes the Top 3 priorities section", () => { +test("JS doctor formatter includes the Korean top-priority section", () => { const report = formatDoctorReport({ rootDir: "/tmp/project", summary: { @@ -45,10 +45,214 @@ test("JS doctor formatter includes the Top 3 priorities section", () => { ], }); - assert.match(report, /Top 3 priorities/); - assert.match(report, /1\. \[error\] Missing example env file/); - assert.match(report, / file: \.env/); - assert.match(report, / next: Create \.env\.example with safe defaults\./); - assert.match(report, /2\. \[warn\] Path alias target does not exist/); - assert.match(report, /3\. \[info\] Package scripts are tidy/); + assert.match(report, /상위 3개 우선순위/); + assert.match(report, /1\. \[오류\] 예시 env 파일 누락/); + assert.match(report, / 파일: \.env/); + assert.match(report, / 다음: 안전한 기본값으로 \.env\.example을 생성하세요\./); + assert.match(report, /2\. \[경고\] 경로 alias 대상이 존재하지 않음/); + assert.match(report, /3\. \[정보\] package script가 정리되어 있음/); +}); + +test("JS formatter translates output-path finding titles like Rust", () => { + const report = formatDoctorReport({ + rootDir: "/tmp/project", + summary: { + status: "blocking issues", + fixesAvailable: 0, + }, + structure: { + isMonorepo: false, + packageCount: 1, + configFiles: 1, + envDirectories: 0, + recommendations: [], + }, + findings: [ + { + severity: "error", + title: "Output directory overlaps the TypeScript source root", + file: "/tmp/project/tsconfig.json", + detail: 'outDir "src" overlaps source root "src".', + hint: "Move emit output outside the source root so build artifacts do not overwrite source files.", + fixable: false, + }, + ], + }); + + assert.match(report, /출력 디렉터리가 TypeScript source root와 겹침/); + assert.match(report, /outDir "src"이 source root "src"와 겹칩니다\./); + assert.match(report, /build artifact가 source file을 덮어쓰지 않도록/); +}); + +test("JS formatter translates dynamic missing concrete env details", () => { + const report = formatDoctorReport({ + rootDir: "/tmp/project", + summary: { + status: "attention needed", + fixesAvailable: 0, + }, + structure: { + isMonorepo: false, + packageCount: 1, + configFiles: 1, + envDirectories: 1, + recommendations: [], + }, + findings: [ + { + severity: "warn", + title: "Declared env contract is not satisfied locally", + file: "/tmp/project/.env.example", + detail: "No concrete value was found for: CI_ONLY.", + hint: "If these are injected by CI, keep the contract documented. Otherwise add them to your local env files.", + fixable: false, + }, + ], + }); + + assert.match(report, /선언된 env 계약이 로컬에서 충족되지 않음/); + assert.match(report, /다음 env key에 대한 구체 값을 찾을 수 없습니다: CI_ONLY\./); + assert.doesNotMatch(report, /No concrete value was found/); +}); + +test("JS formatter translates dynamic env sync details", () => { + const report = formatDoctorReport({ + rootDir: "/tmp/project", + summary: { + status: "attention needed", + fixesAvailable: 1, + }, + structure: { + isMonorepo: false, + packageCount: 1, + configFiles: 1, + envDirectories: 1, + recommendations: [], + }, + findings: [ + { + severity: "warn", + title: ".env.example is missing keys", + file: "/tmp/project/.env.example", + detail: "Missing keys: OTHER.", + hint: 'Run "maximus fix" to append the missing keys to .env.example.', + fixable: true, + }, + ], + }); + + assert.match(report, /\.env\.example에 누락된 key가 있음/); + assert.match(report, /누락된 key: OTHER\./); + assert.match(report, /"maximus fix"를 실행해 \.env\.example에 누락된 key를 추가하세요\./); + assert.doesNotMatch(report, /Missing keys/); + assert.doesNotMatch(report, /append the missing keys/); +}); + +test("JS formatter translates workspace runner and EditorConfig human messages", () => { + const report = formatDoctorReport({ + rootDir: "/tmp/project", + summary: { + status: "attention needed", + fixesAvailable: 0, + }, + structure: { + isMonorepo: true, + packageCount: 2, + configFiles: 4, + envDirectories: 0, + recommendations: [], + }, + findings: [ + { + severity: "warn", + title: "pnpm-workspace.yaml does not declare any package patterns", + file: "/tmp/project/pnpm-workspace.yaml", + detail: "No package globs were found under packages:, so workspace packages are not declared yet.", + hint: "Add a packages: block with one or more workspace globs, or remove the file until the repo actually needs a workspace definition.", + fixable: false, + }, + { + severity: "warn", + title: "Jest and Vitest configs coexist", + file: "/tmp/project/package.json", + detail: + "This directory declares both Jest and Vitest configuration, so tests can run under different environments depending on the command.", + hint: "Pick one runner for this package, or document the split with separate config ownership and scripts.", + fixable: false, + }, + { + severity: "warn", + title: "EditorConfig and Prettier disagree", + file: "/tmp/project/.editorconfig", + detail: + "EditorConfig sets indent_style=tab, indent_size=4, end_of_line=crlf, but Prettier sets useTabs=false, tabWidth=2, endOfLine=lf.", + hint: "Align EditorConfig and Prettier so editor saves do not fight formatter output.", + fixable: false, + }, + ], + }); + + assert.match(report, /pnpm-workspace\.yaml이 package pattern을 선언하지 않음/); + assert.match(report, /workspace package가 아직 선언되지 않았습니다/); + assert.match(report, /Jest와 Vitest config가 함께 존재함/); + assert.match(report, /명령에 따라 서로 다른 환경에서 test가 실행될 수 있습니다/); + assert.match(report, /EditorConfig와 Prettier 설정이 일치하지 않음/); + assert.match(report, /EditorConfig는 indent_style=tab, indent_size=4, end_of_line=crlf를 설정하지만/); + assert.match(report, /편집기 저장과 포매터 출력이 충돌하지 않도록/); + assert.doesNotMatch(report, /does not declare any package patterns/); + assert.doesNotMatch(report, /This directory declares both Jest and Vitest/); + assert.doesNotMatch(report, /EditorConfig sets/); + assert.doesNotMatch(report, /formatter output/); +}); + +test("JS formatter translates duplicate config and structure guidance", () => { + const report = formatDoctorReport({ + rootDir: "/tmp/project", + summary: { + status: "blocking issues", + fixesAvailable: 0, + }, + structure: { + isMonorepo: true, + packageCount: 2, + configFiles: 4, + envDirectories: 0, + recommendations: [ + "Introduce a shared tsconfig.base.json so packages inherit one source of truth.", + "Reduce repo-wide ESLint entry points unless packages genuinely need different rule sets.", + ], + }, + findings: [ + { + severity: "error", + title: "ESLint config is declared in multiple places", + file: "/tmp/project/package.json", + detail: "Found 2 ESLint config sources in ..", + hint: "Keep a single ESLint entry point per directory to avoid drift.", + fixable: false, + }, + { + severity: "error", + title: "Legacy and flat ESLint configs coexist", + file: "/tmp/project/.eslintrc.json", + detail: + "This directory contains both legacy .eslintrc.* files and flat eslint.config.* files, so ESLint can resolve different rule sets depending on the entry point.", + hint: + "Migrate to eslint.config.* as the single source of truth, then remove the legacy .eslintrc.* files after the new config fully replaces them.", + fixable: false, + }, + ], + }); + + assert.match(report, /ESLint 설정이 여러 위치에 선언됨/); + assert.match(report, /\.에서 ESLint 설정 출처 2개를 찾았습니다/); + assert.match(report, /차이를 피하려면 디렉터리마다 ESLint 진입점을 하나만 유지하세요/); + assert.match(report, /legacy ESLint 설정과 flat 설정이 함께 존재함/); + assert.match(report, /eslint\.config\.\*를 단일 기준으로 마이그레이션/); + assert.match(report, /shared tsconfig\.base\.json을 도입하세요/); + assert.match(report, /repo 전체 ESLint 진입점을 줄이세요/); + assert.doesNotMatch(report, /Found 2 ESLint config sources/); + assert.doesNotMatch(report, /Keep a single ESLint/); + assert.doesNotMatch(report, /Migrate to eslint\.config/); + assert.doesNotMatch(report, /Reduce repo-wide ESLint/); }); diff --git a/test/golden-rust/clean-project.audit.txt b/test/golden-rust/clean-project.audit.txt index dc2a826..607592a 100644 --- a/test/golden-rust/clean-project.audit.txt +++ b/test/golden-rust/clean-project.audit.txt @@ -1,13 +1,13 @@ Maximus audit -Target: +대상: -Status: clean -Findings: 0 error, 0 warnings, 0 info -Fixes available: 0 +상태: 정상 +발견 항목: 오류 0개, 경고 0개, 정보 0개 +적용 가능한 수정: 0개 -Structure: single package, 1 package(s), 1 config file(s), 0 env folder(s) +구조: 단일 패키지, 패키지 1개, 설정 파일 1개, env 폴더 0개 -No config drift detected. +설정 차이가 감지되지 않았습니다. -Recommendations -- Current config surface looks healthy. Keep shared rules centralized as the repo grows. +권장 사항 +- 현재 설정 표면은 정상입니다. repo가 커져도 shared rule을 중앙에 유지하세요. diff --git a/test/golden-rust/clean-project.doctor.txt b/test/golden-rust/clean-project.doctor.txt index 6f26737..08ad097 100644 --- a/test/golden-rust/clean-project.doctor.txt +++ b/test/golden-rust/clean-project.doctor.txt @@ -1,14 +1,14 @@ Maximus doctor -Target: +대상: -Diagnosis: clean -Project shape: single package, 1 package(s), 1 config file(s), 0 env folder(s) +진단: 정상 +프로젝트 구조: 단일 패키지, 패키지 1개, 설정 파일 1개, env 폴더 0개 -Prescription -- No automatic fixes are currently available. -- No manual follow-up is required right now. +처방 +- 현재 적용 가능한 자동 수정이 없습니다. +- 지금은 수동 후속 조치가 필요하지 않습니다. -No config drift detected. +설정 차이가 감지되지 않았습니다. -Recommended structure -- Current config surface looks healthy. Keep shared rules centralized as the repo grows. +권장 구조 +- 현재 설정 표면은 정상입니다. repo가 커져도 shared rule을 중앙에 유지하세요. diff --git a/test/golden-rust/env-missing-example.audit.txt b/test/golden-rust/env-missing-example.audit.txt index a12e9ee..dd1c1c0 100644 --- a/test/golden-rust/env-missing-example.audit.txt +++ b/test/golden-rust/env-missing-example.audit.txt @@ -1,18 +1,18 @@ Maximus audit -Target: +대상: -Status: attention needed -Findings: 0 error, 2 warnings, 0 info -Fixes available: 1 +상태: 조치 필요 +발견 항목: 오류 0개, 경고 2개, 정보 0개 +적용 가능한 수정: 1개 -Structure: single package, 0 package(s), 1 config file(s), 1 env folder(s) +구조: 단일 패키지, 패키지 0개, 설정 파일 1개, env 폴더 1개 -Findings -- [warn] Concrete env file ".env" is not protected by .gitignore - file: .env - detail: Add ".env" to .gitignore. - hint: Protect concrete env files with an exact .gitignore entry before committing secrets. -- [warn] Missing .env.example contract - file: .env - detail: Runtime env files exist, but .env.example is missing. - hint: Run "maximus fix" to create a blank contract file. +발견 항목 +- [경고] 구체 env 파일 ".env"이 .gitignore로 보호되지 않음 + 파일: .env + 상세: .gitignore에 ".env"를 추가하세요. + 힌트: secret을 커밋하기 전에 구체 env 파일을 정확한 .gitignore 항목으로 보호하세요. +- [경고] .env.example 계약 파일 누락 + 파일: .env + 상세: 실행 env 파일은 있지만 .env.example이 없습니다. + 힌트: "maximus fix"를 실행해 빈 계약 파일을 생성하세요. diff --git a/test/golden-rust/env-missing-example.doctor.txt b/test/golden-rust/env-missing-example.doctor.txt index 7ef23df..e806ee4 100644 --- a/test/golden-rust/env-missing-example.doctor.txt +++ b/test/golden-rust/env-missing-example.doctor.txt @@ -1,27 +1,27 @@ Maximus doctor -Target: +대상: -Diagnosis: attention needed -Project shape: single package, 0 package(s), 1 config file(s), 1 env folder(s) +진단: 조치 필요 +프로젝트 구조: 단일 패키지, 패키지 0개, 설정 파일 1개, env 폴더 1개 -Prescription -- Run "maximus fix" to apply 1 safe fix(es). -- Review 1 manual issue(s) in priority order below. +처방 +- 안전한 수정 1개를 적용하려면 "maximus fix"를 실행하세요. +- 아래 우선순위에 따라 수동 확인 항목 1개를 검토하세요. -Top 3 priorities -1. [warn] Concrete env file ".env" is not protected by .gitignore - file: .env - next: Protect concrete env files with an exact .gitignore entry before committing secrets. -2. [warn] Missing .env.example contract - file: .env - next: Run "maximus fix" to create a blank contract file. +상위 3개 우선순위 +1. [경고] 구체 env 파일 ".env"이 .gitignore로 보호되지 않음 + 파일: .env + 다음: secret을 커밋하기 전에 구체 env 파일을 정확한 .gitignore 항목으로 보호하세요. +2. [경고] .env.example 계약 파일 누락 + 파일: .env + 다음: "maximus fix"를 실행해 빈 계약 파일을 생성하세요. -Findings -- [warn] Concrete env file ".env" is not protected by .gitignore - file: .env - detail: Add ".env" to .gitignore. - hint: Protect concrete env files with an exact .gitignore entry before committing secrets. -- [warn] Missing .env.example contract - file: .env - detail: Runtime env files exist, but .env.example is missing. - hint: Run "maximus fix" to create a blank contract file. +발견 항목 +- [경고] 구체 env 파일 ".env"이 .gitignore로 보호되지 않음 + 파일: .env + 상세: .gitignore에 ".env"를 추가하세요. + 힌트: secret을 커밋하기 전에 구체 env 파일을 정확한 .gitignore 항목으로 보호하세요. +- [경고] .env.example 계약 파일 누락 + 파일: .env + 상세: 실행 env 파일은 있지만 .env.example이 없습니다. + 힌트: "maximus fix"를 실행해 빈 계약 파일을 생성하세요. diff --git a/test/golden-rust/env-missing-example.fix-dry-run.txt b/test/golden-rust/env-missing-example.fix-dry-run.txt index ace2750..7c9a8a2 100644 --- a/test/golden-rust/env-missing-example.fix-dry-run.txt +++ b/test/golden-rust/env-missing-example.fix-dry-run.txt @@ -1,16 +1,16 @@ Maximus fix -Target: +대상: -Dry run: 1 safe fix(es) available. +Dry run: 적용 가능한 안전한 수정 1개가 있습니다. -Post-check: 0 error, 2 warnings, 0 info +사후 점검: 오류 0개, 경고 2개, 정보 0개 -Remaining findings -- [warn] Concrete env file ".env" is not protected by .gitignore - file: .env - detail: Add ".env" to .gitignore. - hint: Protect concrete env files with an exact .gitignore entry before committing secrets. -- [warn] Missing .env.example contract - file: .env - detail: Runtime env files exist, but .env.example is missing. - hint: Run "maximus fix" to create a blank contract file. +남은 발견 항목 +- [경고] 구체 env 파일 ".env"이 .gitignore로 보호되지 않음 + 파일: .env + 상세: .gitignore에 ".env"를 추가하세요. + 힌트: secret을 커밋하기 전에 구체 env 파일을 정확한 .gitignore 항목으로 보호하세요. +- [경고] .env.example 계약 파일 누락 + 파일: .env + 상세: 실행 env 파일은 있지만 .env.example이 없습니다. + 힌트: "maximus fix"를 실행해 빈 계약 파일을 생성하세요. diff --git a/test/golden-rust/tsconfig-missing-alias.audit.txt b/test/golden-rust/tsconfig-missing-alias.audit.txt index ebf67d2..713b439 100644 --- a/test/golden-rust/tsconfig-missing-alias.audit.txt +++ b/test/golden-rust/tsconfig-missing-alias.audit.txt @@ -1,14 +1,14 @@ Maximus audit -Target: +대상: -Status: blocking issues -Findings: 1 error, 0 warnings, 0 info -Fixes available: 0 +상태: 차단 이슈 있음 +발견 항목: 오류 1개, 경고 0개, 정보 0개 +적용 가능한 수정: 0개 -Structure: single package, 1 package(s), 2 config file(s), 0 env folder(s) +구조: 단일 패키지, 패키지 1개, 설정 파일 2개, env 폴더 0개 -Findings -- [error] Path alias target does not exist - file: tsconfig.json - detail: @missing/* points to ghost/*, but the resolved path was not found. - hint: Update or remove stale aliases before they break editor and build resolution. +발견 항목 +- [오류] 경로 alias 대상이 존재하지 않음 + 파일: tsconfig.json + 상세: @missing/*는 ghost/*를 가리키지만 해석된 경로를 찾을 수 없습니다. + 힌트: editor와 build 해석을 깨기 전에 오래된 alias를 수정하거나 제거하세요. diff --git a/test/golden-rust/tsconfig-missing-alias.doctor.txt b/test/golden-rust/tsconfig-missing-alias.doctor.txt index da90c02..dd88bb7 100644 --- a/test/golden-rust/tsconfig-missing-alias.doctor.txt +++ b/test/golden-rust/tsconfig-missing-alias.doctor.txt @@ -1,20 +1,20 @@ Maximus doctor -Target: +대상: -Diagnosis: blocking issues -Project shape: single package, 1 package(s), 2 config file(s), 0 env folder(s) +진단: 차단 이슈 있음 +프로젝트 구조: 단일 패키지, 패키지 1개, 설정 파일 2개, env 폴더 0개 -Prescription -- No automatic fixes are currently available. -- Review 1 manual issue(s) in priority order below. +처방 +- 현재 적용 가능한 자동 수정이 없습니다. +- 아래 우선순위에 따라 수동 확인 항목 1개를 검토하세요. -Top 3 priorities -1. [error] Path alias target does not exist - file: tsconfig.json - next: Update or remove stale aliases before they break editor and build resolution. +상위 3개 우선순위 +1. [오류] 경로 alias 대상이 존재하지 않음 + 파일: tsconfig.json + 다음: editor와 build 해석을 깨기 전에 오래된 alias를 수정하거나 제거하세요. -Findings -- [error] Path alias target does not exist - file: tsconfig.json - detail: @missing/* points to ghost/*, but the resolved path was not found. - hint: Update or remove stale aliases before they break editor and build resolution. +발견 항목 +- [오류] 경로 alias 대상이 존재하지 않음 + 파일: tsconfig.json + 상세: @missing/*는 ghost/*를 가리키지만 해석된 경로를 찾을 수 없습니다. + 힌트: editor와 build 해석을 깨기 전에 오래된 alias를 수정하거나 제거하세요. diff --git a/test/golden-rust/tsconfig-patterns.doctor.txt b/test/golden-rust/tsconfig-patterns.doctor.txt index a194581..461f443 100644 --- a/test/golden-rust/tsconfig-patterns.doctor.txt +++ b/test/golden-rust/tsconfig-patterns.doctor.txt @@ -1,34 +1,34 @@ Maximus doctor -Target: +대상: -Diagnosis: attention needed -Project shape: single package, 0 package(s), 4 config file(s), 0 env folder(s) +진단: 조치 필요 +프로젝트 구조: 단일 패키지, 패키지 0개, 설정 파일 4개, env 폴더 0개 -Prescription -- No automatic fixes are currently available. -- Review 3 manual issue(s) in priority order below. +처방 +- 현재 적용 가능한 자동 수정이 없습니다. +- 아래 우선순위에 따라 수동 확인 항목 3개를 검토하세요. -Top 3 priorities -1. [warn] Include pattern does not match any files - file: empty-include/tsconfig.json - next: Fix or remove empty include patterns before TypeScript silently skips expected inputs. -2. [info] Exclude pattern does not filter any included files - file: empty-exclude/tsconfig.json - next: Remove or tighten exclude entries that do not change the effective TypeScript input set. -3. [info] Exclude pattern does not filter any included files - file: exclude-only/tsconfig.json - next: Remove or tighten exclude entries that do not change the effective TypeScript input set. +상위 3개 우선순위 +1. [경고] include pattern이 어떤 파일과도 일치하지 않음 + 파일: empty-include/tsconfig.json + 다음: TypeScript가 예상 입력을 조용히 건너뛰기 전에 빈 include pattern을 수정하거나 제거하세요. +2. [정보] exclude pattern이 포함 파일을 제외하지 않음 + 파일: empty-exclude/tsconfig.json + 다음: 실제 TypeScript 입력 집합을 바꾸지 않는 exclude 항목을 제거하거나 좁히세요. +3. [정보] exclude pattern이 포함 파일을 제외하지 않음 + 파일: exclude-only/tsconfig.json + 다음: 실제 TypeScript 입력 집합을 바꾸지 않는 exclude 항목을 제거하거나 좁히세요. -Findings -- [warn] Include pattern does not match any files - file: empty-include/tsconfig.json - detail: include pattern "src/missing/**/*.ts" matched 0 files under base dir /empty-include. - hint: Fix or remove empty include patterns before TypeScript silently skips expected inputs. -- [info] Exclude pattern does not filter any included files - file: empty-exclude/tsconfig.json - detail: exclude pattern "generated/**/*.ts" removed 0 files from 1 included file(s) under base dir /empty-exclude. - hint: Remove or tighten exclude entries that do not change the effective TypeScript input set. -- [info] Exclude pattern does not filter any included files - file: exclude-only/tsconfig.json - detail: exclude pattern "missing/**/*.ts" removed 0 files from 1 included file(s) under base dir /exclude-only. - hint: Remove or tighten exclude entries that do not change the effective TypeScript input set. +발견 항목 +- [경고] include pattern이 어떤 파일과도 일치하지 않음 + 파일: empty-include/tsconfig.json + 상세: include pattern "src/missing/**/*.ts"은 base dir /empty-include 아래에서 파일 0개와 일치했습니다. + 힌트: TypeScript가 예상 입력을 조용히 건너뛰기 전에 빈 include pattern을 수정하거나 제거하세요. +- [정보] exclude pattern이 포함 파일을 제외하지 않음 + 파일: empty-exclude/tsconfig.json + 상세: exclude pattern "generated/**/*.ts"은 base dir /empty-exclude 아래 포함 파일 1개 중 0개를 제외했습니다. + 힌트: 실제 TypeScript 입력 집합을 바꾸지 않는 exclude 항목을 제거하거나 좁히세요. +- [정보] exclude pattern이 포함 파일을 제외하지 않음 + 파일: exclude-only/tsconfig.json + 상세: exclude pattern "missing/**/*.ts"은 base dir /exclude-only 아래 포함 파일 1개 중 0개를 제외했습니다. + 힌트: 실제 TypeScript 입력 집합을 바꾸지 않는 exclude 항목을 제거하거나 좁히세요. diff --git a/test/golden-rust/windows-crlf.doctor.txt b/test/golden-rust/windows-crlf.doctor.txt index fb9fe2a..619e656 100644 --- a/test/golden-rust/windows-crlf.doctor.txt +++ b/test/golden-rust/windows-crlf.doctor.txt @@ -1,34 +1,34 @@ Maximus doctor -Target: +대상: -Diagnosis: blocking issues -Project shape: single package, 0 package(s), 2 config file(s), 1 env folder(s) +진단: 차단 이슈 있음 +프로젝트 구조: 단일 패키지, 패키지 0개, 설정 파일 2개, env 폴더 1개 -Prescription -- Run "maximus fix" to apply 1 safe fix(es). -- Review 2 manual issue(s) in priority order below. +처방 +- 안전한 수정 1개를 적용하려면 "maximus fix"를 실행하세요. +- 아래 우선순위에 따라 수동 확인 항목 2개를 검토하세요. -Top 3 priorities -1. [error] Path alias target does not exist - file: tsconfig.json - next: Update or remove stale aliases before they break editor and build resolution. -2. [warn] Concrete env file ".env" is not protected by .gitignore - file: .env - next: Protect concrete env files with an exact .gitignore entry before committing secrets. -3. [warn] Missing .env.example contract - file: .env - next: Run "maximus fix" to create a blank contract file. +상위 3개 우선순위 +1. [오류] 경로 alias 대상이 존재하지 않음 + 파일: tsconfig.json + 다음: editor와 build 해석을 깨기 전에 오래된 alias를 수정하거나 제거하세요. +2. [경고] 구체 env 파일 ".env"이 .gitignore로 보호되지 않음 + 파일: .env + 다음: secret을 커밋하기 전에 구체 env 파일을 정확한 .gitignore 항목으로 보호하세요. +3. [경고] .env.example 계약 파일 누락 + 파일: .env + 다음: "maximus fix"를 실행해 빈 계약 파일을 생성하세요. -Findings -- [error] Path alias target does not exist - file: tsconfig.json - detail: @app/* points to src/*, but the resolved path was not found. - hint: Update or remove stale aliases before they break editor and build resolution. -- [warn] Concrete env file ".env" is not protected by .gitignore - file: .env - detail: Add ".env" to .gitignore. - hint: Protect concrete env files with an exact .gitignore entry before committing secrets. -- [warn] Missing .env.example contract - file: .env - detail: Runtime env files exist, but .env.example is missing. - hint: Run "maximus fix" to create a blank contract file. +발견 항목 +- [오류] 경로 alias 대상이 존재하지 않음 + 파일: tsconfig.json + 상세: @app/*는 src/*를 가리키지만 해석된 경로를 찾을 수 없습니다. + 힌트: editor와 build 해석을 깨기 전에 오래된 alias를 수정하거나 제거하세요. +- [경고] 구체 env 파일 ".env"이 .gitignore로 보호되지 않음 + 파일: .env + 상세: .gitignore에 ".env"를 추가하세요. + 힌트: secret을 커밋하기 전에 구체 env 파일을 정확한 .gitignore 항목으로 보호하세요. +- [경고] .env.example 계약 파일 누락 + 파일: .env + 상세: 실행 env 파일은 있지만 .env.example이 없습니다. + 힌트: "maximus fix"를 실행해 빈 계약 파일을 생성하세요. diff --git a/test/output-path-overlap.test.js b/test/output-path-overlap.test.js index 13fd82b..541d050 100644 --- a/test/output-path-overlap.test.js +++ b/test/output-path-overlap.test.js @@ -16,14 +16,14 @@ test("fixture-backed output path overlap audits stay wired through the CLI", asy const overlap = runAudit("./test/fixtures/output-path-overlap/outdir-src"); assert.equal(overlap.status, 1, overlap.stderr); - assert.ok(overlap.stdout.includes("Output directory overlaps the TypeScript source root")); - assert.ok(overlap.stdout.includes('outDir "src" overlaps source root "src".')); + assert.ok(overlap.stdout.includes("출력 디렉터리가 TypeScript source root와 겹침")); + assert.ok(overlap.stdout.includes('outDir "src"이 source root "src"와 겹칩니다.')); const safe = runAudit("./test/fixtures/output-path-overlap/outdir-dist"); assert.equal(safe.status, 0, safe.stderr); - assert.ok(safe.stdout.includes("No config drift detected.")); - assert.ok(!safe.stdout.includes("Output directory overlaps the TypeScript source root")); - assert.ok(!safe.stdout.includes("Output directory is nested inside the TypeScript source root")); + assert.ok(safe.stdout.includes("설정 차이가 감지되지 않았습니다.")); + assert.ok(!safe.stdout.includes("출력 디렉터리가 TypeScript source root와 겹침")); + assert.ok(!safe.stdout.includes("출력 디렉터리가 TypeScript source root 안에 있음")); }); test("CLI audit handles rootDir-dot, mixed inputs, and unmatched-include overlap boundaries", async (t) => { @@ -99,24 +99,24 @@ test("CLI audit handles rootDir-dot, mixed inputs, and unmatched-include overlap const rootdirDot = runAudit(path.join(rootDir, "rootdir-dot")); assert.equal(rootdirDot.status, 1, rootdirDot.stderr); - assert.ok(rootdirDot.stdout.includes("Output directory is nested inside the TypeScript source root")); - assert.ok(rootdirDot.stdout.includes('outDir "src" is nested inside source root ".".')); + assert.ok(rootdirDot.stdout.includes("출력 디렉터리가 TypeScript source root 안에 있음")); + assert.ok(rootdirDot.stdout.includes('outDir "src"이 source root "." 안에 있습니다.')); const outdirDot = runAudit(path.join(rootDir, "outdir-dot")); assert.equal(outdirDot.status, 1, outdirDot.stderr); - assert.ok(outdirDot.stdout.includes("Output directory contains TypeScript input files")); - assert.ok(outdirDot.stdout.includes('outDir "." contains TypeScript input "src/index.ts".')); + assert.ok(outdirDot.stdout.includes("출력 디렉터리에 TypeScript 입력 파일이 포함됨")); + assert.ok(outdirDot.stdout.includes('outDir "."에 TypeScript 입력 "src/index.ts"이 포함됩니다.')); const mixed = runAudit(path.join(rootDir, "mixed")); assert.equal(mixed.status, 1, mixed.stderr); - assert.ok(mixed.stdout.includes("Output directory overlaps the TypeScript source root")); - assert.ok(mixed.stdout.includes('outDir "src" overlaps source root "src".')); + assert.ok(mixed.stdout.includes("출력 디렉터리가 TypeScript source root와 겹침")); + assert.ok(mixed.stdout.includes('outDir "src"이 source root "src"와 겹칩니다.')); const safe = runAudit(path.join(rootDir, "safe")); assert.equal(safe.status, 1, safe.stderr); - assert.ok(!safe.stdout.includes("Output directory overlaps the TypeScript source root")); - assert.ok(!safe.stdout.includes("Output directory is nested inside the TypeScript source root")); - assert.ok(safe.stdout.includes("Include pattern does not match any files")); + assert.ok(!safe.stdout.includes("출력 디렉터리가 TypeScript source root와 겹침")); + assert.ok(!safe.stdout.includes("출력 디렉터리가 TypeScript source root 안에 있음")); + assert.ok(safe.stdout.includes("include pattern이 어떤 파일과도 일치하지 않음")); }); function runAudit(target) { diff --git a/test/packed-wrapper-fallback.test.js b/test/packed-wrapper-fallback.test.js index 79f96e1..d80ce49 100644 --- a/test/packed-wrapper-fallback.test.js +++ b/test/packed-wrapper-fallback.test.js @@ -39,7 +39,7 @@ test("packed install without the optional runtime blocks config files, Rust-only assert.equal(configResult.stdout.trim(), ""); assert.match( configResult.stderr, - /A Rust runtime is required when a Maximus config file is present/, + /Maximus config file이 있을 때는 Rust runtime이 필요합니다/, ); const rustOnlyResult = await runPackedWrapper(installRoot, [ @@ -52,7 +52,7 @@ test("packed install without the optional runtime blocks config files, Rust-only assert.equal(rustOnlyResult.stdout.trim(), ""); assert.match( rustOnlyResult.stderr, - /A Rust runtime is required for options not supported by the frozen JS compatibility path/, + /frozen JS compatibility path에서 지원하지 않는 옵션에는 Rust runtime이 필요합니다/, ); assert.match(rustOnlyResult.stderr, /--only/); @@ -68,7 +68,7 @@ test("packed install without the optional runtime blocks config files, Rust-only assert.equal(outputResult.stdout.trim(), ""); assert.match( outputResult.stderr, - /A Rust runtime is required for options not supported by the frozen JS compatibility path/, + /frozen JS compatibility path에서 지원하지 않는 옵션에는 Rust runtime이 필요합니다/, ); assert.match(outputResult.stderr, /--output/); await assert.rejects(access(outputPath)); diff --git a/test/tsconfig-patterns-cli.test.js b/test/tsconfig-patterns-cli.test.js index 30c71cb..e74324b 100644 --- a/test/tsconfig-patterns-cli.test.js +++ b/test/tsconfig-patterns-cli.test.js @@ -19,36 +19,36 @@ test("fixture-backed tsconfig pattern audits stay wired through the CLI", async name: "empty-include", expectedStatus: 1, expected: [ - "Include pattern does not match any files", - 'include pattern "src/missing/**/*.ts" matched 0 files', + "include pattern이 어떤 파일과도 일치하지 않음", + 'include pattern "src/missing/**/*.ts"은', ], - absent: ["Exclude pattern does not filter any included files"], + absent: ["exclude pattern이 포함 파일을 제외하지 않음"], }, { name: "empty-exclude", expectedStatus: 0, expected: [ - "Exclude pattern does not filter any included files", - 'exclude pattern "generated/**/*.ts" removed 0 files from 1 included file(s)', + "exclude pattern이 포함 파일을 제외하지 않음", + 'exclude pattern "generated/**/*.ts"은', ], - absent: ["Include pattern does not match any files"], + absent: ["include pattern이 어떤 파일과도 일치하지 않음"], }, { name: "exclude-only", expectedStatus: 0, expected: [ - "Exclude pattern does not filter any included files", - 'exclude pattern "missing/**/*.ts" removed 0 files from 1 included file(s)', + "exclude pattern이 포함 파일을 제외하지 않음", + 'exclude pattern "missing/**/*.ts"은', ], - absent: ["Include pattern does not match any files"], + absent: ["include pattern이 어떤 파일과도 일치하지 않음"], }, { name: "useful-patterns", expectedStatus: 0, - expected: ["No config drift detected."], + expected: ["설정 차이가 감지되지 않았습니다."], absent: [ - "Include pattern does not match any files", - "Exclude pattern does not filter any included files", + "include pattern이 어떤 파일과도 일치하지 않음", + "exclude pattern이 포함 파일을 제외하지 않음", ], }, ]; @@ -108,12 +108,12 @@ test("CLI audit respects allowJs when evaluating tsconfig include patterns", asy const withoutAllowJs = runAudit(path.join(rootDir, "js-without-allowjs")); assert.equal(withoutAllowJs.status, 1, withoutAllowJs.stderr); - assert.ok(withoutAllowJs.stdout.includes("Include pattern does not match any files")); + assert.ok(withoutAllowJs.stdout.includes("include pattern이 어떤 파일과도 일치하지 않음")); const withAllowJs = runAudit(path.join(rootDir, "js-with-allowjs")); assert.equal(withAllowJs.status, 0, withAllowJs.stderr); - assert.ok(withAllowJs.stdout.includes("No config drift detected.")); - assert.ok(!withAllowJs.stdout.includes("Include pattern does not match any files")); + assert.ok(withAllowJs.stdout.includes("설정 차이가 감지되지 않았습니다.")); + assert.ok(!withAllowJs.stdout.includes("include pattern이 어떤 파일과도 일치하지 않음")); }); test("CLI audit matches question-mark and zero-width star tsconfig include globs", async (t) => { @@ -154,8 +154,8 @@ test("CLI audit matches question-mark and zero-width star tsconfig include globs const result = runAudit(path.join(rootDir, target)); assert.equal(result.status, 0, result.stderr); - assert.ok(result.stdout.includes("No config drift detected.")); - assert.ok(!result.stdout.includes("Include pattern does not match any files")); + assert.ok(result.stdout.includes("설정 차이가 감지되지 않았습니다.")); + assert.ok(!result.stdout.includes("include pattern이 어떤 파일과도 일치하지 않음")); } }); @@ -227,18 +227,18 @@ test("CLI audit respects inherited allowJs and outDir when evaluating tsconfig p const inheritedAllowJs = runAudit(path.join(rootDir, "inherited-allowjs")); assert.equal(inheritedAllowJs.status, 0, inheritedAllowJs.stderr); - assert.ok(inheritedAllowJs.stdout.includes("No config drift detected.")); - assert.ok(!inheritedAllowJs.stdout.includes("Include pattern does not match any files")); + assert.ok(inheritedAllowJs.stdout.includes("설정 차이가 감지되지 않았습니다.")); + assert.ok(!inheritedAllowJs.stdout.includes("include pattern이 어떤 파일과도 일치하지 않음")); const inheritedOutDir = runAudit(path.join(rootDir, "inherited-outdir")); assert.equal(inheritedOutDir.status, 0, inheritedOutDir.stderr); - assert.ok(inheritedOutDir.stdout.includes("Exclude pattern does not filter any included files")); - assert.ok(inheritedOutDir.stdout.includes('exclude pattern "dist/**/*.d.ts" removed 0 files from 1 included file(s)')); + assert.ok(inheritedOutDir.stdout.includes("exclude pattern이 포함 파일을 제외하지 않음")); + assert.ok(inheritedOutDir.stdout.includes('exclude pattern "dist/**/*.d.ts"은')); const outdirInclude = runAudit(path.join(rootDir, "outdir-include")); assert.equal(outdirInclude.status, 1, outdirInclude.stderr); - assert.ok(outdirInclude.stdout.includes("Include pattern does not match any files")); - assert.ok(outdirInclude.stdout.includes('include pattern "dist" matched 0 files')); + assert.ok(outdirInclude.stdout.includes("include pattern이 어떤 파일과도 일치하지 않음")); + assert.ok(outdirInclude.stdout.includes('include pattern "dist"은')); }); test("CLI audit skips explicit empty inputs and default node_modules when evaluating tsconfig patterns", async (t) => { @@ -320,20 +320,20 @@ test("CLI audit skips explicit empty inputs and default node_modules when evalua const result = runAudit(path.join(rootDir, target)); assert.equal(result.status, 0, result.stderr); - assert.ok(result.stdout.includes("No config drift detected.")); - assert.ok(!result.stdout.includes("Include pattern does not match any files")); - assert.ok(!result.stdout.includes("Exclude pattern does not filter any included files")); + assert.ok(result.stdout.includes("설정 차이가 감지되지 않았습니다.")); + assert.ok(!result.stdout.includes("include pattern이 어떤 파일과도 일치하지 않음")); + assert.ok(!result.stdout.includes("exclude pattern이 포함 파일을 제외하지 않음")); } const filesWithExclude = runAudit(path.join(rootDir, "files-with-exclude")); assert.equal(filesWithExclude.status, 0, filesWithExclude.stderr); - assert.ok(filesWithExclude.stdout.includes("Exclude pattern does not filter any included files")); - assert.ok(filesWithExclude.stdout.includes('exclude pattern "src/**/*.d.ts" removed 0 files from 1 included file(s)')); + assert.ok(filesWithExclude.stdout.includes("exclude pattern이 포함 파일을 제외하지 않음")); + assert.ok(filesWithExclude.stdout.includes('exclude pattern "src/**/*.d.ts"은')); const duplicateExcludes = runAudit(path.join(rootDir, "duplicate-excludes")); assert.equal(duplicateExcludes.status, 0, duplicateExcludes.stderr); - assert.ok(duplicateExcludes.stdout.includes("Exclude pattern does not filter any included files")); - assert.ok(duplicateExcludes.stdout.includes('exclude pattern "src/generated/**/*.d.ts" removed 0 files from 0 included file(s)')); + assert.ok(duplicateExcludes.stdout.includes("exclude pattern이 포함 파일을 제외하지 않음")); + assert.ok(duplicateExcludes.stdout.includes('exclude pattern "src/generated/**/*.d.ts"은')); }); test("CLI audit inherits top-level pattern fields and reports invalid pattern entries", async (t) => { @@ -454,38 +454,54 @@ test("CLI audit inherits top-level pattern fields and reports invalid pattern en const inheritedInclude = runAudit(path.join(rootDir, "app-inherited-include")); assert.equal(inheritedInclude.status, 1, inheritedInclude.stderr); - assert.ok(inheritedInclude.stdout.includes("Include pattern does not match any files")); - assert.ok(inheritedInclude.stdout.includes('include pattern "./src/**/*.ts" matched 0 files')); + assert.ok(inheritedInclude.stdout.includes("include pattern이 어떤 파일과도 일치하지 않음")); + assert.ok(inheritedInclude.stdout.includes('include pattern "./src/**/*.ts"은')); const missingExtends = runAudit(path.join(rootDir, "app-missing-extends")); assert.equal(missingExtends.status, 1, missingExtends.stderr); - assert.ok(missingExtends.stdout.includes("Inherited tsconfig could not be found")); + assert.ok(missingExtends.stdout.includes("상속된 tsconfig를 찾을 수 없음")); assert.ok(missingExtends.stdout.includes("shared-missing/tsconfig.base.json")); const inheritedFilesEmpty = runAudit(path.join(rootDir, "app-inherited-files-empty")); assert.equal(inheritedFilesEmpty.status, 0, inheritedFilesEmpty.stderr); - assert.ok(inheritedFilesEmpty.stdout.includes("No config drift detected.")); - assert.ok(!inheritedFilesEmpty.stdout.includes("Exclude pattern does not filter any included files")); + assert.ok(inheritedFilesEmpty.stdout.includes("설정 차이가 감지되지 않았습니다.")); + assert.ok(!inheritedFilesEmpty.stdout.includes("exclude pattern이 포함 파일을 제외하지 않음")); const invalidPatternEntry = runAudit(path.join(rootDir, "invalid-pattern-entry")); assert.equal(invalidPatternEntry.status, 1, invalidPatternEntry.stderr); - assert.ok(invalidPatternEntry.stdout.includes('"include" contains a non-string pattern')); - assert.ok(invalidPatternEntry.stdout.includes("declares include[0], but TypeScript expects string patterns.")); + assert.ok(invalidPatternEntry.stdout.includes('"include"에 string이 아닌 pattern이 포함됨')); + assert.ok( + invalidPatternEntry.stdout.includes( + "include[0]를 선언하지만 TypeScript는 string pattern을 기대합니다.", + ), + ); const invalidFilesEntry = runAudit(path.join(rootDir, "invalid-files-entry")); assert.equal(invalidFilesEntry.status, 1, invalidFilesEntry.stderr); - assert.ok(invalidFilesEntry.stdout.includes('"files" entries must point to explicit files')); - assert.ok(invalidFilesEntry.stdout.includes("declares files[0] as src/*.ts")); + assert.ok(invalidFilesEntry.stdout.includes('"files" 항목은 명시적인 파일을 가리켜야 함')); + assert.ok( + invalidFilesEntry.stdout.includes( + "files[0] as src/*.ts를 선언하지만 TypeScript files 항목에는 glob wildcard를 사용할 수 없습니다.", + ), + ); const invalidFilesDirectory = runAudit(path.join(rootDir, "invalid-files-directory")); assert.equal(invalidFilesDirectory.status, 1, invalidFilesDirectory.stderr); - assert.ok(invalidFilesDirectory.stdout.includes('"files" entries must point to files')); - assert.ok(invalidFilesDirectory.stdout.includes("declares files[0] as src, but that path resolves to a directory.")); + assert.ok(invalidFilesDirectory.stdout.includes('"files" 항목은 파일을 가리켜야 함')); + assert.ok( + invalidFilesDirectory.stdout.includes( + "files[0] as src를 선언하지만 해당 path는 directory로 해석됩니다.", + ), + ); const invalidFilesMissing = runAudit(path.join(rootDir, "invalid-files-missing")); assert.equal(invalidFilesMissing.status, 1, invalidFilesMissing.stderr); - assert.ok(invalidFilesMissing.stdout.includes('"files" entries must point to existing files')); - assert.ok(invalidFilesMissing.stdout.includes("declares files[0] as src/missing.ts")); + assert.ok(invalidFilesMissing.stdout.includes('"files" 항목은 존재하는 파일을 가리켜야 함')); + assert.ok( + invalidFilesMissing.stdout.includes( + "files[0] as src/missing.ts를 선언하지만 해당 path는 존재하는 파일로 해석되지 않습니다.", + ), + ); }); test("CLI audit treats missing Next generated types include as info-only", async (t) => { @@ -524,27 +540,27 @@ test("CLI audit treats missing Next generated types include as info-only", async const nextResult = runAudit(path.join(rootDir, "next-app")); assert.equal(nextResult.status, 0, nextResult.stderr); - assert.ok(nextResult.stdout.includes("Include pattern does not match any files")); - assert.ok(nextResult.stdout.includes('include pattern ".next/types/**/*.ts" matched 0 files')); + assert.ok(nextResult.stdout.includes("include pattern이 어떤 파일과도 일치하지 않음")); + assert.ok(nextResult.stdout.includes('include pattern ".next/types/**/*.ts"은')); assert.ok( - nextResult.stdout.includes("Next.js generates .next/types during development or build"), + nextResult.stdout.includes("Next.js는 개발 또는 build 중 .next/types를 생성하므로"), ); assert.ok( !nextResult.stdout.includes( - "Fix or remove empty include patterns before TypeScript silently skips expected inputs.", + "TypeScript가 예상 입력을 조용히 건너뛰기 전에 빈 include pattern을 수정하거나 제거하세요.", ), ); const plainResult = runAudit(path.join(rootDir, "plain-app")); assert.equal(plainResult.status, 1, plainResult.stderr); - assert.ok(plainResult.stdout.includes("Include pattern does not match any files")); + assert.ok(plainResult.stdout.includes("include pattern이 어떤 파일과도 일치하지 않음")); assert.ok( plainResult.stdout.includes( - "Fix or remove empty include patterns before TypeScript silently skips expected inputs.", + "TypeScript가 예상 입력을 조용히 건너뛰기 전에 빈 include pattern을 수정하거나 제거하세요.", ), ); assert.ok( - !plainResult.stdout.includes("Next.js generates .next/types during development or build"), + !plainResult.stdout.includes("Next.js는 개발 또는 build 중 .next/types를 생성하므로"), ); }); diff --git a/test/wrapper-runtime.test.js b/test/wrapper-runtime.test.js index b0b0ac4..b38f7f5 100644 --- a/test/wrapper-runtime.test.js +++ b/test/wrapper-runtime.test.js @@ -299,7 +299,7 @@ test("wrapper blocks the frozen JS fallback when Rust-only flags are requested", assert.equal(result.code, 1); assert.equal(result.stdout.trim(), ""); - assert.match(result.stderr, /A Rust runtime is required/); + assert.match(result.stderr, /Rust runtime이 필요합니다/); assert.match(result.stderr, /--only/); const outputPath = path.join(rootDir, "report.json"); @@ -313,7 +313,7 @@ test("wrapper blocks the frozen JS fallback when Rust-only flags are requested", assert.equal(outputResult.code, 1); assert.equal(outputResult.stdout.trim(), ""); - assert.match(outputResult.stderr, /A Rust runtime is required/); + assert.match(outputResult.stderr, /Rust runtime이 필요합니다/); assert.match(outputResult.stderr, /--output/); await assert.rejects(access(outputPath)); }); @@ -362,7 +362,7 @@ test("wrapper blocks output format flags on the frozen JS fallback", async (t) = assert.equal(result.code, 1); assert.equal(result.stdout.trim(), ""); - assert.match(result.stderr, /A Rust runtime is required/); + assert.match(result.stderr, /Rust runtime이 필요합니다/); assert.match(result.stderr, /--format/); const leadingResult = await runWrapper(path.join(rootDir, "bootstrap.mjs"), rootDir, [ @@ -374,7 +374,7 @@ test("wrapper blocks output format flags on the frozen JS fallback", async (t) = assert.equal(leadingResult.code, 1); assert.equal(leadingResult.stdout.trim(), ""); - assert.match(leadingResult.stderr, /A Rust runtime is required/); + assert.match(leadingResult.stderr, /Rust runtime이 필요합니다/); assert.match(leadingResult.stderr, /--format/); }); @@ -470,7 +470,7 @@ test("wrapper blocks the frozen JS fallback when a Maximus config file is presen assert.equal(result.code, 1); assert.equal(result.stdout.trim(), ""); - assert.match(result.stderr, /A Rust runtime is required when a Maximus config file is present/); + assert.match(result.stderr, /Maximus config file이 있을 때는 Rust runtime이 필요합니다/); assert.match(result.stderr, /maximus\.config\.json/); }); @@ -523,7 +523,7 @@ test("wrapper blocks the frozen JS fallback for doctor target config files", asy assert.equal(result.code, 1); assert.equal(result.stdout.trim(), ""); - assert.match(result.stderr, /A Rust runtime is required when a Maximus config file is present/); + assert.match(result.stderr, /Maximus config file이 있을 때는 Rust runtime이 필요합니다/); assert.match(result.stderr, /maximus\.config\.json/); }); @@ -584,7 +584,7 @@ test("wrapper blocks the frozen JS fallback when config is found through a symli assert.equal(result.code, 1); assert.equal(result.stdout.trim(), ""); - assert.match(result.stderr, /A Rust runtime is required when a Maximus config file is present/); + assert.match(result.stderr, /Maximus config file이 있을 때는 Rust runtime이 필요합니다/); assert.match(result.stderr, /maximus\.config\.json/); }); @@ -652,7 +652,7 @@ test("wrapper prefers the real project config over a lexical mount config", asyn assert.equal(result.code, 1); assert.equal(result.stdout.trim(), ""); - assert.match(result.stderr, /A Rust runtime is required when a Maximus config file is present/); + assert.match(result.stderr, /Maximus config file이 있을 때는 Rust runtime이 필요합니다/); assert.match(result.stderr, /\/real\/maximus\.config\.json/); assert.doesNotMatch(result.stderr, /\/mount\/maximus\.config\.json/); }); @@ -746,11 +746,11 @@ test("wrapper prints help before checking config files on the frozen JS fallback const result = await runWrapper(path.join(rootDir, "bootstrap.mjs"), rootDir, ["--help"]); assert.equal(result.code, 0); - assert.match(result.stdout, /Usage/); + assert.match(result.stdout, /사용법/); assert.match(result.stdout, /maximus fix \[path\] --dry-run \[--json\]/); assert.doesNotMatch(result.stdout, /--only /); - assert.match(result.stdout, /--format.*require the Rust runtime/); - assert.match(result.stdout, /require the Rust runtime/); + assert.match(result.stdout, /--format.*Rust runtime이 필요/); + assert.match(result.stdout, /Rust runtime이 필요/); assert.equal(result.stderr.trim(), ""); }); @@ -796,7 +796,7 @@ test("wrapper blocks the frozen JS fallback for fix without dry-run", async (t) assert.equal(result.code, 1); assert.equal(result.stdout.trim(), ""); - assert.match(result.stderr, /A Rust runtime is required/); + assert.match(result.stderr, /Rust runtime이 필요합니다/); assert.match(result.stderr, /fix \(without --dry-run\)/); });