From fc705f57cca7324673988984a5b77b6c983ba975 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Jul 2026 01:54:58 +0000 Subject: [PATCH] =?UTF-8?q?=EC=98=88=EC=99=B8=20=EB=A9=94=EC=8B=9C?= =?UTF-8?q?=EC=A7=80=20=EC=98=81=EC=96=B4=20=EC=A7=80=EC=9B=90=20=ED=99=95?= =?UTF-8?q?=EB=8C=80=203/4=20(v1.20.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 콘텐츠 관리(Modrinth 설치·업데이트·제거·의존성)와 Java 런타임 준비의 오류 문구 105곳을 Localized로 변환했습니다. 문자열이 이어붙는 형태와 ArgumentException(message, paramName)처럼 두 번째 인자가 문자열인 형태도 함께 처리했습니다. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01A4KU6BZL5oWTjH2eDB18Yu --- CHANGELOG.md | 11 ++++ ContentManagementServices.cs | 120 +++++++++++++++++------------------ README.md | 8 +-- RuntimeCompatibility.cs | 90 +++++++++++++------------- docs/ai/SYNC_STATE.md | 8 +++ version.json | 4 +- 6 files changed, 130 insertions(+), 111 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37b3982..efbbf54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ Product versions follow [Semantic Versioning](https://semver.org/), while `26.2.45.xx` is a separate internal build number. +## [1.20.0] - 2026-07-27 + +### Korean + +- **오류 문구 영어 지원 확대 (3/4)**: 콘텐츠 관리(Modrinth 설치·업데이트·제거·의존성)와 Java 런타임 준비의 오류 문구 105개를 한국어·영어로 제공합니다. +- 예외 타입은 그대로 두고 영어 문구만 함께 담는 방식을 이어갑니다. 기존 예외 처리 동작에는 영향이 없습니다. + +### English + +- **More error messages available in English (3 of 4)**: 105 messages across content management (Modrinth install, update, removal, dependencies) and Java runtime preparation are now available in both Korean and English. +- This continues carrying the English wording alongside the unchanged exception type, so existing exception handling behaves exactly as before. ## [1.19.0] - 2026-07-27 ### Korean diff --git a/ContentManagementServices.cs b/ContentManagementServices.cs index 358f17e..eb27186 100644 --- a/ContentManagementServices.cs +++ b/ContentManagementServices.cs @@ -83,7 +83,7 @@ private static ContentManifestModel LoadContentManifest(string serverDirectory) if (!File.Exists(path)) return manifest; FileInfo file = new FileInfo(path); if (file.Length <= 0 || file.Length > MaximumContentManifestBytes) - throw new InvalidDataException("콘텐츠 manifest 크기가 올바르지 않습니다."); + throw Localized(new InvalidDataException("콘텐츠 manifest 크기가 올바르지 않습니다."), "The content manifest size is invalid."); Dictionary root; try { @@ -91,27 +91,27 @@ private static ContentManifestModel LoadContentManifest(string serverDirectory) } catch (Exception exception) { - throw new InvalidDataException("콘텐츠 manifest가 손상되었습니다. 파일을 덮어쓰지 않았습니다.", exception); + throw Localized(new InvalidDataException("콘텐츠 manifest가 손상되었습니다. 파일을 덮어쓰지 않았습니다.", exception), "The content manifest is damaged. The file was not overwritten."); } if (root == null || GetJsonInt(root, "schemaVersion") != ContentManifestSchemaVersion) - throw new InvalidDataException("지원하지 않는 콘텐츠 manifest 형식입니다."); + throw Localized(new InvalidDataException("지원하지 않는 콘텐츠 manifest 형식입니다."), "Unsupported content manifest format."); manifest.UpdatedUtc = GetJsonString(root, "updatedUtc"); object[] items = root.ContainsKey("items") ? root["items"] as object[] : null; - if (items == null) throw new InvalidDataException("콘텐츠 manifest 항목 목록이 없습니다."); - if (items.Length > 10000) throw new InvalidDataException("콘텐츠 manifest 항목이 지나치게 많습니다."); + if (items == null) throw Localized(new InvalidDataException("콘텐츠 manifest 항목 목록이 없습니다."), "The content manifest has no entry list."); + if (items.Length > 10000) throw Localized(new InvalidDataException("콘텐츠 manifest 항목이 지나치게 많습니다."), "The content manifest has too many entries."); HashSet ids = new HashSet(StringComparer.OrdinalIgnoreCase); HashSet paths = new HashSet(StringComparer.OrdinalIgnoreCase); HashSet managedProjects = new HashSet(StringComparer.OrdinalIgnoreCase); for (int i = 0; i < items.Length; i++) { Dictionary value = items[i] as Dictionary; - if (value == null) throw new InvalidDataException("콘텐츠 manifest 항목 형식이 잘못되었습니다."); + if (value == null) throw Localized(new InvalidDataException("콘텐츠 manifest 항목 형식이 잘못되었습니다."), "A content manifest entry has an invalid format."); ContentManifestEntry entry = DeserializeContentEntry(value); ValidateContentManifestEntry(serverDirectory, entry); - if (!ids.Add(entry.Id)) throw new InvalidDataException("콘텐츠 manifest에 중복 ID가 있습니다: " + entry.Id); + if (!ids.Add(entry.Id)) throw Localized(new InvalidDataException("콘텐츠 manifest에 중복 ID가 있습니다: " + entry.Id), "The content manifest contains a duplicate ID: " + entry.Id); string activePath = entry.Active ? entry.RelativePath : entry.DisabledRelativePath; - if (!paths.Add(activePath)) throw new InvalidDataException("콘텐츠 manifest에 중복 파일 경로가 있습니다: " + activePath); - if (IsManagedModrinthEntry(entry) && !managedProjects.Add(GetContentProjectKey(entry.Kind, entry.WorldName, entry.ProjectId))) throw new InvalidDataException("콘텐츠 manifest에 같은 Modrinth 프로젝트가 중복되어 있습니다: " + entry.ProjectId); + if (!paths.Add(activePath)) throw Localized(new InvalidDataException("콘텐츠 manifest에 중복 파일 경로가 있습니다: " + activePath), "The content manifest contains a duplicate file path: " + activePath); + if (IsManagedModrinthEntry(entry) && !managedProjects.Add(GetContentProjectKey(entry.Kind, entry.WorldName, entry.ProjectId))) throw Localized(new InvalidDataException("콘텐츠 manifest에 같은 Modrinth 프로젝트가 중복되어 있습니다: " + entry.ProjectId), "The content manifest contains a duplicate Modrinth project: " + entry.ProjectId); manifest.Items.Add(entry); } ValidateContentManifestDependencyGraph(manifest); @@ -132,16 +132,16 @@ private static void SaveContentManifest(string serverDirectory, ContentManifestM { ContentManifestEntry entry = manifest.Items[i]; ValidateContentManifestEntry(serverDirectory, entry); - if (!ids.Add(entry.Id)) throw new InvalidDataException("콘텐츠 manifest에 중복 ID가 있습니다: " + entry.Id); + if (!ids.Add(entry.Id)) throw Localized(new InvalidDataException("콘텐츠 manifest에 중복 ID가 있습니다: " + entry.Id), "The content manifest contains a duplicate ID: " + entry.Id); string activePath = entry.Active ? entry.RelativePath : entry.DisabledRelativePath; - if (!paths.Add(activePath)) throw new InvalidDataException("콘텐츠 manifest에 중복 파일 경로가 있습니다: " + activePath); - if (IsManagedModrinthEntry(entry) && !managedProjects.Add(GetContentProjectKey(entry.Kind, entry.WorldName, entry.ProjectId))) throw new InvalidDataException("콘텐츠 manifest에 같은 Modrinth 프로젝트가 중복되어 있습니다: " + entry.ProjectId); + if (!paths.Add(activePath)) throw Localized(new InvalidDataException("콘텐츠 manifest에 중복 파일 경로가 있습니다: " + activePath), "The content manifest contains a duplicate file path: " + activePath); + if (IsManagedModrinthEntry(entry) && !managedProjects.Add(GetContentProjectKey(entry.Kind, entry.WorldName, entry.ProjectId))) throw Localized(new InvalidDataException("콘텐츠 manifest에 같은 Modrinth 프로젝트가 중복되어 있습니다: " + entry.ProjectId), "The content manifest contains a duplicate Modrinth project: " + entry.ProjectId); items.Add(SerializeContentEntry(entry)); } ValidateContentManifestDependencyGraph(manifest); root["items"] = items.ToArray(); string json = new JavaScriptSerializer { MaxJsonLength = MaximumContentManifestBytes }.Serialize(root); - if (Encoding.UTF8.GetByteCount(json) > MaximumContentManifestBytes) throw new InvalidDataException("콘텐츠 manifest가 안전 크기 제한을 초과했습니다."); + if (Encoding.UTF8.GetByteCount(json) > MaximumContentManifestBytes) throw Localized(new InvalidDataException("콘텐츠 manifest가 안전 크기 제한을 초과했습니다."), "The content manifest exceeds the safe size limit."); string directory = GetContentMetadataDirectory(serverDirectory); Directory.CreateDirectory(directory); string path = GetContentManifestPath(serverDirectory); @@ -204,19 +204,19 @@ private static Dictionary SerializeContentEntry(ContentManifestE private static void ValidateContentManifestEntry(string serverDirectory, ContentManifestEntry entry) { if (entry == null || string.IsNullOrWhiteSpace(entry.Id) || entry.Id.Length > 128 || !IsContentKind(entry.Kind)) - throw new InvalidDataException("콘텐츠 manifest ID 또는 종류가 올바르지 않습니다."); + throw Localized(new InvalidDataException("콘텐츠 manifest ID 또는 종류가 올바르지 않습니다."), "The content manifest ID or kind is invalid."); if (string.IsNullOrWhiteSpace(entry.Source) || entry.Source.Length > 32 || string.IsNullOrWhiteSpace(entry.FileName) || Path.GetFileName(entry.FileName) != entry.FileName) - throw new InvalidDataException("콘텐츠 manifest 출처 또는 파일명이 올바르지 않습니다."); + throw Localized(new InvalidDataException("콘텐츠 manifest 출처 또는 파일명이 올바르지 않습니다."), "The content manifest source or file name is invalid."); entry.RelativePath = NormalizeContentRelativePath(entry.RelativePath); entry.DisabledRelativePath = NormalizeContentRelativePath(entry.DisabledRelativePath); GetSafeContentPath(serverDirectory, entry.RelativePath); GetSafeContentPath(serverDirectory, entry.DisabledRelativePath); - if (entry.Dependencies == null || entry.Dependencies.Length > 256) throw new InvalidDataException("콘텐츠 의존성 목록이 올바르지 않습니다."); + if (entry.Dependencies == null || entry.Dependencies.Length > 256) throw Localized(new InvalidDataException("콘텐츠 의존성 목록이 올바르지 않습니다."), "The content dependency list is invalid."); HashSet dependencies = new HashSet(StringComparer.OrdinalIgnoreCase); for (int i = 0; i < entry.Dependencies.Length; i++) - if (string.IsNullOrWhiteSpace(entry.Dependencies[i]) || entry.Dependencies[i].Length > 128 || !dependencies.Add(entry.Dependencies[i])) throw new InvalidDataException("콘텐츠 의존성 식별자가 올바르지 않습니다."); - if (!string.IsNullOrEmpty(entry.Sha512) && !IsHexString(entry.Sha512, 128)) throw new InvalidDataException("콘텐츠 SHA-512 값이 올바르지 않습니다."); - if (!string.IsNullOrEmpty(entry.Sha1) && !IsHexString(entry.Sha1, 40)) throw new InvalidDataException("콘텐츠 SHA-1 값이 올바르지 않습니다."); + if (string.IsNullOrWhiteSpace(entry.Dependencies[i]) || entry.Dependencies[i].Length > 128 || !dependencies.Add(entry.Dependencies[i])) throw Localized(new InvalidDataException("콘텐츠 의존성 식별자가 올바르지 않습니다."), "A content dependency identifier is invalid."); + if (!string.IsNullOrEmpty(entry.Sha512) && !IsHexString(entry.Sha512, 128)) throw Localized(new InvalidDataException("콘텐츠 SHA-512 값이 올바르지 않습니다."), "The content SHA-512 value is invalid."); + if (!string.IsNullOrEmpty(entry.Sha1) && !IsHexString(entry.Sha1, 40)) throw Localized(new InvalidDataException("콘텐츠 SHA-1 값이 올바르지 않습니다."), "The content SHA-1 value is invalid."); } private static bool IsContentKind(string value) @@ -253,13 +253,13 @@ private static void ValidateContentManifestDependencyGraph(ContentManifestModel private static string NormalizeContentRelativePath(string value) { - if (string.IsNullOrWhiteSpace(value) || value.Length > 1024) throw new InvalidDataException("콘텐츠 상대 경로가 올바르지 않습니다."); + if (string.IsNullOrWhiteSpace(value) || value.Length > 1024) throw Localized(new InvalidDataException("콘텐츠 상대 경로가 올바르지 않습니다."), "The relative content path is invalid."); string normalized = value.Replace('\\', '/').Trim('/'); if (normalized.Length == 0 || Path.IsPathRooted(normalized) || normalized.IndexOf('\0') >= 0) - throw new InvalidDataException("콘텐츠 상대 경로가 올바르지 않습니다."); + throw Localized(new InvalidDataException("콘텐츠 상대 경로가 올바르지 않습니다."), "The relative content path is invalid."); string[] parts = normalized.Split('/'); for (int i = 0; i < parts.Length; i++) - if (parts[i].Length == 0 || parts[i] == "." || parts[i] == "..") throw new InvalidDataException("콘텐츠 상대 경로가 서버 밖을 가리킵니다."); + if (parts[i].Length == 0 || parts[i] == "." || parts[i] == "..") throw Localized(new InvalidDataException("콘텐츠 상대 경로가 서버 밖을 가리킵니다."), "The relative content path points outside the server."); return normalized; } @@ -267,7 +267,7 @@ private static string GetSafeContentPath(string serverDirectory, string relative { string root = Path.GetFullPath(serverDirectory).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar; string path = Path.GetFullPath(Path.Combine(root, NormalizeContentRelativePath(relativePath).Replace('/', Path.DirectorySeparatorChar))); - if (!path.StartsWith(root, StringComparison.OrdinalIgnoreCase)) throw new InvalidDataException("콘텐츠 경로가 서버 폴더 밖을 가리킵니다."); + if (!path.StartsWith(root, StringComparison.OrdinalIgnoreCase)) throw Localized(new InvalidDataException("콘텐츠 경로가 서버 폴더 밖을 가리킵니다."), "The content path points outside the server folder."); return path; } @@ -400,8 +400,8 @@ private static void SetContentEnabled(string serverDirectory, ContentManifestEnt if (entry.Active == enabled) return; string source = GetSafeContentPath(serverDirectory, entry.Active ? entry.RelativePath : entry.DisabledRelativePath); string destination = GetSafeContentPath(serverDirectory, enabled ? entry.RelativePath : entry.DisabledRelativePath); - if (!File.Exists(source) && !Directory.Exists(source)) throw new FileNotFoundException("콘텐츠 파일을 찾지 못했습니다.", source); - if (File.Exists(destination) || Directory.Exists(destination)) throw new IOException("활성화 상태를 바꿀 대상 경로가 이미 존재합니다."); + if (!File.Exists(source) && !Directory.Exists(source)) throw Localized(new FileNotFoundException("콘텐츠 파일을 찾지 못했습니다.", source), "The content file was not found."); + if (File.Exists(destination) || Directory.Exists(destination)) throw Localized(new IOException("활성화 상태를 바꿀 대상 경로가 이미 존재합니다."), "The destination path for the enable/disable change already exists."); Directory.CreateDirectory(Path.GetDirectoryName(destination)); if (File.Exists(source)) File.Move(source, destination); else Directory.Move(source, destination); entry.Active = enabled; @@ -424,11 +424,11 @@ private static void RemoveContentItem(string serverDirectory, ContentManifestEnt { ContentManifestEntry candidate = manifest.Items[i]; if (ReferenceEquals(candidate, entry) || !string.Equals(candidate.Kind, entry.Kind, StringComparison.OrdinalIgnoreCase) || !string.Equals(candidate.WorldName ?? string.Empty, entry.WorldName ?? string.Empty, StringComparison.OrdinalIgnoreCase)) continue; - if (Array.Exists(candidate.Dependencies ?? new string[0], delegate(string dependency) { return string.Equals(dependency, entry.ProjectId, StringComparison.OrdinalIgnoreCase); })) throw new InvalidOperationException("다른 설치 콘텐츠가 이 필수 의존성을 사용하고 있어 제거할 수 없습니다: " + candidate.FileName); + if (Array.Exists(candidate.Dependencies ?? new string[0], delegate(string dependency) { return string.Equals(dependency, entry.ProjectId, StringComparison.OrdinalIgnoreCase); })) throw Localized(new InvalidOperationException("다른 설치 콘텐츠가 이 필수 의존성을 사용하고 있어 제거할 수 없습니다: " + candidate.FileName), "This required dependency cannot be removed because other installed content uses it: " + candidate.FileName); } } string source = GetSafeContentPath(serverDirectory, entry.Active ? entry.RelativePath : entry.DisabledRelativePath); - if (!File.Exists(source) && !Directory.Exists(source)) throw new FileNotFoundException("제거할 콘텐츠 파일을 찾지 못했습니다.", source); + if (!File.Exists(source) && !Directory.Exists(source)) throw Localized(new FileNotFoundException("제거할 콘텐츠 파일을 찾지 못했습니다.", source), "The content file to remove was not found."); string trashRelative = ".mineharbor/content-trash/" + DateTime.UtcNow.ToString("yyyyMMdd-HHmmss-fff", CultureInfo.InvariantCulture) + "/" + entry.Kind + "/" + entry.FileName; string trash = GetSafeContentPath(serverDirectory, trashRelative); Directory.CreateDirectory(Path.GetDirectoryName(trash)); @@ -495,7 +495,7 @@ private static ModrinthFileInfo GetCompatibleModrinthFileForKind(string projectI string gameJson = new JavaScriptSerializer().Serialize(new string[] { options.MinecraftVersion }); string url = "https://api.modrinth.com/v2/project/" + Uri.EscapeDataString(projectId) + "/version?include_changelog=false&loaders=" + Uri.EscapeDataString(loaderJson) + "&game_versions=" + Uri.EscapeDataString(gameJson); object[] versions = new JavaScriptSerializer().DeserializeObject(DownloadModrinthText(url)) as object[]; - if (versions == null || versions.Length == 0) throw new InvalidDataException("선택한 Minecraft 버전과 로더에 맞는 콘텐츠 파일을 찾지 못했습니다."); + if (versions == null || versions.Length == 0) throw Localized(new InvalidDataException("선택한 Minecraft 버전과 로더에 맞는 콘텐츠 파일을 찾지 못했습니다."), "No content file was found for the selected Minecraft version and loader."); Dictionary selected = null; for (int pass = 0; pass < 2 && selected == null; pass++) { @@ -508,7 +508,7 @@ private static ModrinthFileInfo GetCompatibleModrinthFileForKind(string projectI break; } } - if (selected == null) throw new InvalidDataException("설치 가능한 콘텐츠 릴리스를 찾지 못했습니다."); + if (selected == null) throw Localized(new InvalidDataException("설치 가능한 콘텐츠 릴리스를 찾지 못했습니다."), "No installable content release was found."); object[] files = selected.ContainsKey("files") ? selected["files"] as object[] : null; Dictionary selectedFile = null; if (files != null) @@ -524,7 +524,7 @@ private static ModrinthFileInfo GetCompatibleModrinthFileForKind(string projectI if (GetJsonBoolean(candidate, "primary")) { selectedFile = candidate; break; } } } - if (selectedFile == null) throw new InvalidDataException("콘텐츠 다운로드 파일을 찾지 못했습니다."); + if (selectedFile == null) throw Localized(new InvalidDataException("콘텐츠 다운로드 파일을 찾지 못했습니다."), "The downloaded content file was not found."); ModrinthFileInfo info = new ModrinthFileInfo(); info.ProjectId = projectId; info.VersionId = GetJsonString(selected, "id"); @@ -543,20 +543,20 @@ private static ModrinthFileInfo GetCompatibleModrinthFileForKind(string projectI private static void ValidateManagedModrinthFile(ModrinthFileInfo info, string kind) { - if (info == null || string.IsNullOrWhiteSpace(info.FileName) || Path.GetFileName(info.FileName) != info.FileName) throw new InvalidDataException("Modrinth가 안전한 파일명을 제공하지 않았습니다."); + if (info == null || string.IsNullOrWhiteSpace(info.FileName) || Path.GetFileName(info.FileName) != info.FileName) throw Localized(new InvalidDataException("Modrinth가 안전한 파일명을 제공하지 않았습니다."), "Modrinth did not provide a safe file name."); if (string.Equals(kind, "datapack", StringComparison.OrdinalIgnoreCase)) { - if (!info.FileName.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)) throw new InvalidDataException("데이터팩은 ZIP 파일만 설치할 수 있습니다."); + if (!info.FileName.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)) throw Localized(new InvalidDataException("데이터팩은 ZIP 파일만 설치할 수 있습니다."), "Only ZIP files can be installed as data packs."); } - else if (!info.FileName.EndsWith(".jar", StringComparison.OrdinalIgnoreCase)) throw new InvalidDataException("플러그인과 모드는 JAR 파일만 설치할 수 있습니다."); + else if (!info.FileName.EndsWith(".jar", StringComparison.OrdinalIgnoreCase)) throw Localized(new InvalidDataException("플러그인과 모드는 JAR 파일만 설치할 수 있습니다."), "Only JAR files can be installed as plugins or mods."); Uri uri; - if (!Uri.TryCreate(info.Url, UriKind.Absolute, out uri) || uri.Scheme != Uri.UriSchemeHttps || !uri.Host.Equals("cdn.modrinth.com", StringComparison.OrdinalIgnoreCase) || !string.IsNullOrEmpty(uri.UserInfo)) throw new InvalidDataException("Modrinth CDN 다운로드 주소를 검증하지 못했습니다."); - if (info.Size <= 0 || info.Size > 536870912L || (!IsHexString(info.Sha512, 128) && !IsHexString(info.Sha1, 40))) throw new InvalidDataException("콘텐츠 파일 크기 또는 해시가 올바르지 않습니다."); + if (!Uri.TryCreate(info.Url, UriKind.Absolute, out uri) || uri.Scheme != Uri.UriSchemeHttps || !uri.Host.Equals("cdn.modrinth.com", StringComparison.OrdinalIgnoreCase) || !string.IsNullOrEmpty(uri.UserInfo)) throw Localized(new InvalidDataException("Modrinth CDN 다운로드 주소를 검증하지 못했습니다."), "Could not verify the Modrinth CDN download address."); + if (info.Size <= 0 || info.Size > 536870912L || (!IsHexString(info.Sha512, 128) && !IsHexString(info.Sha1, 40))) throw Localized(new InvalidDataException("콘텐츠 파일 크기 또는 해시가 올바르지 않습니다."), "The content file size or hash is invalid."); } private static string InstallManagedModrinthContent(string projectId, LauncherOptions options, string kind, string worldName, IProgress progress, CancellationToken cancellationToken) { - if (FindManagedModrinthEntry(options.ServerDirectory, projectId, kind, worldName) != null) throw new InvalidOperationException("같은 Modrinth 프로젝트가 이미 설치되어 있습니다."); + if (FindManagedModrinthEntry(options.ServerDirectory, projectId, kind, worldName) != null) throw Localized(new InvalidOperationException("같은 Modrinth 프로젝트가 이미 설치되어 있습니다."), "The same Modrinth project is already installed."); HashSet visiting = new HashSet(StringComparer.OrdinalIgnoreCase); HashSet complete = new HashSet(StringComparer.OrdinalIgnoreCase); return InstallManagedModrinthContentRecursive(projectId, options, kind, worldName, visiting, complete, progress, cancellationToken, 0); @@ -565,9 +565,9 @@ private static string InstallManagedModrinthContent(string projectId, LauncherOp private static string InstallManagedModrinthContentRecursive(string projectId, LauncherOptions options, string kind, string worldName, HashSet visiting, HashSet complete, IProgress progress, CancellationToken cancellationToken, int depth) { cancellationToken.ThrowIfCancellationRequested(); - if (depth > 32) throw new InvalidDataException("콘텐츠 의존성 단계가 지나치게 깊습니다."); + if (depth > 32) throw Localized(new InvalidDataException("콘텐츠 의존성 단계가 지나치게 깊습니다."), "The content dependency chain is nested too deeply."); if (complete.Contains(projectId)) return string.Empty; - if (!visiting.Add(projectId)) throw new InvalidDataException("콘텐츠 의존성 순환을 발견했습니다: " + projectId); + if (!visiting.Add(projectId)) throw Localized(new InvalidDataException("콘텐츠 의존성 순환을 발견했습니다: " + projectId), "A content dependency cycle was found: " + projectId); ReportContentProgress(progress, "metadata", 5, projectId); ModrinthFileInfo file = GetCompatibleModrinthFileForKind(projectId, options, kind); List dependencyIds = new List(); @@ -581,7 +581,7 @@ private static string InstallManagedModrinthContentRecursive(string projectId, L string dependencyVersion = GetJsonString(dependency, "version_id"); if (!string.IsNullOrEmpty(dependencyVersion)) dependencyProject = GetProjectIdForModrinthVersion(dependencyVersion); } - if (string.IsNullOrEmpty(dependencyProject)) throw new InvalidDataException("필수 콘텐츠 의존성의 프로젝트를 확인하지 못했습니다."); + if (string.IsNullOrEmpty(dependencyProject)) throw Localized(new InvalidDataException("필수 콘텐츠 의존성의 프로젝트를 확인하지 못했습니다."), "Could not resolve the project for a required content dependency."); if (!dependencyIds.Contains(dependencyProject, StringComparer.OrdinalIgnoreCase)) dependencyIds.Add(dependencyProject); InstallManagedModrinthContentRecursive(dependencyProject, options, kind, worldName, visiting, complete, progress, cancellationToken, depth + 1); } @@ -617,14 +617,14 @@ private static string InstallManagedModrinthFile(ModrinthFileInfo file, Launcher ContentManifestEntry existing = null; for (int i = 0; i < manifest.Items.Count; i++) if (string.Equals(manifest.Items[i].ProjectId, file.ProjectId, StringComparison.OrdinalIgnoreCase) && string.Equals(manifest.Items[i].Kind, kind, StringComparison.OrdinalIgnoreCase) && string.Equals(manifest.Items[i].WorldName ?? string.Empty, worldName ?? string.Empty, StringComparison.OrdinalIgnoreCase)) { existing = manifest.Items[i]; break; } - if (existing != null && !update) throw new InvalidOperationException("같은 Modrinth 프로젝트가 이미 설치되어 있습니다."); + if (existing != null && !update) throw Localized(new InvalidOperationException("같은 Modrinth 프로젝트가 이미 설치되어 있습니다."), "The same Modrinth project is already installed."); string relativeDirectory = GetContentTargetRelativeDirectory(options, kind, worldName); string destinationRelative = NormalizeContentRelativePath(relativeDirectory + "/" + file.FileName); string entryId = existing == null ? "modrinth-" + ComputeStableContentId(kind + ":" + file.ProjectId + ":" + (worldName ?? string.Empty)) : existing.Id; string disabledRelative = NormalizeContentRelativePath(".mineharbor/disabled/" + kind + "/" + entryId + "-" + file.FileName); bool targetActive = existing == null || existing.Active; string destination = GetSafeContentPath(options.ServerDirectory, targetActive ? destinationRelative : disabledRelative); - if (existing == null && (File.Exists(destination) || Directory.Exists(destination))) throw new IOException("같은 이름의 콘텐츠 파일이 이미 존재합니다."); + if (existing == null && (File.Exists(destination) || Directory.Exists(destination))) throw Localized(new IOException("같은 이름의 콘텐츠 파일이 이미 존재합니다."), "A content file with the same name already exists."); Directory.CreateDirectory(Path.GetDirectoryName(destination)); string temporary = destination + ".download-" + Guid.NewGuid().ToString("N"); string previous = null; @@ -634,12 +634,12 @@ private static string InstallManagedModrinthFile(ModrinthFileInfo file, Launcher { ReportContentProgress(progress, "download", 10, file.FileName); DownloadModrinthBinaryWithProgress(file.Url, temporary, file.Size, progress, cancellationToken); - if (!VerifyContentFileHash(temporary, file.Sha512, file.Sha1)) throw new InvalidDataException("다운로드한 콘텐츠의 무결성 검증에 실패했습니다."); + if (!VerifyContentFileHash(temporary, file.Sha512, file.Sha1)) throw Localized(new InvalidDataException("다운로드한 콘텐츠의 무결성 검증에 실패했습니다."), "Integrity verification of the downloaded content failed."); if (string.Equals(kind, "datapack", StringComparison.OrdinalIgnoreCase)) ValidateDatapackArchive(temporary); if (existing != null) { previousOriginalPath = GetSafeContentPath(options.ServerDirectory, existing.Active ? existing.RelativePath : existing.DisabledRelativePath); - if (!File.Exists(previousOriginalPath)) throw new FileNotFoundException("업데이트할 기존 콘텐츠 파일을 찾지 못했습니다.", previousOriginalPath); + if (!File.Exists(previousOriginalPath)) throw Localized(new FileNotFoundException("업데이트할 기존 콘텐츠 파일을 찾지 못했습니다.", previousOriginalPath), "The existing content file to update was not found."); string backupRelative = ".mineharbor/content-backups/" + DateTime.UtcNow.ToString("yyyyMMdd-HHmmss-fff", CultureInfo.InvariantCulture) + "/" + existing.FileName; previous = GetSafeContentPath(options.ServerDirectory, backupRelative); Directory.CreateDirectory(Path.GetDirectoryName(previous)); @@ -682,13 +682,13 @@ private static string GetContentTargetRelativeDirectory(LauncherOptions options, { if (string.Equals(kind, "datapack", StringComparison.OrdinalIgnoreCase)) { - if (string.IsNullOrWhiteSpace(worldName) || Path.GetFileName(worldName) != worldName) throw new InvalidDataException("데이터팩을 설치할 월드가 올바르지 않습니다."); + if (string.IsNullOrWhiteSpace(worldName) || Path.GetFileName(worldName) != worldName) throw Localized(new InvalidDataException("데이터팩을 설치할 월드가 올바르지 않습니다."), "The world to install the data pack into is invalid."); string world = Path.Combine(options.ServerDirectory, worldName); - if (!Directory.Exists(world) || !File.Exists(Path.Combine(world, "level.dat"))) throw new DirectoryNotFoundException("선택한 Minecraft 월드를 찾지 못했습니다."); + if (!Directory.Exists(world) || !File.Exists(Path.Combine(world, "level.dat"))) throw Localized(new DirectoryNotFoundException("선택한 Minecraft 월드를 찾지 못했습니다."), "The selected Minecraft world was not found."); return NormalizeContentRelativePath(worldName + "/datapacks"); } string folder = GetContentFolderName(options.ServerType); - if (string.IsNullOrEmpty(folder)) throw new InvalidOperationException("이 서버 종류는 플러그인 또는 모드 자동 설치를 지원하지 않습니다."); + if (string.IsNullOrEmpty(folder)) throw Localized(new InvalidOperationException("이 서버 종류는 플러그인 또는 모드 자동 설치를 지원하지 않습니다."), "This server type does not support automatic plugin or mod installation."); return folder; } @@ -718,11 +718,11 @@ private static void DownloadModrinthBinaryWithProgress(string url, string path, cancellationToken.ThrowIfCancellationRequested(); output.Write(buffer, 0, read); total = checked(total + read); - if (total > expectedSize) throw new InvalidDataException("콘텐츠 다운로드 크기가 공식 정보보다 큽니다."); + if (total > expectedSize) throw Localized(new InvalidDataException("콘텐츠 다운로드 크기가 공식 정보보다 큽니다."), "The content download is larger than the official metadata states."); ReportContentProgress(progress, "download", 10 + (int)Math.Min(80L, total * 80L / expectedSize), Path.GetFileName(path)); } output.Flush(true); - if (total != expectedSize) throw new InvalidDataException("콘텐츠 다운로드 크기가 공식 정보와 다릅니다."); + if (total != expectedSize) throw Localized(new InvalidDataException("콘텐츠 다운로드 크기가 공식 정보와 다릅니다."), "The content download size differs from the official metadata."); } } } @@ -745,7 +745,7 @@ private static bool CheckContentUpdate(LauncherOptions options, InstalledContent private static string UpdateManagedContent(LauncherOptions options, InstalledContentItem item, IProgress progress, CancellationToken cancellationToken) { - if (item == null || item.Entry == null || !item.Managed || !string.Equals(item.Entry.Source, "modrinth", StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("MineHarbor가 설치한 Modrinth 콘텐츠만 자동 업데이트할 수 있습니다."); + if (item == null || item.Entry == null || !item.Managed || !string.Equals(item.Entry.Source, "modrinth", StringComparison.OrdinalIgnoreCase)) throw Localized(new InvalidOperationException("MineHarbor가 설치한 Modrinth 콘텐츠만 자동 업데이트할 수 있습니다."), "Only Modrinth content installed by MineHarbor can be updated automatically."); ModrinthFileInfo latest = item.UpdateFile ?? GetCompatibleModrinthFileForKind(item.Entry.ProjectId, options, item.Entry.Kind); if (string.Equals(latest.VersionId, item.Entry.VersionId, StringComparison.OrdinalIgnoreCase)) return item.FullPath; HashSet visiting = new HashSet(StringComparer.OrdinalIgnoreCase); @@ -755,18 +755,18 @@ private static string UpdateManagedContent(LauncherOptions options, InstalledCon private static string InstallLocalContentFile(string sourcePath, LauncherOptions options, string kind, string worldName) { - if (!File.Exists(sourcePath)) throw new FileNotFoundException("설치 파일을 찾지 못했습니다.", sourcePath); + if (!File.Exists(sourcePath)) throw Localized(new FileNotFoundException("설치 파일을 찾지 못했습니다.", sourcePath), "The installation file was not found."); string extension = Path.GetExtension(sourcePath); if (string.Equals(kind, "datapack", StringComparison.OrdinalIgnoreCase)) { - if (!extension.Equals(".zip", StringComparison.OrdinalIgnoreCase)) throw new InvalidDataException("데이터팩은 ZIP 파일만 설치할 수 있습니다."); + if (!extension.Equals(".zip", StringComparison.OrdinalIgnoreCase)) throw Localized(new InvalidDataException("데이터팩은 ZIP 파일만 설치할 수 있습니다."), "Only ZIP files can be installed as data packs."); ValidateDatapackArchive(sourcePath); } - else if (!extension.Equals(".jar", StringComparison.OrdinalIgnoreCase)) throw new InvalidDataException("플러그인과 모드는 JAR 파일만 설치할 수 있습니다."); + else if (!extension.Equals(".jar", StringComparison.OrdinalIgnoreCase)) throw Localized(new InvalidDataException("플러그인과 모드는 JAR 파일만 설치할 수 있습니다."), "Only JAR files can be installed as plugins or mods."); string directory = GetContentTargetRelativeDirectory(options, kind, worldName); string relative = NormalizeContentRelativePath(directory + "/" + Path.GetFileName(sourcePath)); string destination = GetSafeContentPath(options.ServerDirectory, relative); - if (File.Exists(destination) || Directory.Exists(destination)) throw new IOException("같은 이름의 콘텐츠가 이미 설치되어 있습니다."); + if (File.Exists(destination) || Directory.Exists(destination)) throw Localized(new IOException("같은 이름의 콘텐츠가 이미 설치되어 있습니다."), "Content with the same name is already installed."); Directory.CreateDirectory(Path.GetDirectoryName(destination)); File.Copy(sourcePath, destination, false); ContentManifestModel manifest = LoadContentManifest(options.ServerDirectory); @@ -834,10 +834,10 @@ private static string ReadServerProperty(string path, string key) private static void ValidateDatapackArchive(string path) { FileInfo file = new FileInfo(path); - if (!file.Exists || file.Length <= 0 || file.Length > 536870912L) throw new InvalidDataException("데이터팩 ZIP 크기가 허용 범위를 벗어났습니다."); + if (!file.Exists || file.Length <= 0 || file.Length > 536870912L) throw Localized(new InvalidDataException("데이터팩 ZIP 크기가 허용 범위를 벗어났습니다."), "The data pack ZIP size is outside the allowed range."); using (ZipArchive archive = ZipFile.OpenRead(path)) { - if (archive.Entries.Count == 0 || archive.Entries.Count > MaximumDatapackEntries) throw new InvalidDataException("데이터팩 ZIP 항목 수가 올바르지 않습니다."); + if (archive.Entries.Count == 0 || archive.Entries.Count > MaximumDatapackEntries) throw Localized(new InvalidDataException("데이터팩 ZIP 항목 수가 올바르지 않습니다."), "The data pack ZIP entry count is invalid."); long expanded = 0; ZipArchiveEntry metadata = null; HashSet paths = new HashSet(StringComparer.OrdinalIgnoreCase); @@ -845,17 +845,17 @@ private static void ValidateDatapackArchive(string path) { ZipArchiveEntry entry = archive.Entries[i]; string name = entry.FullName.Replace('\\', '/'); - if (!IsSafeDatapackEntryPath(name) || !paths.Add(name)) throw new InvalidDataException("데이터팩 ZIP 경로가 안전하지 않습니다."); + if (!IsSafeDatapackEntryPath(name) || !paths.Add(name)) throw Localized(new InvalidDataException("데이터팩 ZIP 경로가 안전하지 않습니다."), "The data pack ZIP path is not safe."); expanded = checked(expanded + entry.Length); - if (expanded > MaximumDatapackExpandedBytes) throw new InvalidDataException("데이터팩의 총 해제 크기가 허용 범위를 초과했습니다."); + if (expanded > MaximumDatapackExpandedBytes) throw Localized(new InvalidDataException("데이터팩의 총 해제 크기가 허용 범위를 초과했습니다."), "The total expanded size of the data pack exceeds the allowed range."); if (string.Equals(name, "pack.mcmeta", StringComparison.OrdinalIgnoreCase)) metadata = entry; } - if (metadata == null || metadata.Length <= 0 || metadata.Length > 1048576L) throw new InvalidDataException("데이터팩 루트에서 pack.mcmeta를 찾지 못했습니다."); + if (metadata == null || metadata.Length <= 0 || metadata.Length > 1048576L) throw Localized(new InvalidDataException("데이터팩 루트에서 pack.mcmeta를 찾지 못했습니다."), "pack.mcmeta was not found in the data pack root."); using (StreamReader reader = new StreamReader(metadata.Open(), Encoding.UTF8)) { Dictionary root = new JavaScriptSerializer().DeserializeObject(reader.ReadToEnd()) as Dictionary; Dictionary pack = root != null && root.ContainsKey("pack") ? root["pack"] as Dictionary : null; - if (pack == null || !pack.ContainsKey("pack_format") || Convert.ToInt32(pack["pack_format"], CultureInfo.InvariantCulture) <= 0) throw new InvalidDataException("pack.mcmeta의 pack_format이 올바르지 않습니다."); + if (pack == null || !pack.ContainsKey("pack_format") || Convert.ToInt32(pack["pack_format"], CultureInfo.InvariantCulture) <= 0) throw Localized(new InvalidDataException("pack.mcmeta의 pack_format이 올바르지 않습니다."), "The pack_format in pack.mcmeta is invalid."); } } } @@ -882,7 +882,7 @@ private static void ValidateContentDependencyGraph(Dictionary private static void VisitContentDependency(string node, Dictionary graph, HashSet complete, HashSet visiting, int depth) { if (complete.Contains(node)) return; - if (depth > 64 || !visiting.Add(node)) throw new InvalidDataException("콘텐츠 의존성 순환을 발견했습니다: " + node); + if (depth > 64 || !visiting.Add(node)) throw Localized(new InvalidDataException("콘텐츠 의존성 순환을 발견했습니다: " + node), "A content dependency cycle was found: " + node); string[] dependencies; if (graph.TryGetValue(node, out dependencies) && dependencies != null) for (int i = 0; i < dependencies.Length; i++) if (!string.IsNullOrWhiteSpace(dependencies[i])) VisitContentDependency(dependencies[i], graph, complete, visiting, depth + 1); diff --git a/README.md b/README.md index c282dbc..d14c140 100644 --- a/README.md +++ b/README.md @@ -26,9 +26,9 @@ | **Windows 설치 프로그램** | 시작 메뉴, 선택적 바탕화면 바로가기, 제거 기능 사용 | **[최신 Release 열기](https://github.com/Mangom72/MineHarbor/releases/latest)** | | **Portable ZIP** | README와 라이선스를 포함한 묶음 보관 | **[최신 Release 열기](https://github.com/Mangom72/MineHarbor/releases/latest)** | -현재 소스 버전은 `v1.19.0`, 내부 빌드는 `26.2.45.87`입니다. MineHarbor 이름으로 배포된 Portable EXE는 같은 링크에서 계속 최신 파일을 받을 수 있습니다. 기존 설치의 `%LOCALAPPDATA%\MinecraftServerLauncher` 데이터는 자동으로 찾아 그대로 사용하며, 새 사용자 데이터 경로는 `%LOCALAPPDATA%\MineHarbor`입니다. +현재 소스 버전은 `v1.20.0`, 내부 빌드는 `26.2.45.88`입니다. MineHarbor 이름으로 배포된 Portable EXE는 같은 링크에서 계속 최신 파일을 받을 수 있습니다. 기존 설치의 `%LOCALAPPDATA%\MinecraftServerLauncher` 데이터는 자동으로 찾아 그대로 사용하며, 새 사용자 데이터 경로는 `%LOCALAPPDATA%\MineHarbor`입니다. -이 README는 로드맵이 아니라 현재 `v1.19.0` 소스와 자동 테스트, 공개 Release 자산에서 확인한 기능만 설명합니다. 서버 종류나 브리지 연결처럼 조건에 따라 달라지는 기능과 지원되지 않는 상태는 아래에 따로 표시합니다. +이 README는 로드맵이 아니라 현재 `v1.20.0` 소스와 자동 테스트, 공개 Release 자산에서 확인한 기능만 설명합니다. 서버 종류나 브리지 연결처럼 조건에 따라 달라지는 기능과 지원되지 않는 상태는 아래에 따로 표시합니다. > [!WARNING] > 현재 릴리스 실행 파일은 요청된 자체서명 인증서로 무결성을 표시하지만 공개 인증 기관이 신뢰한 배포자 서명은 아닙니다. 따라서 Windows SmartScreen 경고가 나타날 수 있습니다. Release의 `SHA256SUMS.txt`와 GitHub 출처를 함께 확인해 주세요. @@ -312,9 +312,9 @@ PR과 `main` 푸시의 일반 CI는 버전·문서 일치, SDK 스타일 `net48` | **Windows installer** | Start Menu, optional desktop shortcut, and uninstall support | **[Open the latest release](https://github.com/Mangom72/MineHarbor/releases/latest)** | | **Portable ZIP** | Keep the launcher, README, and license together | **[Open the latest release](https://github.com/Mangom72/MineHarbor/releases/latest)** | -Current source version: `v1.19.0` · internal build: `26.2.45.87`. MineHarbor releases keep the same permanent Portable URL. Existing data under `%LOCALAPPDATA%\MinecraftServerLauncher` is detected and preserved; new user-data installations use `%LOCALAPPDATA%\MineHarbor`. +Current source version: `v1.20.0` · internal build: `26.2.45.88`. MineHarbor releases keep the same permanent Portable URL. Existing data under `%LOCALAPPDATA%\MinecraftServerLauncher` is detected and preserved; new user-data installations use `%LOCALAPPDATA%\MineHarbor`. -This README documents shipped behavior verified against the current `v1.19.0` source, automated tests, and public release assets—not roadmap items. Conditional and unsupported behavior is called out explicitly below. +This README documents shipped behavior verified against the current `v1.20.0` source, automated tests, and public release assets—not roadmap items. Conditional and unsupported behavior is called out explicitly below. > [!WARNING] > Release executables carry the requested self-signed integrity signature, not a publisher identity trusted by a public certificate authority. Windows SmartScreen can therefore still warn. Verify the GitHub source and the release `SHA256SUMS.txt`. diff --git a/RuntimeCompatibility.cs b/RuntimeCompatibility.cs index 2d680f1..0756eea 100644 --- a/RuntimeCompatibility.cs +++ b/RuntimeCompatibility.cs @@ -107,7 +107,7 @@ private static CompatibleJavaRuntime PrepareCompatibleJavaRuntime(LauncherOption } if (!Environment.Is64BitOperatingSystem) { - throw new PlatformNotSupportedException("이 런처가 준비하는 Minecraft Java 서버 런타임은 64비트 Windows가 필요합니다."); + throw Localized(new PlatformNotSupportedException("이 런처가 준비하는 Minecraft Java 서버 런타임은 64비트 Windows가 필요합니다."), "The Minecraft Java server runtime prepared by this launcher requires 64-bit Windows."); } JavaRuntimeRequirement requirement = ResolveCompatibleJavaRequirement(options, customJavaMajor); @@ -120,7 +120,7 @@ private static CompatibleJavaRuntime PrepareCompatibleJavaRuntime(LauncherOption JavaExecutableProbe bundledProbe = ProbeJavaExecutable(bundledJavaPath, requirement.MajorVersion); if (!bundledProbe.IsValid) { - throw new InvalidDataException("기존 Java 25 캐시 검증 실패: " + bundledProbe.Error); + throw Localized(new InvalidDataException("기존 Java 25 캐시 검증 실패: " + bundledProbe.Error), "Existing Java 25 cache verification failed: " + bundledProbe.Error); } return CreateCompatibleJavaRuntime(requirement, bundledJavaPath, bundledProbe, "기존 Java 25 캐시", false, false); } @@ -143,7 +143,7 @@ private static CompatibleJavaRuntime PrepareCompatibleJavaRuntime(LauncherOption JavaExecutableProbe downloadedProbe = ProbeJavaExecutable(downloadedJavaPath, requirement.MajorVersion); if (!downloadedProbe.IsValid) { - throw new InvalidDataException("다운로드한 Java 검증 실패: " + downloadedProbe.Error); + throw Localized(new InvalidDataException("다운로드한 Java 검증 실패: " + downloadedProbe.Error), "Downloaded Java verification failed: " + downloadedProbe.Error); } return CreateCompatibleJavaRuntime(requirement, downloadedJavaPath, downloadedProbe, "Eclipse Temurin 캐시", false, false); } @@ -402,7 +402,7 @@ private static int GetMojangMetadataJavaMajor(string minecraftVersion) { if (string.IsNullOrWhiteSpace(minecraftVersion)) { - throw new InvalidDataException("Minecraft 버전이 비어 있습니다."); + throw Localized(new InvalidDataException("Minecraft 버전이 비어 있습니다."), "The Minecraft version is empty."); } lock (RuntimeCompatibilityMojangCacheLock) { @@ -419,7 +419,7 @@ private static int GetMojangMetadataJavaMajor(string minecraftVersion) object[] versions = manifest != null && manifest.ContainsKey("versions") ? manifest["versions"] as object[] : null; if (versions == null) { - throw new InvalidDataException("Mojang 버전 목록 형식이 올바르지 않습니다."); + throw Localized(new InvalidDataException("Mojang 버전 목록 형식이 올바르지 않습니다."), "The Mojang version manifest format is invalid."); } string versionMetadataUrl = null; for (int i = 0; i < versions.Length; i = checked(i + 1)) @@ -433,7 +433,7 @@ private static int GetMojangMetadataJavaMajor(string minecraftVersion) } if (!IsTrustedRuntimeCompatibilityUri(versionMetadataUrl, RuntimeCompatibilityMojangMetadataHosts)) { - throw new InvalidDataException("선택한 Minecraft 버전의 신뢰할 수 있는 Mojang 메타데이터 URL을 찾지 못했습니다."); + throw Localized(new InvalidDataException("선택한 Minecraft 버전의 신뢰할 수 있는 Mojang 메타데이터 URL을 찾지 못했습니다."), "No trusted Mojang metadata URL was found for the selected Minecraft version."); } string versionJson = DownloadRuntimeCompatibilityText(versionMetadataUrl, RuntimeCompatibilityMojangMetadataHosts, 4194304); @@ -441,12 +441,12 @@ private static int GetMojangMetadataJavaMajor(string minecraftVersion) Dictionary javaVersion = versionRoot != null && versionRoot.ContainsKey("javaVersion") ? versionRoot["javaVersion"] as Dictionary : null; if (javaVersion == null || !javaVersion.ContainsKey("majorVersion")) { - throw new InvalidDataException("Mojang 메타데이터에 javaVersion.majorVersion이 없습니다."); + throw Localized(new InvalidDataException("Mojang 메타데이터에 javaVersion.majorVersion이 없습니다."), "The Mojang metadata has no javaVersion.majorVersion."); } int major = Convert.ToInt32(javaVersion["majorVersion"], CultureInfo.InvariantCulture); if (major < 8 || major > 30) { - throw new InvalidDataException("Mojang 메타데이터가 예상 범위를 벗어난 Java 버전을 반환했습니다: " + major); + throw Localized(new InvalidDataException("Mojang 메타데이터가 예상 범위를 벗어난 Java 버전을 반환했습니다: " + major), "The Mojang metadata returned a Java version outside the expected range: " + major); } lock (RuntimeCompatibilityMojangCacheLock) { @@ -467,13 +467,13 @@ private static string GetCompatibleRuntimesRoot(string serversRoot) { if (string.IsNullOrWhiteSpace(serversRoot)) { - throw new ArgumentException("서버 루트 폴더가 비어 있습니다.", "serversRoot"); + throw Localized(new ArgumentException("서버 루트 폴더가 비어 있습니다.", "serversRoot"), "The server root folder is empty."); } string fullServersRoot = Path.GetFullPath(serversRoot); string runtimesRoot = Path.GetFullPath(Path.Combine(fullServersRoot, "runtimes")); if (!IsPathWithinRuntimeCompatibilityRoot(runtimesRoot, fullServersRoot)) { - throw new InvalidDataException("Java 런타임 캐시 경로가 서버 루트 밖을 가리킵니다."); + throw Localized(new InvalidDataException("Java 런타임 캐시 경로가 서버 루트 밖을 가리킵니다."), "The Java runtime cache path points outside the server root."); } Directory.CreateDirectory(runtimesRoot); return runtimesRoot; @@ -518,7 +518,7 @@ private static string GetCompatibleRuntimeDirectory(string runtimesRoot, int maj string directory = Path.GetFullPath(Path.Combine(runtimesRoot, "java-" + majorVersion.ToString(CultureInfo.InvariantCulture))); if (!IsPathWithinRuntimeCompatibilityRoot(directory, runtimesRoot)) { - throw new InvalidDataException("Java 런타임 경로가 캐시 루트 밖을 가리킵니다."); + throw Localized(new InvalidDataException("Java 런타임 경로가 캐시 루트 밖을 가리킵니다."), "The Java runtime path points outside the cache root."); } return directory; } @@ -541,11 +541,11 @@ private static string PrepareAdoptiumRuntime(int majorVersion, string runtimesRo FileInfo zipInfo = new FileInfo(temporaryZipPath); if (zipInfo.Length != package.Size) { - throw new InvalidDataException("Java ZIP 크기가 API 정보와 일치하지 않습니다. 예상 " + package.Size + "바이트, 실제 " + zipInfo.Length + "바이트입니다."); + throw Localized(new InvalidDataException("Java ZIP 크기가 API 정보와 일치하지 않습니다. 예상 " + package.Size + "바이트, 실제 " + zipInfo.Length + "바이트입니다."), "The Java ZIP size does not match the API metadata. Expected " + package.Size + " bytes but found " + zipInfo.Length + " bytes."); } if (!string.Equals(GetRuntimeCompatibilityFileSha256(temporaryZipPath), package.Checksum, StringComparison.OrdinalIgnoreCase)) { - throw new InvalidDataException("Java ZIP의 SHA-256 검증에 실패했습니다."); + throw Localized(new InvalidDataException("Java ZIP의 SHA-256 검증에 실패했습니다."), "SHA-256 verification of the Java ZIP failed."); } DeleteRuntimeCompatibilityDirectoryIfPresent(stagingDirectory, runtimesRoot); @@ -554,12 +554,12 @@ private static string PrepareAdoptiumRuntime(int majorVersion, string runtimesRo string stagingJavaPath = FindRuntimeCompatibilityJavaExecutable(stagingDirectory); if (string.IsNullOrEmpty(stagingJavaPath)) { - throw new FileNotFoundException("압축을 푼 Java 런타임에서 bin\\java.exe를 찾지 못했습니다."); + throw Localized(new FileNotFoundException("압축을 푼 Java 런타임에서 bin\\java.exe를 찾지 못했습니다."), "bin\\java.exe was not found in the extracted Java runtime."); } JavaExecutableProbe stagingProbe = ProbeJavaExecutable(stagingJavaPath, majorVersion); if (!stagingProbe.IsValid) { - throw new InvalidDataException("압축을 푼 Java 실행 파일 검증 실패: " + stagingProbe.Error); + throw Localized(new InvalidDataException("압축을 푼 Java 실행 파일 검증 실패: " + stagingProbe.Error), "Extracted Java executable verification failed: " + stagingProbe.Error); } WriteRuntimeCompatibilityMarker(stagingDirectory, majorVersion, package, stagingJavaPath); @@ -575,12 +575,12 @@ private static string PrepareAdoptiumRuntime(int majorVersion, string runtimesRo string installedJavaPath = ResolveMarkedRuntimeJavaPath(targetDirectory, installedMarker); if (string.IsNullOrEmpty(installedJavaPath) || !File.Exists(installedJavaPath)) { - throw new FileNotFoundException("준비된 Java 캐시에서 java.exe를 찾지 못했습니다."); + throw Localized(new FileNotFoundException("준비된 Java 캐시에서 java.exe를 찾지 못했습니다."), "java.exe was not found in the prepared Java cache."); } JavaExecutableProbe installedProbe = ProbeJavaExecutable(installedJavaPath, majorVersion); if (!installedProbe.IsValid) { - throw new InvalidDataException("설치한 Java 캐시의 최종 검증 실패: " + installedProbe.Error); + throw Localized(new InvalidDataException("설치한 Java 캐시의 최종 검증 실패: " + installedProbe.Error), "Final verification of the installed Java cache failed: " + installedProbe.Error); } if (previousMoved) { @@ -657,7 +657,7 @@ private static AdoptiumRuntimePackage QueryAdoptiumRuntimePackage(int majorVersi object[] releases = CreateRuntimeCompatibilityJsonSerializer().DeserializeObject(json) as object[]; if (releases == null) { - throw new InvalidDataException("Adoptium 응답이 배열이 아닙니다."); + throw Localized(new InvalidDataException("Adoptium 응답이 배열이 아닙니다."), "The Adoptium response is not an array."); } for (int i = 0; i < releases.Length; i = checked(i + 1)) { @@ -687,19 +687,19 @@ private static AdoptiumRuntimePackage QueryAdoptiumRuntimePackage(int majorVersi package.ImageType = imageType; if (!IsTrustedRuntimeCompatibilityUri(package.Link, RuntimeCompatibilityAdoptiumDownloadHosts)) { - throw new InvalidDataException("Adoptium이 허용되지 않은 다운로드 호스트를 반환했습니다."); + throw Localized(new InvalidDataException("Adoptium이 허용되지 않은 다운로드 호스트를 반환했습니다."), "Adoptium returned a download host that is not allowed."); } if (!IsSha256Text(package.Checksum)) { - throw new InvalidDataException("Adoptium SHA-256 값 형식이 올바르지 않습니다."); + throw Localized(new InvalidDataException("Adoptium SHA-256 값 형식이 올바르지 않습니다."), "The Adoptium SHA-256 value format is invalid."); } if (package.Size < 1048576L || package.Size > RuntimeCompatibilityMaximumPackageBytes) { - throw new InvalidDataException("Adoptium ZIP 크기가 허용 범위를 벗어났습니다: " + package.Size); + throw Localized(new InvalidDataException("Adoptium ZIP 크기가 허용 범위를 벗어났습니다: " + package.Size), "The Adoptium ZIP size is outside the allowed range: " + package.Size); } if (!package.Name.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)) { - throw new InvalidDataException("Adoptium 패키지가 ZIP 파일이 아닙니다: " + package.Name); + throw Localized(new InvalidDataException("Adoptium 패키지가 ZIP 파일이 아닙니다: " + package.Name), "The Adoptium package is not a ZIP file: " + package.Name); } return package; } @@ -720,7 +720,7 @@ private static string DownloadRuntimeCompatibilityText(string url, string[] allo { if (response.ContentLength > maximumBytes) { - throw new InvalidDataException("메타데이터 응답이 허용 크기를 초과했습니다."); + throw Localized(new InvalidDataException("메타데이터 응답이 허용 크기를 초과했습니다."), "The metadata response exceeds the allowed size."); } byte[] chunk = new byte[32768]; int total = 0; @@ -734,7 +734,7 @@ private static string DownloadRuntimeCompatibilityText(string url, string[] allo total = checked(total + read); if (total > maximumBytes) { - throw new InvalidDataException("메타데이터 응답이 허용 크기를 초과했습니다."); + throw Localized(new InvalidDataException("메타데이터 응답이 허용 크기를 초과했습니다."), "The metadata response exceeds the allowed size."); } buffer.Write(chunk, 0, read); } @@ -750,7 +750,7 @@ private static void DownloadRuntimeCompatibilityFile(string url, string destinat { if (response.ContentLength >= 0L && response.ContentLength != expectedSize) { - throw new InvalidDataException("다운로드 응답 크기가 Adoptium API 정보와 일치하지 않습니다."); + throw Localized(new InvalidDataException("다운로드 응답 크기가 Adoptium API 정보와 일치하지 않습니다."), "The download response size does not match the Adoptium API metadata."); } byte[] buffer = new byte[1048576]; long total = 0L; @@ -764,13 +764,13 @@ private static void DownloadRuntimeCompatibilityFile(string url, string destinat total = checked(total + read); if (total > expectedSize || total > RuntimeCompatibilityMaximumPackageBytes) { - throw new InvalidDataException("Java ZIP 다운로드가 예상 크기를 초과했습니다."); + throw Localized(new InvalidDataException("Java ZIP 다운로드가 예상 크기를 초과했습니다."), "The Java ZIP download exceeded the expected size."); } destination.Write(buffer, 0, read); } if (total != expectedSize) { - throw new EndOfStreamException("Java ZIP 다운로드가 완료되기 전에 연결이 끝났습니다."); + throw Localized(new EndOfStreamException("Java ZIP 다운로드가 완료되기 전에 연결이 끝났습니다."), "The connection ended before the Java ZIP download finished."); } destination.Flush(true); } @@ -782,13 +782,13 @@ private static HttpWebResponse OpenRuntimeCompatibilityResponse(string url, stri Uri current; if (!Uri.TryCreate(url, UriKind.Absolute, out current)) { - throw new InvalidDataException("다운로드 URL 형식이 올바르지 않습니다."); + throw Localized(new InvalidDataException("다운로드 URL 형식이 올바르지 않습니다."), "The download URL format is invalid."); } for (int redirectCount = 0; redirectCount <= 8; redirectCount = checked(redirectCount + 1)) { if (!IsTrustedRuntimeCompatibilityUri(current.AbsoluteUri, allowedHosts)) { - throw new InvalidDataException("허용되지 않은 HTTPS 다운로드 주소입니다: " + current.Host); + throw Localized(new InvalidDataException("허용되지 않은 HTTPS 다운로드 주소입니다: " + current.Host), "This HTTPS download address is not allowed: " + current.Host); } HttpWebRequest request = (HttpWebRequest)WebRequest.Create(current); request.Method = "GET"; @@ -807,12 +807,12 @@ private static HttpWebResponse OpenRuntimeCompatibilityResponse(string url, stri response.Close(); if (string.IsNullOrWhiteSpace(location)) { - throw new WebException("리디렉션 응답에 Location 헤더가 없습니다."); + throw Localized(new WebException("리디렉션 응답에 Location 헤더가 없습니다."), "The redirect response has no Location header."); } Uri redirected; if (!Uri.TryCreate(current, location, out redirected) || !IsTrustedRuntimeCompatibilityUri(redirected.AbsoluteUri, allowedHosts)) { - throw new InvalidDataException("리디렉션 대상 호스트가 허용 목록에 없습니다."); + throw Localized(new InvalidDataException("리디렉션 대상 호스트가 허용 목록에 없습니다."), "The redirect target host is not on the allow list."); } current = redirected; continue; @@ -824,7 +824,7 @@ private static HttpWebResponse OpenRuntimeCompatibilityResponse(string url, stri } return response; } - throw new WebException("다운로드 리디렉션 횟수가 허용 범위를 초과했습니다."); + throw Localized(new WebException("다운로드 리디렉션 횟수가 허용 범위를 초과했습니다."), "The number of download redirects exceeds the allowed range."); } private static bool IsTrustedRuntimeCompatibilityUri(string url, string[] allowedHosts) @@ -858,7 +858,7 @@ private static void ExtractRuntimeCompatibilityZip(string zipPath, string destin entryCount = checked(entryCount + 1); if (entryCount > RuntimeCompatibilityMaximumZipEntries) { - throw new InvalidDataException("Java ZIP 항목 수가 허용 범위를 초과했습니다."); + throw Localized(new InvalidDataException("Java ZIP 항목 수가 허용 범위를 초과했습니다."), "The Java ZIP entry count exceeds the allowed range."); } string relativePath = (entry.FullName ?? string.Empty).Replace('/', Path.DirectorySeparatorChar); if (string.IsNullOrWhiteSpace(relativePath) || Path.IsPathRooted(relativePath)) @@ -867,7 +867,7 @@ private static void ExtractRuntimeCompatibilityZip(string zipPath, string destin { continue; } - throw new InvalidDataException("Java ZIP에 절대 경로 항목이 포함되어 있습니다."); + throw Localized(new InvalidDataException("Java ZIP에 절대 경로 항목이 포함되어 있습니다."), "The Java ZIP contains an absolute-path entry."); } string destinationPath; try @@ -876,11 +876,11 @@ private static void ExtractRuntimeCompatibilityZip(string zipPath, string destin } catch (Exception ex) { - throw new InvalidDataException("Java ZIP 항목 경로가 올바르지 않습니다: " + SummarizeRuntimeCompatibilityError(ex)); + throw Localized(new InvalidDataException("Java ZIP 항목 경로가 올바르지 않습니다: " + SummarizeRuntimeCompatibilityError(ex)), "A Java ZIP entry path is invalid: " + SummarizeRuntimeCompatibilityError(ex)); } if (!destinationPath.StartsWith(rootPrefix, StringComparison.OrdinalIgnoreCase)) { - throw new InvalidDataException("Java ZIP 경로 탈출 항목을 차단했습니다: " + entry.FullName); + throw Localized(new InvalidDataException("Java ZIP 경로 탈출 항목을 차단했습니다: " + entry.FullName), "Blocked a path-escaping entry in the Java ZIP: " + entry.FullName); } bool isDirectory = string.IsNullOrEmpty(entry.Name) || relativePath.EndsWith(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal); if (isDirectory) @@ -890,12 +890,12 @@ private static void ExtractRuntimeCompatibilityZip(string zipPath, string destin } if (entry.Length < 0L || entry.Length > RuntimeCompatibilityMaximumExtractedBytes) { - throw new InvalidDataException("Java ZIP 항목 크기가 허용 범위를 벗어났습니다."); + throw Localized(new InvalidDataException("Java ZIP 항목 크기가 허용 범위를 벗어났습니다."), "A Java ZIP entry size is outside the allowed range."); } totalExtracted = checked(totalExtracted + entry.Length); if (totalExtracted > RuntimeCompatibilityMaximumExtractedBytes) { - throw new InvalidDataException("Java ZIP의 전체 압축 해제 크기가 허용 범위를 초과했습니다."); + throw Localized(new InvalidDataException("Java ZIP의 전체 압축 해제 크기가 허용 범위를 초과했습니다."), "The total expanded size of the Java ZIP exceeds the allowed range."); } string parent = Path.GetDirectoryName(destinationPath); if (!string.IsNullOrEmpty(parent)) @@ -926,13 +926,13 @@ private static void CopyRuntimeCompatibilityStream(Stream source, Stream destina total = checked(total + read); if (total > expectedLength) { - throw new InvalidDataException("Java ZIP 항목이 선언된 크기를 초과했습니다."); + throw Localized(new InvalidDataException("Java ZIP 항목이 선언된 크기를 초과했습니다."), "A Java ZIP entry exceeded its declared size."); } destination.Write(buffer, 0, read); } if (total != expectedLength) { - throw new EndOfStreamException("Java ZIP 항목이 선언된 크기보다 짧습니다."); + throw Localized(new EndOfStreamException("Java ZIP 항목이 선언된 크기보다 짧습니다."), "A Java ZIP entry is shorter than its declared size."); } } @@ -942,7 +942,7 @@ private static void WriteRuntimeCompatibilityMarker(string stagingDirectory, int string fullJava = Path.GetFullPath(javaPath); if (!fullJava.StartsWith(fullStaging, StringComparison.OrdinalIgnoreCase)) { - throw new InvalidDataException("java.exe가 준비 폴더 밖을 가리킵니다."); + throw Localized(new InvalidDataException("java.exe가 준비 폴더 밖을 가리킵니다."), "java.exe points outside the staging folder."); } string relativeJava = fullJava.Substring(fullStaging.Length).Replace(Path.DirectorySeparatorChar, '/'); StringBuilder marker = new StringBuilder(); @@ -1337,16 +1337,16 @@ private static void EnsureRuntimeCompatibilityDirectoryIsSafe(string path, strin string fullRoot = Path.GetFullPath(runtimesRoot).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); if (string.Equals(fullPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar), fullRoot, StringComparison.OrdinalIgnoreCase) || !IsPathWithinRuntimeCompatibilityRoot(fullPath, fullRoot)) { - throw new InvalidDataException("런타임 작업 경로가 캐시 루트 밖을 가리킵니다."); + throw Localized(new InvalidDataException("런타임 작업 경로가 캐시 루트 밖을 가리킵니다."), "The runtime work path points outside the cache root."); } string name = Path.GetFileName(fullPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)); if (string.IsNullOrEmpty(name) || (!name.StartsWith("java-", StringComparison.OrdinalIgnoreCase) && !name.StartsWith(".java-", StringComparison.OrdinalIgnoreCase))) { - throw new InvalidDataException("런타임 작업 폴더 이름이 허용된 형식이 아닙니다."); + throw Localized(new InvalidDataException("런타임 작업 폴더 이름이 허용된 형식이 아닙니다."), "The runtime work folder name does not use an allowed format."); } if (Directory.Exists(fullPath) && (File.GetAttributes(fullPath) & FileAttributes.ReparsePoint) != 0) { - throw new InvalidDataException("런타임 캐시 폴더가 재분석 지점을 가리켜 작업을 중단했습니다."); + throw Localized(new InvalidDataException("런타임 캐시 폴더가 재분석 지점을 가리켜 작업을 중단했습니다."), "The operation stopped because the runtime cache folder points to a reparse point."); } } @@ -1391,7 +1391,7 @@ private static void DeleteRuntimeCompatibilityFileIfPresent(string path, string } if (!IsPathWithinRuntimeCompatibilityRoot(path, runtimesRoot) || !Path.GetFileName(path).StartsWith(".java-", StringComparison.OrdinalIgnoreCase)) { - throw new InvalidDataException("임시 런타임 파일 경로가 허용 범위를 벗어났습니다."); + throw Localized(new InvalidDataException("임시 런타임 파일 경로가 허용 범위를 벗어났습니다."), "The temporary runtime file path is outside the allowed range."); } File.SetAttributes(path, FileAttributes.Normal); File.Delete(path); diff --git a/docs/ai/SYNC_STATE.md b/docs/ai/SYNC_STATE.md index 7ce303e..4210c80 100644 --- a/docs/ai/SYNC_STATE.md +++ b/docs/ai/SYNC_STATE.md @@ -1,5 +1,13 @@ # AI Agent Synchronization State +## Claude Exception Localization 3/4 - 2026-07-27 + +- **Current Version**: 1.20.0 (build 26.2.45.88) +- **Branch**: `claude/remote-control-u4tvpt` +- **Status**: 예외 메시지 이중화 3차분 105곳 적용 +- 변환: `ContentManagementServices`(60), `RuntimeCompatibility`(45). +- `ArgumentException(message, paramName)`처럼 두 번째 인자가 문자열인 형태도 `Localized`로 감쌌습니다. 매개변수 이름은 그대로 유지됩니다. +- **남은 작업**: `decompiled/Launcher.decompiled.cs`(67) → 4차분. 이 파일은 CRLF·CR·LF가 섞여 있어 일반 편집 도구로 저장하면 줄바꿈이 전체 정규화되므로 반드시 바이트 단위로 편집해야 합니다. ## Claude Exception Localization 2/4 - 2026-07-27 - **Current Version**: 1.19.0 (build 26.2.45.87) diff --git a/version.json b/version.json index 29e44ec..d480ea3 100644 --- a/version.json +++ b/version.json @@ -1,5 +1,5 @@ { - "productVersion": "1.19.0", - "buildNumber": "26.2.45.87", + "productVersion": "1.20.0", + "buildNumber": "26.2.45.88", "minimumSupportedVersion": "0.1.0" }