From 81e64c47898b1eaf3fd00d25b72b469a485b0ee7 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Jul 2026 01:51:46 +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=202/4=20(v1.19.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 백업·복원, 콘텐츠 다운로드, 중복 설정, 명령 브리지, UPnP 외부 접속, 서버 관리 기능의 오류 문구 95곳을 Localized로 변환했습니다. 예외 타입은 그대로 두고 Exception.Data에 영어 문구만 덧붙이는 v1.18.0 방식을 이어갑니다. 문자열이 이어붙는 형태는 영어 쪽도 같은 값을 이어붙이도록 개별 변환했습니다. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01A4KU6BZL5oWTjH2eDB18Yu --- BackupAndProfileTools.cs | 60 ++++++++++++++++++------------------- CHANGELOG.md | 11 +++++++ ContentAndDiagnostics.cs | 42 +++++++++++++------------- DuplicationSettings.cs | 32 ++++++++++---------- ManagedServerDashboard.cs | 2 +- ModernLauncherGui.cs | 4 +-- QuickCommandsAndBridge.cs | 24 +++++++-------- README.md | 8 ++--- ServerManagementFeatures.cs | 10 +++---- UpnpCore.cs | 2 +- UpnpExternalAccess.cs | 12 ++++---- docs/ai/SYNC_STATE.md | 8 +++++ version.json | 4 +-- 13 files changed, 119 insertions(+), 100 deletions(-) diff --git a/BackupAndProfileTools.cs b/BackupAndProfileTools.cs index e441100..c37890c 100644 --- a/BackupAndProfileTools.cs +++ b/BackupAndProfileTools.cs @@ -736,13 +736,13 @@ private string EnsureNewProfileDirectory(string name) { if (!IsValidProfileName(name)) { - throw new InvalidDataException("프로필 이름은 1~48자로 입력해 주세요."); + throw Localized(new InvalidDataException("프로필 이름은 1~48자로 입력해 주세요."), "A profile name must be 1 to 48 characters."); } string directory = GetProfileDirectory(serversRoot, name); EnsureSafeProfilePath(serversRoot, directory); if (Directory.Exists(directory)) { - throw new IOException("같은 이름의 프로필 폴더가 이미 있습니다."); + throw Localized(new IOException("같은 이름의 프로필 폴더가 이미 있습니다."), "A profile folder with the same name already exists."); } Directory.CreateDirectory(directory); return directory; @@ -793,12 +793,12 @@ private static string CreateComprehensiveServerBackup(string serverDirectory, in string fullServerDirectory = Path.GetFullPath(serverDirectory); if (!Directory.Exists(fullServerDirectory)) { - throw new DirectoryNotFoundException("백업할 서버 폴더를 찾지 못했습니다."); + throw Localized(new DirectoryNotFoundException("백업할 서버 폴더를 찾지 못했습니다."), "The server folder to back up was not found."); } List files = CollectProfileBackupFiles(fullServerDirectory); if (files.Count == 0) { - throw new InvalidOperationException("백업할 서버 파일이 없습니다."); + throw Localized(new InvalidOperationException("백업할 서버 파일이 없습니다."), "There are no server files to back up."); } long totalSize = 0L; for (int i = 0; i < files.Count; i++) @@ -811,7 +811,7 @@ private static string CreateComprehensiveServerBackup(string serverDirectory, in long required = checked(totalSize + 104857600L); if (drive.AvailableFreeSpace < required) { - throw new IOException("백업에 필요한 여유 공간이 부족합니다. 최소 " + FormatBackupSize(required) + "가 필요합니다."); + throw Localized(new IOException("백업에 필요한 여유 공간이 부족합니다. 최소 " + FormatBackupSize(required) + "가 필요합니다."), "Not enough free space for the backup. At least " + FormatBackupSize(required) + " is required."); } string safeReason = string.Equals(reason, "restore-safety", StringComparison.OrdinalIgnoreCase) ? "pre-restore" : string.Equals(reason, "manual", StringComparison.OrdinalIgnoreCase) ? "manual" : "automatic"; string finalPath = Path.Combine(backupDirectory, "server-" + DateTime.Now.ToString("yyyyMMdd-HHmmss-fff") + "-" + safeReason + ".zip"); @@ -850,7 +850,7 @@ private static void CollectProfileBackupFilesRecursive(string root, string direc { if (depth > 128) { - throw new InvalidDataException("서버 폴더 단계가 지나치게 깊습니다."); + throw Localized(new InvalidDataException("서버 폴더 단계가 지나치게 깊습니다."), "The server folder is nested too deeply."); } DirectoryInfo directoryInfo = new DirectoryInfo(directory); if ((directoryInfo.Attributes & FileAttributes.ReparsePoint) != 0) @@ -902,7 +902,7 @@ private static BackupManifestItem AddProfileFileToArchive(ZipArchive archive, st string relative = GetRelativeBackupPath(root, path).Replace('\\', '/'); if (string.IsNullOrEmpty(relative) || relative.StartsWith("../", StringComparison.Ordinal) || relative.IndexOf("/../", StringComparison.Ordinal) >= 0) { - throw new InvalidDataException("백업 파일 경로가 서버 폴더 밖을 가리킵니다."); + throw Localized(new InvalidDataException("백업 파일 경로가 서버 폴더 밖을 가리킵니다."), "A backup file path points outside the server folder."); } ZipArchiveEntry entry = archive.CreateEntry("profile/" + relative, CompressionLevel.Fastest); entry.LastWriteTime = File.GetLastWriteTime(path); @@ -955,7 +955,7 @@ private static List VerifyComprehensiveBackup(string backupP { if (archive.Entries.Count > maximumBackupEntryCount) { - throw new InvalidDataException("백업 항목 수가 안전 제한을 초과했습니다."); + throw Localized(new InvalidDataException("백업 항목 수가 안전 제한을 초과했습니다."), "The number of backup entries exceeds the safety limit."); } items = ReadBackupManifest(archive); Dictionary entries = new Dictionary(StringComparer.OrdinalIgnoreCase); @@ -967,19 +967,19 @@ private static List VerifyComprehensiveBackup(string backupP string relative = entry.FullName.Substring(8).Replace('\\', '/'); ValidateBackupRelativePath(relative); if (entry.FullName.EndsWith("/", StringComparison.Ordinal)) continue; - if (entries.ContainsKey(relative)) throw new InvalidDataException("백업에 중복 파일 경로가 있습니다: " + relative); + if (entries.ContainsKey(relative)) throw Localized(new InvalidDataException("백업에 중복 파일 경로가 있습니다: " + relative), "The backup contains a duplicate file path: " + relative); expandedBytes = checked(expandedBytes + entry.Length); - if (expandedBytes > maximumBackupExpandedBytes) throw new InvalidDataException("백업의 총 해제 크기가 안전 제한을 초과했습니다."); + if (expandedBytes > maximumBackupExpandedBytes) throw Localized(new InvalidDataException("백업의 총 해제 크기가 안전 제한을 초과했습니다."), "The total expanded size of the backup exceeds the safety limit."); entries.Add(relative, entry); } } - if (entries.Count != items.Count) throw new InvalidDataException("백업 파일 목록이 manifest와 정확히 일치하지 않습니다."); + if (entries.Count != items.Count) throw Localized(new InvalidDataException("백업 파일 목록이 manifest와 정확히 일치하지 않습니다."), "The backup file list does not exactly match the manifest."); for (int i = 0; i < items.Count; i++) { ZipArchiveEntry entry; if (!entries.TryGetValue(items[i].RelativePath, out entry) || entry.Length != items[i].Length) { - throw new InvalidDataException("백업 파일 목록이나 크기가 manifest와 다릅니다: " + items[i].RelativePath); + throw Localized(new InvalidDataException("백업 파일 목록이나 크기가 manifest와 다릅니다: " + items[i].RelativePath), "The backup file list or size differs from the manifest: " + items[i].RelativePath); } using (SHA256 sha = SHA256.Create()) using (Stream input = entry.Open()) @@ -987,7 +987,7 @@ private static List VerifyComprehensiveBackup(string backupP string hash = ToLowerHex(sha.ComputeHash(input)); if (!string.Equals(hash, items[i].Sha256, StringComparison.OrdinalIgnoreCase)) { - throw new InvalidDataException("백업 파일의 SHA-256이 일치하지 않습니다: " + items[i].RelativePath); + throw Localized(new InvalidDataException("백업 파일의 SHA-256이 일치하지 않습니다: " + items[i].RelativePath), "A backup file's SHA-256 does not match: " + items[i].RelativePath); } } } @@ -1000,14 +1000,14 @@ private static List VerifyComprehensiveBackup(string backupP FileInfo file = new FileInfo(path); if (!file.Exists || file.Length != items[i].Length) { - throw new InvalidDataException("복원 staging 파일을 검증하지 못했습니다: " + items[i].RelativePath); + throw Localized(new InvalidDataException("복원 staging 파일을 검증하지 못했습니다: " + items[i].RelativePath), "Could not verify a restore staging file: " + items[i].RelativePath); } using (SHA256 sha = SHA256.Create()) using (FileStream stream = file.OpenRead()) { if (!string.Equals(ToLowerHex(sha.ComputeHash(stream)), items[i].Sha256, StringComparison.OrdinalIgnoreCase)) { - throw new InvalidDataException("복원 staging 파일의 SHA-256이 일치하지 않습니다: " + items[i].RelativePath); + throw Localized(new InvalidDataException("복원 staging 파일의 SHA-256이 일치하지 않습니다: " + items[i].RelativePath), "A restore staging file's SHA-256 does not match: " + items[i].RelativePath); } } } @@ -1020,7 +1020,7 @@ private static List ReadBackupManifest(ZipArchive archive) ZipArchiveEntry manifestEntry = archive.GetEntry("backup-manifest.tsv"); if (manifestEntry == null || manifestEntry.Length <= 0 || manifestEntry.Length > 16777216L) { - throw new InvalidDataException("지원되는 백업 manifest를 찾지 못했습니다."); + throw Localized(new InvalidDataException("지원되는 백업 manifest를 찾지 못했습니다."), "No supported backup manifest was found."); } List result = new List(); using (StreamReader reader = new StreamReader(manifestEntry.Open(), Encoding.UTF8)) @@ -1036,7 +1036,7 @@ private static List ReadBackupManifest(ZipArchive archive) long length; if (parts.Length != 3 || parts[0].Length != 64 || !long.TryParse(parts[1], NumberStyles.None, CultureInfo.InvariantCulture, out length) || length < 0) { - throw new InvalidDataException("백업 manifest 항목 형식이 잘못되었습니다."); + throw Localized(new InvalidDataException("백업 manifest 항목 형식이 잘못되었습니다."), "A backup manifest entry has an invalid format."); } string relative; try @@ -1045,7 +1045,7 @@ private static List ReadBackupManifest(ZipArchive archive) } catch (FormatException) { - throw new InvalidDataException("백업 manifest 파일 경로를 해석하지 못했습니다."); + throw Localized(new InvalidDataException("백업 manifest 파일 경로를 해석하지 못했습니다."), "Could not decode a file path in the backup manifest."); } ValidateBackupRelativePath(relative); BackupManifestItem item = new BackupManifestItem(); @@ -1057,7 +1057,7 @@ private static List ReadBackupManifest(ZipArchive archive) } if (result.Count == 0) { - throw new InvalidDataException("백업 manifest에 파일이 없습니다."); + throw Localized(new InvalidDataException("백업 manifest에 파일이 없습니다."), "The backup manifest contains no files."); } return result; } @@ -1068,7 +1068,7 @@ private static void RestoreComprehensiveBackup(string serverDirectory, string ba string parent = Path.GetDirectoryName(fullServer); if (string.IsNullOrEmpty(parent) || !Directory.Exists(fullServer)) { - throw new DirectoryNotFoundException("복원할 프로필 폴더를 찾지 못했습니다."); + throw Localized(new DirectoryNotFoundException("복원할 프로필 폴더를 찾지 못했습니다."), "The profile folder to restore was not found."); } VerifyComprehensiveBackup(backupPath, null); CreateComprehensiveServerBackup(fullServer, retentionCount, "restore-safety"); @@ -1152,13 +1152,13 @@ private static void ExtractComprehensiveBackup(string backupPath, string staging } string key = candidate.FullName.Substring(8).Replace('\\', '/'); ValidateBackupRelativePath(key); - if (entries.ContainsKey(key)) throw new InvalidDataException("백업에 중복 파일 경로가 있습니다: " + key); + if (entries.ContainsKey(key)) throw Localized(new InvalidDataException("백업에 중복 파일 경로가 있습니다: " + key), "The backup contains a duplicate file path: " + key); entries.Add(key, candidate); } for (int i = 0; i < items.Count; i++) { ZipArchiveEntry entry; - if (!entries.TryGetValue(items[i].RelativePath, out entry)) throw new InvalidDataException("manifest 파일을 백업에서 찾지 못했습니다: " + items[i].RelativePath); + if (!entries.TryGetValue(items[i].RelativePath, out entry)) throw Localized(new InvalidDataException("manifest 파일을 백업에서 찾지 못했습니다: " + items[i].RelativePath), "A manifest file was not found in the backup: " + items[i].RelativePath); string relative = items[i].RelativePath.Replace('/', Path.DirectorySeparatorChar); string destination = GetSafeBackupDestination(staging, relative); Directory.CreateDirectory(Path.GetDirectoryName(destination)); @@ -1176,7 +1176,7 @@ private static void ValidateBackupRelativePath(string relative) { if (string.IsNullOrWhiteSpace(relative) || Path.IsPathRooted(relative) || relative.IndexOf('\0') >= 0) { - throw new InvalidDataException("백업에 안전하지 않은 파일 경로가 있습니다."); + throw Localized(new InvalidDataException("백업에 안전하지 않은 파일 경로가 있습니다."), "The backup contains an unsafe file path."); } string normalized = relative.Replace('/', Path.DirectorySeparatorChar); string[] parts = normalized.Split(Path.DirectorySeparatorChar); @@ -1184,7 +1184,7 @@ private static void ValidateBackupRelativePath(string relative) { if (parts[i] == ".." || parts[i].Length == 0) { - throw new InvalidDataException("백업 파일 경로가 서버 폴더 밖을 가리킵니다."); + throw Localized(new InvalidDataException("백업 파일 경로가 서버 폴더 밖을 가리킵니다."), "A backup file path points outside the server folder."); } } } @@ -1196,7 +1196,7 @@ private static string GetSafeBackupDestination(string root, string relative) string candidate = Path.GetFullPath(Path.Combine(root, relative)); if (!candidate.StartsWith(fullRoot, StringComparison.OrdinalIgnoreCase)) { - throw new InvalidDataException("백업 파일 경로가 복원 폴더 밖을 가리킵니다."); + throw Localized(new InvalidDataException("백업 파일 경로가 복원 폴더 밖을 가리킵니다."), "A backup file path points outside the restore folder."); } return candidate; } @@ -1239,7 +1239,7 @@ private static string GetRelativeBackupPath(string root, string path) } if (!fullPath.StartsWith(fullRoot, StringComparison.OrdinalIgnoreCase)) { - throw new InvalidDataException("서버 폴더 밖의 파일은 백업할 수 없습니다."); + throw Localized(new InvalidDataException("서버 폴더 밖의 파일은 백업할 수 없습니다."), "Files outside the server folder cannot be backed up."); } return fullPath.Substring(fullRoot.Length); } @@ -1338,7 +1338,7 @@ private static int FindAvailableServerPort(string serversRoot, int startingPort, return candidate; } } - throw new InvalidOperationException("사용 가능한 서버 포트를 찾지 못했습니다."); + throw Localized(new InvalidOperationException("사용 가능한 서버 포트를 찾지 못했습니다."), "No available server port was found."); } private static void CopyProfileDirectory(string source, string destination) @@ -1347,7 +1347,7 @@ private static void CopyProfileDirectory(string source, string destination) string fullDestination = Path.GetFullPath(destination); if (string.Equals(fullSource, fullDestination, StringComparison.OrdinalIgnoreCase) || fullDestination.StartsWith(fullSource.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)) { - throw new InvalidDataException("서버 폴더 안으로 자기 자신을 복사할 수 없습니다."); + throw Localized(new InvalidDataException("서버 폴더 안으로 자기 자신을 복사할 수 없습니다."), "A folder cannot be copied into itself."); } CopyProfileDirectoryRecursive(fullSource, fullDestination, 0); } @@ -1356,7 +1356,7 @@ private static void CopyProfileDirectoryRecursive(string source, string destinat { if (depth > 128) { - throw new InvalidDataException("서버 폴더 단계가 지나치게 깊습니다."); + throw Localized(new InvalidDataException("서버 폴더 단계가 지나치게 깊습니다."), "The server folder is nested too deeply."); } DirectoryInfo sourceInfo = new DirectoryInfo(source); if ((sourceInfo.Attributes & FileAttributes.ReparsePoint) != 0) @@ -1422,7 +1422,7 @@ private static void EnsurePathInsideRoot(string root, string path) string fullPath = Path.GetFullPath(path).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar; if (!fullPath.StartsWith(fullRoot, StringComparison.OrdinalIgnoreCase) || fullPath.Length <= fullRoot.Length) { - throw new InvalidDataException("대상 경로가 허용된 서버 폴더 밖을 가리킵니다."); + throw Localized(new InvalidDataException("대상 경로가 허용된 서버 폴더 밖을 가리킵니다."), "The destination path points outside the allowed server folder."); } } } diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fc2143..37b3982 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.19.0] - 2026-07-27 + +### Korean + +- **오류 문구 영어 지원 확대 (2/4)**: 백업·복원, 콘텐츠 다운로드, 중복 설정, 명령 브리지, UPnP 외부 접속, 서버 관리 기능의 오류 문구 95개를 한국어·영어로 제공합니다. +- 예외 타입은 그대로 두고 영어 문구만 함께 담는 v1.18.0의 방식을 이어갑니다. 기존 예외 처리 동작에는 영향이 없습니다. + +### English + +- **More error messages available in English (2 of 4)**: 95 messages across backup and restore, content downloads, duplication settings, the command bridge, UPnP external access, and server management features are now available in both Korean and English. +- This continues the v1.18.0 approach of carrying the English wording alongside the unchanged exception type, so existing exception handling behaves exactly as before. ## [1.18.0] - 2026-07-27 ### Korean diff --git a/ContentAndDiagnostics.cs b/ContentAndDiagnostics.cs index 54215d0..64035a2 100644 --- a/ContentAndDiagnostics.cs +++ b/ContentAndDiagnostics.cs @@ -583,7 +583,7 @@ private static string InstallModrinthProject(string projectId, LauncherOptions o { if (depth > 12) { - throw new InvalidDataException("콘텐츠 의존성 단계가 지나치게 깊습니다."); + throw Localized(new InvalidDataException("콘텐츠 의존성 단계가 지나치게 깊습니다."), "The content dependency chain is nested too deeply."); } if (string.IsNullOrEmpty(projectId) || !visited.Add(projectId)) { @@ -622,7 +622,7 @@ private static ModrinthFileInfo GetCompatibleModrinthFile(string projectId, Laun object[] versions = new JavaScriptSerializer().DeserializeObject(DownloadModrinthText(url)) as object[]; if (versions == null || versions.Length == 0) { - throw new InvalidDataException("선택한 서버 버전과 로더에 맞는 콘텐츠 파일을 찾지 못했습니다."); + throw Localized(new InvalidDataException("선택한 서버 버전과 로더에 맞는 콘텐츠 파일을 찾지 못했습니다."), "No content file was found for the selected server version and loader."); } Dictionary selected = null; for (int pass = 0; pass < 2 && selected == null; pass++) @@ -645,7 +645,7 @@ private static ModrinthFileInfo GetCompatibleModrinthFile(string projectId, Laun } if (selected == null) { - throw new InvalidDataException("설치 가능한 콘텐츠 릴리스를 찾지 못했습니다."); + throw Localized(new InvalidDataException("설치 가능한 콘텐츠 릴리스를 찾지 못했습니다."), "No installable content release was found."); } object[] files = selected.ContainsKey("files") ? selected["files"] as object[] : null; Dictionary selectedFile = null; @@ -670,7 +670,7 @@ private static ModrinthFileInfo GetCompatibleModrinthFile(string projectId, Laun } if (selectedFile == null) { - throw new InvalidDataException("콘텐츠 다운로드 파일을 찾지 못했습니다."); + throw Localized(new InvalidDataException("콘텐츠 다운로드 파일을 찾지 못했습니다."), "The downloaded content file was not found."); } ModrinthFileInfo info = new ModrinthFileInfo(); info.ProjectId = projectId; @@ -710,19 +710,19 @@ private static void ValidateModrinthFileInfo(ModrinthFileInfo info) Uri uri; if (string.IsNullOrWhiteSpace(info.FileName) || !info.FileName.EndsWith(".jar", StringComparison.OrdinalIgnoreCase) || Path.GetFileName(info.FileName) != info.FileName) { - throw new InvalidDataException("Modrinth가 안전한 JAR 파일명을 제공하지 않았습니다."); + throw Localized(new InvalidDataException("Modrinth가 안전한 JAR 파일명을 제공하지 않았습니다."), "Modrinth did not provide a safe JAR file name."); } if (!Uri.TryCreate(info.Url, UriKind.Absolute, out uri) || uri.Scheme != Uri.UriSchemeHttps || !uri.Host.Equals("cdn.modrinth.com", StringComparison.OrdinalIgnoreCase)) { - throw new InvalidDataException("Modrinth CDN 다운로드 주소를 검증하지 못했습니다."); + throw Localized(new InvalidDataException("Modrinth CDN 다운로드 주소를 검증하지 못했습니다."), "Could not verify the Modrinth CDN download address."); } if (info.Size <= 0 || info.Size > 536870912L) { - throw new InvalidDataException("콘텐츠 파일 크기가 허용 범위를 벗어났습니다."); + throw Localized(new InvalidDataException("콘텐츠 파일 크기가 허용 범위를 벗어났습니다."), "The content file size is outside the allowed range."); } if ((string.IsNullOrEmpty(info.Sha512) || info.Sha512.Length != 128) && (string.IsNullOrEmpty(info.Sha1) || info.Sha1.Length != 40)) { - throw new InvalidDataException("콘텐츠 파일의 무결성 해시를 확인하지 못했습니다."); + throw Localized(new InvalidDataException("콘텐츠 파일의 무결성 해시를 확인하지 못했습니다."), "Could not verify the content file's integrity hash."); } } @@ -731,7 +731,7 @@ private static string DownloadAndInstallModrinthFile(ModrinthFileInfo file, Laun string folderName = GetContentFolderName(options.ServerType); if (folderName == null) { - throw new InvalidOperationException("이 서버 종류는 콘텐츠 자동 설치를 지원하지 않습니다."); + throw Localized(new InvalidOperationException("이 서버 종류는 콘텐츠 자동 설치를 지원하지 않습니다."), "This server type does not support automatic content installation."); } string folder = Path.Combine(options.ServerDirectory, folderName); Directory.CreateDirectory(folder); @@ -742,7 +742,7 @@ private static string DownloadAndInstallModrinthFile(ModrinthFileInfo file, Laun DownloadModrinthBinary(file.Url, temporary, file.Size); if (!VerifyModrinthHash(temporary, file)) { - throw new InvalidDataException("다운로드한 콘텐츠의 무결성 검증에 실패했습니다."); + throw Localized(new InvalidDataException("다운로드한 콘텐츠의 무결성 검증에 실패했습니다."), "Integrity verification of the downloaded content failed."); } if (File.Exists(destination)) { @@ -766,7 +766,7 @@ private static string DownloadModrinthText(string url) Uri uri; if (!Uri.TryCreate(url, UriKind.Absolute, out uri) || uri.Scheme != Uri.UriSchemeHttps || !uri.Host.Equals("api.modrinth.com", StringComparison.OrdinalIgnoreCase)) { - throw new InvalidDataException("Modrinth API 주소가 안전하지 않습니다."); + throw Localized(new InvalidDataException("Modrinth API 주소가 안전하지 않습니다."), "The Modrinth API address is not safe."); } HttpWebRequest request = (HttpWebRequest)WebRequest.Create(uri); request.Method = "GET"; @@ -780,7 +780,7 @@ private static string DownloadModrinthText(string url) { if (response.StatusCode != HttpStatusCode.OK || response.ResponseUri == null || response.ResponseUri.Scheme != Uri.UriSchemeHttps || !response.ResponseUri.Host.Equals("api.modrinth.com", StringComparison.OrdinalIgnoreCase) || response.ContentLength > 8388608L) { - throw new WebException("Modrinth API가 정상 응답하지 않았습니다."); + throw Localized(new WebException("Modrinth API가 정상 응답하지 않았습니다."), "The Modrinth API did not respond normally."); } using (StreamReader reader = new StreamReader(response.GetResponseStream(), Encoding.UTF8)) { @@ -802,7 +802,7 @@ private static void DownloadModrinthBinary(string url, string path, long expecte { if (response.StatusCode != HttpStatusCode.OK || response.ResponseUri == null || response.ResponseUri.Scheme != Uri.UriSchemeHttps || !response.ResponseUri.Host.Equals("cdn.modrinth.com", StringComparison.OrdinalIgnoreCase) || response.ContentLength > expectedSize) { - throw new WebException("Modrinth CDN이 정상 응답하지 않았습니다."); + throw Localized(new WebException("Modrinth CDN이 정상 응답하지 않았습니다."), "The Modrinth CDN did not respond normally."); } using (Stream input = response.GetResponseStream()) using (FileStream output = new FileStream(path, FileMode.CreateNew, FileAccess.Write, FileShare.None)) @@ -816,13 +816,13 @@ private static void DownloadModrinthBinary(string url, string path, long expecte total = checked(total + read); if (total > expectedSize) { - throw new InvalidDataException("콘텐츠 다운로드 크기가 공식 정보보다 큽니다."); + throw Localized(new InvalidDataException("콘텐츠 다운로드 크기가 공식 정보보다 큽니다."), "The content download is larger than the official metadata states."); } } output.Flush(true); if (total != expectedSize) { - throw new InvalidDataException("콘텐츠 다운로드 크기가 공식 정보와 다릅니다."); + throw Localized(new InvalidDataException("콘텐츠 다운로드 크기가 공식 정보와 다릅니다."), "The content download size differs from the official metadata."); } } } @@ -837,11 +837,11 @@ private static Image DownloadModrinthImage(string url) Uri uri; if (!Uri.TryCreate(url, UriKind.Absolute, out uri) || uri.Scheme != Uri.UriSchemeHttps) { - throw new InvalidDataException("Modrinth 이미지 주소를 검증하지 못했습니다."); + throw Localized(new InvalidDataException("Modrinth 이미지 주소를 검증하지 못했습니다."), "Could not verify the Modrinth image address."); } if (!uri.Host.Equals("cdn.modrinth.com", StringComparison.OrdinalIgnoreCase) && !uri.Host.Equals("wsrv.nl", StringComparison.OrdinalIgnoreCase)) { - throw new InvalidDataException("Modrinth 이미지 주소(호스트)를 검증하지 못했습니다."); + throw Localized(new InvalidDataException("Modrinth 이미지 주소(호스트)를 검증하지 못했습니다."), "Could not verify the Modrinth image address host."); } HttpWebRequest request = (HttpWebRequest)WebRequest.Create(uri); request.Method = "GET"; @@ -855,11 +855,11 @@ private static Image DownloadModrinthImage(string url) { if (response.StatusCode != HttpStatusCode.OK || response.ResponseUri == null || response.ResponseUri.Scheme != Uri.UriSchemeHttps || response.ContentLength > 8388608L) { - throw new WebException("Modrinth 이미지 응답을 검증하지 못했습니다."); + throw Localized(new WebException("Modrinth 이미지 응답을 검증하지 못했습니다."), "Could not verify the Modrinth image response."); } if (!response.ResponseUri.Host.Equals("cdn.modrinth.com", StringComparison.OrdinalIgnoreCase) && !response.ResponseUri.Host.Equals("wsrv.nl", StringComparison.OrdinalIgnoreCase)) { - throw new WebException("Modrinth 이미지 응답 주소를 검증하지 못했습니다."); + throw Localized(new WebException("Modrinth 이미지 응답 주소를 검증하지 못했습니다."), "Could not verify the Modrinth image response address."); } using (Stream input = response.GetResponseStream()) using (MemoryStream buffer = new MemoryStream()) @@ -872,7 +872,7 @@ private static Image DownloadModrinthImage(string url) total = checked(total + read); if (total > 8388608) { - throw new InvalidDataException("Modrinth 아이콘 크기가 허용 범위를 초과했습니다."); + throw Localized(new InvalidDataException("Modrinth 아이콘 크기가 허용 범위를 초과했습니다."), "The Modrinth icon size exceeds the allowed range."); } buffer.Write(block, 0, read); } @@ -880,7 +880,7 @@ private static Image DownloadModrinthImage(string url) using (Image decoded = Image.FromStream(buffer, true, true)) { long pixels = checked((long)decoded.Width * (long)decoded.Height); - if (decoded.Width < 1 || decoded.Height < 1 || decoded.Width > 4096 || decoded.Height > 4096 || pixels > 16777216L) throw new InvalidDataException("Modrinth 아이콘 해상도가 허용 범위를 초과했습니다."); + if (decoded.Width < 1 || decoded.Height < 1 || decoded.Width > 4096 || decoded.Height > 4096 || pixels > 16777216L) throw Localized(new InvalidDataException("Modrinth 아이콘 해상도가 허용 범위를 초과했습니다."), "The Modrinth icon resolution exceeds the allowed range."); return new Bitmap(decoded); } } diff --git a/DuplicationSettings.cs b/DuplicationSettings.cs index ed3edb4..d99c119 100644 --- a/DuplicationSettings.cs +++ b/DuplicationSettings.cs @@ -78,7 +78,7 @@ private static string ApplyDuplicationSettings( { if (pistonDuplication || gravityBlockDuplication || tripwireDuplication) { - throw new NotSupportedException("선택한 서버 종류에는 MineHarbor가 안전하게 적용할 수 있는 Paper 계열 복사 설정이 없습니다."); + throw Localized(new NotSupportedException("선택한 서버 종류에는 MineHarbor가 안전하게 적용할 수 있는 Paper 계열 복사 설정이 없습니다."), "The selected server type has no Paper-family duplication settings that MineHarbor can apply safely."); } return null; } @@ -103,7 +103,7 @@ private static string ApplyDuplicationSettings( || gravityBlockDuplication && !gravitySupported || tripwireDuplication && !tripwireSupported) { - throw new NotSupportedException("선택한 서버 버전에서 지원되지 않는 Paper 복사 설정을 적용할 수 없습니다."); + throw Localized(new NotSupportedException("선택한 서버 버전에서 지원되지 않는 Paper 복사 설정을 적용할 수 없습니다."), "The Paper duplication settings are not supported on the selected server version."); } if (!File.Exists(configurationPath) && !pistonDuplication && !gravityBlockDuplication && !tripwireDuplication) @@ -117,7 +117,7 @@ private static string ApplyDuplicationSettings( FileInfo existing = new FileInfo(configurationPath); if (existing.Length > MaximumDuplicationConfigurationBytes) { - throw new InvalidDataException("Paper 설정 파일이 안전한 크기 제한을 초과했습니다."); + throw Localized(new InvalidDataException("Paper 설정 파일이 안전한 크기 제한을 초과했습니다."), "The Paper settings file exceeds the safe size limit."); } Dictionary current = ReadUnsupportedPaperSettings(configurationPath, modern); if (ManagedDuplicationValuesMatch(current, pistonSupported, gravitySupported, tripwireSupported, pistonDuplication, gravityBlockDuplication, tripwireDuplication)) @@ -261,11 +261,11 @@ private static Dictionary ReadUnsupportedPaperSettings(string path string key; string valueText; if (!TryParseYamlEntry(lines[index], out key, out valueText) || !IsManagedDuplicationKey(key)) continue; - if (result.ContainsKey(key)) throw new InvalidDataException("Paper 설정에 중복된 복사 설정이 있습니다: " + key); + if (result.ContainsKey(key)) throw Localized(new InvalidDataException("Paper 설정에 중복된 복사 설정이 있습니다: " + key), "The Paper settings contain a duplicate duplication setting: " + key); bool value; if (!bool.TryParse(RemoveYamlComment(valueText), out value)) { - throw new InvalidDataException("Paper 복사 설정 값이 true 또는 false가 아닙니다: " + key); + throw Localized(new InvalidDataException("Paper 복사 설정 값이 true 또는 false가 아닙니다: " + key), "A Paper duplication setting value is not true or false: " + key); } result.Add(key, value); } @@ -314,7 +314,7 @@ private static void WriteUnsupportedPaperSettings(string path, bool modern, Dict string key; string valueText; if (!TryParseYamlEntry(lines[index], out key, out valueText) || !desired.ContainsKey(key)) continue; - if (!found.Add(key)) throw new InvalidDataException("Paper 설정에 중복된 복사 설정이 있습니다: " + key); + if (!found.Add(key)) throw Localized(new InvalidDataException("Paper 설정에 중복된 복사 설정이 있습니다: " + key), "The Paper settings contain a duplicate duplication setting: " + key); string comment = GetYamlInlineComment(valueText); lines[index] = new string(' ', location.ChildIndent) + key + ": " + desired[key].ToString().ToLowerInvariant() + comment; } @@ -334,7 +334,7 @@ private static void WriteUnsupportedPaperSettings(string path, bool modern, Dict File.WriteAllLines(temporary, lines.ToArray(), new UTF8Encoding(false)); if (new FileInfo(temporary).Length > MaximumDuplicationConfigurationBytes) { - throw new InvalidDataException("수정된 Paper 설정 파일이 안전한 크기 제한을 초과했습니다."); + throw Localized(new InvalidDataException("수정된 Paper 설정 파일이 안전한 크기 제한을 초과했습니다."), "The modified Paper settings file exceeds the safe size limit."); } ReplaceFile(temporary, path); } @@ -349,11 +349,11 @@ private static string[] ReadSafeYamlLines(string path) FileInfo file = new FileInfo(path); if ((file.Attributes & FileAttributes.ReparsePoint) != 0) { - throw new InvalidDataException("연결된 Paper 설정 파일은 수정할 수 없습니다."); + throw Localized(new InvalidDataException("연결된 Paper 설정 파일은 수정할 수 없습니다."), "A linked Paper settings file cannot be modified."); } if (file.Length > MaximumDuplicationConfigurationBytes) { - throw new InvalidDataException("Paper 설정 파일이 안전한 크기 제한을 초과했습니다."); + throw Localized(new InvalidDataException("Paper 설정 파일이 안전한 크기 제한을 초과했습니다."), "The Paper settings file exceeds the safe size limit."); } return File.ReadAllLines(path, Encoding.UTF8); } @@ -383,8 +383,8 @@ private static YamlSectionLocation FindUnsupportedSettingsSection(string[] lines string parentKey; string parentValue; if (GetYamlIndent(lines[index]) != 0 || !TryParseYamlEntry(lines[index], out parentKey, out parentValue) || parentKey != "settings") continue; - if (location.ParentStart >= 0) throw new InvalidDataException("구형 Paper 설정에 settings 구역이 중복되어 있습니다."); - if (RemoveYamlComment(parentValue).Length != 0) throw new InvalidDataException("구형 Paper settings 구역이 지원하지 않는 인라인 형식입니다."); + if (location.ParentStart >= 0) throw Localized(new InvalidDataException("구형 Paper 설정에 settings 구역이 중복되어 있습니다."), "The legacy Paper settings contain a duplicate settings section."); + if (RemoveYamlComment(parentValue).Length != 0) throw Localized(new InvalidDataException("구형 Paper settings 구역이 지원하지 않는 인라인 형식입니다."), "The legacy Paper settings section uses an unsupported inline format."); location.ParentStart = index; } if (location.ParentStart < 0) @@ -407,10 +407,10 @@ private static YamlSectionLocation FindUnsupportedSettingsSection(string[] lines string key; string valueText; if (indent != location.SectionIndent || !TryParseYamlEntry(lines[index], out key, out valueText) || key != "unsupported-settings") continue; - if (location.SectionStart >= 0) throw new InvalidDataException("Paper 설정에 unsupported-settings 구역이 중복되어 있습니다."); + if (location.SectionStart >= 0) throw Localized(new InvalidDataException("Paper 설정에 unsupported-settings 구역이 중복되어 있습니다."), "The Paper settings contain a duplicate unsupported-settings section."); if (RemoveYamlComment(valueText).Length != 0) { - throw new InvalidDataException("Paper unsupported-settings 구역이 지원하지 않는 인라인 형식입니다."); + throw Localized(new InvalidDataException("Paper unsupported-settings 구역이 지원하지 않는 인라인 형식입니다."), "The Paper unsupported-settings section uses an unsupported inline format."); } location.SectionStart = index; } @@ -463,7 +463,7 @@ private static int GetYamlIndent(string line) while (indent < line.Length && line[indent] == ' ') indent++; if (indent < line.Length && line[indent] == '\t') { - throw new InvalidDataException("탭 들여쓰기가 있는 Paper 설정은 안전하게 수정할 수 없습니다."); + throw Localized(new InvalidDataException("탭 들여쓰기가 있는 Paper 설정은 안전하게 수정할 수 없습니다."), "Paper settings indented with tabs cannot be modified safely."); } return indent; } @@ -507,7 +507,7 @@ private static void EnsureSafeDuplicationConfigurationPath(string serverDirector string prefix = root + Path.DirectorySeparatorChar; if (!candidate.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) { - throw new InvalidDataException("Paper 설정 경로가 서버 폴더 밖을 가리킵니다."); + throw Localized(new InvalidDataException("Paper 설정 경로가 서버 폴더 밖을 가리킵니다."), "The Paper settings path points outside the server folder."); } DirectoryInfo current = new DirectoryInfo(Path.GetDirectoryName(candidate)); @@ -515,7 +515,7 @@ private static void EnsureSafeDuplicationConfigurationPath(string serverDirector { if (current.Exists && (current.Attributes & FileAttributes.ReparsePoint) != 0) { - throw new InvalidDataException("연결된 폴더 안의 Paper 설정은 수정할 수 없습니다."); + throw Localized(new InvalidDataException("연결된 폴더 안의 Paper 설정은 수정할 수 없습니다."), "Paper settings inside a linked folder cannot be modified."); } if (string.Equals(current.FullName.TrimEnd(Path.DirectorySeparatorChar), root, StringComparison.OrdinalIgnoreCase)) break; current = current.Parent; diff --git a/ManagedServerDashboard.cs b/ManagedServerDashboard.cs index 4235701..dc43210 100644 --- a/ManagedServerDashboard.cs +++ b/ManagedServerDashboard.cs @@ -763,7 +763,7 @@ private void StartSession(ManagedProfileRecord profile, bool automaticRestart) { if (!process.Start()) { - throw new InvalidOperationException("관리 서버 프로세스를 시작하지 못했습니다."); + throw Localized(new InvalidOperationException("관리 서버 프로세스를 시작하지 못했습니다."), "Could not start the managed server process."); } session.Process = process; TryRecordOperationEvent( diff --git a/ModernLauncherGui.cs b/ModernLauncherGui.cs index f80ebf0..069d265 100644 --- a/ModernLauncherGui.cs +++ b/ModernLauncherGui.cs @@ -1053,7 +1053,7 @@ private static LauncherOptions ConfigureServerPropertiesGui(string serversRootDi { - throw new OperationCanceledException("서버 설정이 취소되었습니다."); + throw Localized(new OperationCanceledException("서버 설정이 취소되었습니다."), "Server setup was cancelled."); } @@ -4181,7 +4181,7 @@ private void ShowModelessToolWindow(string key, Func
factory, bool blocksS Form form = factory(); - if (form == null) throw new InvalidOperationException("기능 창을 만들지 못했습니다."); + if (form == null) throw Localized(new InvalidOperationException("기능 창을 만들지 못했습니다."), "Could not create the feature window."); modelessToolWindows[key] = form; diff --git a/QuickCommandsAndBridge.cs b/QuickCommandsAndBridge.cs index 5dc876d..2cc68b4 100644 --- a/QuickCommandsAndBridge.cs +++ b/QuickCommandsAndBridge.cs @@ -1406,29 +1406,29 @@ private static BridgeReleaseInfo GetBridgeReleaseInfo() string json = DownloadTextWithUserAgent(GetLauncherUpdateMetadataUrl(), "MineHarbor/0.4", GetGitHubReleaseDownloadHosts(), LauncherUpdateMaximumMetadataCharacters); Dictionary root = new JavaScriptSerializer().DeserializeObject(json) as Dictionary; Dictionary bridge = root != null && root.ContainsKey("bridge") ? root["bridge"] as Dictionary : null; - if (bridge == null) throw new InvalidDataException("업데이트 정보에 명령 브리지 자산이 없습니다."); + if (bridge == null) throw Localized(new InvalidDataException("업데이트 정보에 명령 브리지 자산이 없습니다."), "The update metadata contains no command bridge asset."); BridgeReleaseInfo info = new BridgeReleaseInfo(); info.Version = Convert.ToString(bridge["version"], CultureInfo.InvariantCulture); info.Protocol = Convert.ToInt32(bridge["protocol"], CultureInfo.InvariantCulture); info.MinimumMinecraft = Convert.ToString(bridge["minimum_minecraft"], CultureInfo.InvariantCulture); info.MaximumMinecraft = Convert.ToString(bridge["maximum_minecraft"], CultureInfo.InvariantCulture); info.Url = Convert.ToString(bridge["download_url"], CultureInfo.InvariantCulture); info.Sha256 = Convert.ToString(bridge["sha256"], CultureInfo.InvariantCulture); info.Size = Convert.ToInt64(bridge["size"], CultureInfo.InvariantCulture); string releaseVersion = root != null && root.ContainsKey("version") ? Convert.ToString(root["version"], CultureInfo.InvariantCulture) : string.Empty; Version parsedBridgeVersion; - if (info.Protocol != CommandBridgeProtocolVersion || info.Size < 1024 || info.Size > 536870912L || !IsValidSha256(info.Sha256) || !TryParseProductVersion(info.Version, out parsedBridgeVersion) || !string.Equals(releaseVersion, info.Version, StringComparison.Ordinal) || !IsAllowedBridgeDownloadUrl(info.Url, info.Version)) throw new InvalidDataException("명령 브리지 메타데이터를 검증하지 못했습니다."); + if (info.Protocol != CommandBridgeProtocolVersion || info.Size < 1024 || info.Size > 536870912L || !IsValidSha256(info.Sha256) || !TryParseProductVersion(info.Version, out parsedBridgeVersion) || !string.Equals(releaseVersion, info.Version, StringComparison.Ordinal) || !IsAllowedBridgeDownloadUrl(info.Url, info.Version)) throw Localized(new InvalidDataException("명령 브리지 메타데이터를 검증하지 못했습니다."), "Could not verify the command bridge metadata."); return info; } private static bool InstallOrUpdateCommandBridge(string serverDirectory, string serverType, string minecraftVersion) { - if (!IsCommandBridgeSupported(serverType, minecraftVersion)) throw new InvalidOperationException("이 서버 종류 또는 버전은 Paper/Purpur 명령 브리지를 지원하지 않습니다."); + if (!IsCommandBridgeSupported(serverType, minecraftVersion)) throw Localized(new InvalidOperationException("이 서버 종류 또는 버전은 Paper/Purpur 명령 브리지를 지원하지 않습니다."), "This server type or version does not support the Paper/Purpur command bridge."); BridgeReleaseInfo release = GetBridgeReleaseInfo(); - if (!IsMinecraftVersionInBridgeRange(minecraftVersion, release.MinimumMinecraft, release.MaximumMinecraft)) throw new InvalidOperationException("선택한 Minecraft 버전은 이 명령 브리지 자산과 호환되지 않습니다."); + if (!IsMinecraftVersionInBridgeRange(minecraftVersion, release.MinimumMinecraft, release.MaximumMinecraft)) throw Localized(new InvalidOperationException("선택한 Minecraft 버전은 이 명령 브리지 자산과 호환되지 않습니다."), "The selected Minecraft version is not compatible with this command bridge asset."); string destination = GetBridgeJarPath(serverDirectory); string managedPath = GetBridgeManagedPath(serverDirectory); BridgeManagedInfo current = ReadBridgeManagedInfo(serverDirectory); - if (File.Exists(destination) && current == null) throw new InvalidOperationException("같은 이름의 사용자 JAR이 있어 덮어쓰지 않았습니다."); - if (File.Exists(destination) && current != null && !string.Equals(GetFileSha256(destination), current.Sha256, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("관리 중인 브리지 JAR이 설치 후 변경되어 자동으로 덮어쓰지 않았습니다."); + if (File.Exists(destination) && current == null) throw Localized(new InvalidOperationException("같은 이름의 사용자 JAR이 있어 덮어쓰지 않았습니다."), "A user JAR with the same name exists, so it was not overwritten."); + if (File.Exists(destination) && current != null && !string.Equals(GetFileSha256(destination), current.Sha256, StringComparison.OrdinalIgnoreCase)) throw Localized(new InvalidOperationException("관리 중인 브리지 JAR이 설치 후 변경되어 자동으로 덮어쓰지 않았습니다."), "The managed bridge JAR changed after installation, so it was not overwritten automatically."); Directory.CreateDirectory(Path.GetDirectoryName(destination)); string temporary = destination + ".다운로드중"; string backup = destination + ".이전"; DeleteFileIfPresent(temporary); DeleteFileIfPresent(backup); try { if (!string.IsNullOrEmpty(BridgeArtifactOverridePath) && File.Exists(BridgeArtifactOverridePath)) File.Copy(BridgeArtifactOverridePath, temporary, true); else DownloadFileWithUserAgent(release.Url, temporary, "MineHarbor/0.4", GetGitHubReleaseDownloadHosts()); - if (!ValidateCommandBridgeArtifact(temporary, release.Size, release.Sha256)) throw new InvalidDataException("명령 브리지 JAR 크기 또는 SHA-256 검증에 실패했습니다."); + if (!ValidateCommandBridgeArtifact(temporary, release.Size, release.Sha256)) throw Localized(new InvalidDataException("명령 브리지 JAR 크기 또는 SHA-256 검증에 실패했습니다."), "Command bridge JAR size or SHA-256 verification failed."); if (File.Exists(destination)) File.Copy(destination, backup, true); - if (BridgeInstallFailureAfterBackup) throw new IOException("브리지 업데이트 복구 테스트 오류"); + if (BridgeInstallFailureAfterBackup) throw Localized(new IOException("브리지 업데이트 복구 테스트 오류"), "Bridge update rollback test error"); ReplaceFile(temporary, destination); BridgeManagedInfo managed = new BridgeManagedInfo(); managed.JarName = Path.GetFileName(destination); managed.Version = release.Version; managed.Protocol = release.Protocol; managed.Sha256 = release.Sha256; managed.InstalledUtc = DateTime.UtcNow.ToString("o", CultureInfo.InvariantCulture); WriteJsonAtomic(managedPath, managed); DeleteFileIfPresent(backup); return true; } @@ -1462,8 +1462,8 @@ private static bool RemoveManagedCommandBridge(string serverDirectory, bool remo { BridgeManagedInfo managed = ReadBridgeManagedInfo(serverDirectory); if (managed == null) return false; string jar = Path.Combine(Path.Combine(serverDirectory, "plugins"), managed.JarName ?? string.Empty); - if (!string.Equals(Path.GetFullPath(jar), Path.GetFullPath(GetBridgeJarPath(serverDirectory)), StringComparison.OrdinalIgnoreCase)) throw new InvalidDataException("관리 대상 브리지 경로가 올바르지 않습니다."); - if (File.Exists(jar) && !string.Equals(GetFileSha256(jar), managed.Sha256, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("브리지 JAR이 설치 후 변경되어 자동 삭제하지 않았습니다."); + if (!string.Equals(Path.GetFullPath(jar), Path.GetFullPath(GetBridgeJarPath(serverDirectory)), StringComparison.OrdinalIgnoreCase)) throw Localized(new InvalidDataException("관리 대상 브리지 경로가 올바르지 않습니다."), "The managed bridge path is invalid."); + if (File.Exists(jar) && !string.Equals(GetFileSha256(jar), managed.Sha256, StringComparison.OrdinalIgnoreCase)) throw Localized(new InvalidOperationException("브리지 JAR이 설치 후 변경되어 자동 삭제하지 않았습니다."), "The bridge JAR changed after installation, so it was not deleted automatically."); DeleteFileIfPresent(jar); DeleteFileIfPresent(GetBridgeManagedPath(serverDirectory)); if (removeData) { string data = Path.Combine(Path.Combine(serverDirectory, "plugins"), "MinecraftServerLauncherCommandBridge"); if (Directory.Exists(data)) Directory.Delete(data, true); } return true; @@ -1530,8 +1530,8 @@ private static void WriteBridgeSessionFile(string serverDirectory, string profil internal static void DeleteBridgeSessionFile(string serverDirectory) { try { DeleteFileIfPresent(Path.Combine(serverDirectory, CommandBridgeSessionFileName)); } catch (Exception ex) { Console.WriteLine("[Bridge] 세션 파일 삭제 실패: " + ex.Message); } } private static void WriteJsonAtomic(string path, object value) { Directory.CreateDirectory(Path.GetDirectoryName(path)); string temporary = path + ".준비중"; File.WriteAllText(temporary, new JavaScriptSerializer().Serialize(value), new UTF8Encoding(false)); ReplaceFile(temporary, path); } - private static string ReadLimitedLine(StreamReader reader, int maximum) { StringBuilder result = new StringBuilder(); while (true) { int value = reader.Read(); if (value < 0) return result.Length == 0 ? null : result.ToString(); char character = (char)value; if (character == '\n') return result.ToString(); if (character != '\r') result.Append(character); if (result.Length > maximum) throw new InvalidDataException("브리지 요청 크기 제한을 초과했습니다."); } } - private static Dictionary DeserializeBridgeObject(string json) { if (string.IsNullOrEmpty(json) || json.Length > CommandBridgeMaximumLineLength) throw new InvalidDataException("브리지 JSON 크기가 올바르지 않습니다."); Dictionary value = new JavaScriptSerializer().DeserializeObject(json) as Dictionary; if (value == null || string.IsNullOrEmpty(BridgeString(value, "type")) || string.IsNullOrEmpty(BridgeString(value, "id"))) throw new InvalidDataException("브리지 JSON 형식이 올바르지 않습니다."); return value; } + private static string ReadLimitedLine(StreamReader reader, int maximum) { StringBuilder result = new StringBuilder(); while (true) { int value = reader.Read(); if (value < 0) return result.Length == 0 ? null : result.ToString(); char character = (char)value; if (character == '\n') return result.ToString(); if (character != '\r') result.Append(character); if (result.Length > maximum) throw Localized(new InvalidDataException("브리지 요청 크기 제한을 초과했습니다."), "The bridge request exceeds the size limit."); } } + private static Dictionary DeserializeBridgeObject(string json) { if (string.IsNullOrEmpty(json) || json.Length > CommandBridgeMaximumLineLength) throw Localized(new InvalidDataException("브리지 JSON 크기가 올바르지 않습니다."), "The bridge JSON size is invalid."); Dictionary value = new JavaScriptSerializer().DeserializeObject(json) as Dictionary; if (value == null || string.IsNullOrEmpty(BridgeString(value, "type")) || string.IsNullOrEmpty(BridgeString(value, "id"))) throw Localized(new InvalidDataException("브리지 JSON 형식이 올바르지 않습니다."), "The bridge JSON format is invalid."); return value; } private static string BridgeString(Dictionary value, string key) { return value != null && value.ContainsKey(key) ? Convert.ToString(value[key], CultureInfo.InvariantCulture) ?? string.Empty : string.Empty; } private static int BridgeInt(Dictionary value, string key) { int result; return int.TryParse(BridgeString(value, key), NumberStyles.Integer, CultureInfo.InvariantCulture, out result) ? result : 0; } private static bool TryBridgeMetric(Dictionary value, string key, out double result) { return double.TryParse(BridgeString(value, key), NumberStyles.Float, CultureInfo.InvariantCulture, out result) && !double.IsNaN(result) && !double.IsInfinity(result) && result >= 0.0; } diff --git a/README.md b/README.md index a3d4cc8..c282dbc 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.18.0`, 내부 빌드는 `26.2.45.86`입니다. MineHarbor 이름으로 배포된 Portable EXE는 같은 링크에서 계속 최신 파일을 받을 수 있습니다. 기존 설치의 `%LOCALAPPDATA%\MinecraftServerLauncher` 데이터는 자동으로 찾아 그대로 사용하며, 새 사용자 데이터 경로는 `%LOCALAPPDATA%\MineHarbor`입니다. +현재 소스 버전은 `v1.19.0`, 내부 빌드는 `26.2.45.87`입니다. MineHarbor 이름으로 배포된 Portable EXE는 같은 링크에서 계속 최신 파일을 받을 수 있습니다. 기존 설치의 `%LOCALAPPDATA%\MinecraftServerLauncher` 데이터는 자동으로 찾아 그대로 사용하며, 새 사용자 데이터 경로는 `%LOCALAPPDATA%\MineHarbor`입니다. -이 README는 로드맵이 아니라 현재 `v1.18.0` 소스와 자동 테스트, 공개 Release 자산에서 확인한 기능만 설명합니다. 서버 종류나 브리지 연결처럼 조건에 따라 달라지는 기능과 지원되지 않는 상태는 아래에 따로 표시합니다. +이 README는 로드맵이 아니라 현재 `v1.19.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.18.0` · internal build: `26.2.45.86`. 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.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`. -This README documents shipped behavior verified against the current `v1.18.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.19.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/ServerManagementFeatures.cs b/ServerManagementFeatures.cs index 61176ae..ed46f96 100644 --- a/ServerManagementFeatures.cs +++ b/ServerManagementFeatures.cs @@ -250,7 +250,7 @@ private async Task ExecuteAutomationJobAsync(AutomationJobClaim claim, Cancellat try { ManagedProfileRecord profile = profiles.Find(delegate(ManagedProfileRecord item) { return string.Equals(Path.GetFullPath(item.Directory), claim.ServerDirectory, StringComparison.OrdinalIgnoreCase); }); - if (profile == null) throw new InvalidOperationException("예약 작업의 서버 프로필을 찾지 못했습니다."); + if (profile == null) throw Localized(new InvalidOperationException("예약 작업의 서버 프로필을 찾지 못했습니다."), "The server profile for the scheduled job was not found."); ServerAutomationConfiguration configuration = ReadServerAutomationConfiguration(profile.Directory); ManagedServerSession session; sessions.TryGetValue(profile.Name, out session); @@ -283,7 +283,7 @@ private async Task ExecuteAutomationJobAsync(AutomationJobClaim claim, Cancellat else { ValidateScheduledCommand(claim.Job.Command); - if (session == null || !IsManagedSessionRunning(session)) throw new InvalidOperationException("예약 명령을 실행할 서버가 꺼져 있습니다."); + if (session == null || !IsManagedSessionRunning(session)) throw Localized(new InvalidOperationException("예약 명령을 실행할 서버가 꺼져 있습니다."), "The server for the scheduled command is stopped."); SendManagedCommand(session, claim.Job.Command); result = ManagedText("명령 전송 완료", "Command sent"); } @@ -305,7 +305,7 @@ private static async Task AnnounceAutomationActionAsync(ManagedServerSession ses private static void SendManagedCommand(ManagedServerSession session, string command) { - if (session == null || !IsManagedSessionRunning(session)) throw new InvalidOperationException("서버가 실행 중이 아닙니다."); + if (session == null || !IsManagedSessionRunning(session)) throw Localized(new InvalidOperationException("서버가 실행 중이 아닙니다."), "The server is not running."); lock (session.SyncRoot) { session.Process.StandardInput.WriteLine(command); @@ -433,7 +433,7 @@ private async void ObserveMainAutomationJobAsync(AutomationJobClaim claim) else if (string.Equals(claim.Job.Action, "command", StringComparison.OrdinalIgnoreCase)) { ValidateScheduledCommand(claim.Job.Command); - if (!serverRunning || !SendServerCommand(claim.Job.Command)) throw new InvalidOperationException("예약 명령을 실행할 서버가 꺼져 있습니다."); + if (!serverRunning || !SendServerCommand(claim.Job.Command)) throw Localized(new InvalidOperationException("예약 명령을 실행할 서버가 꺼져 있습니다."), "The server for the scheduled command is stopped."); result = ManagedText("명령 전송 완료", "Command sent"); } else @@ -452,7 +452,7 @@ private async void ObserveMainAutomationJobAsync(AutomationJobClaim claim) await Task.Delay(TimeSpan.FromSeconds(claim.Job.WarningSeconds), mainAutomationCancellation.Token); } mainScheduledRestart = restart; - if (!SendServerCommand("stop")) throw new InvalidOperationException("서버에 종료 명령을 보내지 못했습니다."); + if (!SendServerCommand("stop")) throw Localized(new InvalidOperationException("서버에 종료 명령을 보내지 못했습니다."), "Could not send the stop command to the server."); result = restart ? ManagedText("재시작 요청 완료", "Restart requested") : ManagedText("종료 요청 완료", "Stop requested"); } } diff --git a/UpnpCore.cs b/UpnpCore.cs index a802da6..4f0e377 100644 --- a/UpnpCore.cs +++ b/UpnpCore.cs @@ -424,7 +424,7 @@ private async Task GetSpecificMappingAsync(UpnpServiceInfo serv int internalPort; if (!int.TryParse(ReadSoapValue(responseBody, "NewInternalPort"), NumberStyles.None, CultureInfo.InvariantCulture, out internalPort)) { - throw new InvalidDataException("공유기의 포트 매핑 응답에서 내부 포트를 확인하지 못했습니다."); + throw Localized(new InvalidDataException("공유기의 포트 매핑 응답에서 내부 포트를 확인하지 못했습니다."), "Could not read the internal port from the router's port-mapping response."); } SoapMappingInfo mapping = new SoapMappingInfo(); mapping.InternalPort = internalPort; diff --git a/UpnpExternalAccess.cs b/UpnpExternalAccess.cs index 13cd57d..f70232f 100644 --- a/UpnpExternalAccess.cs +++ b/UpnpExternalAccess.cs @@ -271,7 +271,7 @@ private static ExternalPortCheckResult CheckExternalPort(int port, WaitHandle st IPAddress parsed; if (!IPAddress.TryParse(result.PublicIp, out parsed) || IPAddress.IsLoopback(parsed)) { - throw new InvalidDataException("외부 검사 서버의 공인 IP 응답을 인식할 수 없습니다."); + throw Localized(new InvalidDataException("외부 검사 서버의 공인 IP 응답을 인식할 수 없습니다."), "The public IP response from the external check service was not recognized."); } for (int attempt = 1; attempt <= Math.Max(1, attempts); attempt++) { @@ -283,7 +283,7 @@ private static ExternalPortCheckResult CheckExternalPort(int port, WaitHandle st bool reachable; if (!bool.TryParse(response, out reachable)) { - throw new InvalidDataException("외부 검사 서버의 포트 응답을 인식할 수 없습니다."); + throw Localized(new InvalidDataException("외부 검사 서버의 포트 응답을 인식할 수 없습니다."), "The port response from the external check service was not recognized."); } result.CheckCompleted = true; if (reachable) @@ -311,7 +311,7 @@ private static string DownloadExternalCheckText(string url, WaitHandle stopped, { if (stopped.WaitOne(0)) { - throw new OperationCanceledException("서버가 종료되어 외부 접속 검사를 중단했습니다."); + throw Localized(new OperationCanceledException("서버가 종료되어 외부 접속 검사를 중단했습니다."), "The external access check stopped because the server shut down."); } try { @@ -325,7 +325,7 @@ private static string DownloadExternalCheckText(string url, WaitHandle stopped, Console.WriteLine("[외부 접속] 검사 서비스 연결 재시도 " + (attempt + 1).ToString(CultureInfo.InvariantCulture) + "/" + attempts.ToString(CultureInfo.InvariantCulture)); if (stopped.WaitOne(attempt * 1000)) { - throw new OperationCanceledException("서버가 종료되어 외부 접속 검사를 중단했습니다."); + throw Localized(new OperationCanceledException("서버가 종료되어 외부 접속 검사를 중단했습니다."), "The external access check stopped because the server shut down."); } } } @@ -721,7 +721,7 @@ private static void PersistTrackedUpnpMapping(int externalPort, int internalPort UpnpMappedPort item = tracked[i]; if (item.ExternalPort == externalPort && string.Equals(item.Protocol, protocol, StringComparison.OrdinalIgnoreCase) && IsTrackedOwnerAlive(item) && !string.Equals(item.Description, description, StringComparison.Ordinal)) { - throw new InvalidOperationException("다른 MineHarbor 실행이 같은 UPnP 포트의 소유권을 기록하고 있습니다."); + throw Localized(new InvalidOperationException("다른 MineHarbor 실행이 같은 UPnP 포트의 소유권을 기록하고 있습니다."), "Another MineHarbor run holds ownership of the same UPnP port."); } } tracked.RemoveAll(delegate(UpnpMappedPort item) @@ -1000,7 +1000,7 @@ private static void EnterUpnpTrackerLock() { try { - if (!upnpTrackerMutex.WaitOne(10000)) throw new TimeoutException("다른 MineHarbor 프로세스가 UPnP 소유권 기록을 사용 중입니다."); + if (!upnpTrackerMutex.WaitOne(10000)) throw Localized(new TimeoutException("다른 MineHarbor 프로세스가 UPnP 소유권 기록을 사용 중입니다."), "Another MineHarbor process is using the UPnP ownership record."); } catch (AbandonedMutexException) { diff --git a/docs/ai/SYNC_STATE.md b/docs/ai/SYNC_STATE.md index d986003..7ce303e 100644 --- a/docs/ai/SYNC_STATE.md +++ b/docs/ai/SYNC_STATE.md @@ -1,5 +1,13 @@ # AI Agent Synchronization State +## Claude Exception Localization 2/4 - 2026-07-27 + +- **Current Version**: 1.19.0 (build 26.2.45.87) +- **Branch**: `claude/remote-control-u4tvpt` +- **Status**: 예외 메시지 이중화 2차분 95곳 적용 +- 변환: `BackupAndProfileTools`(30), `ContentAndDiagnostics`(21), `DuplicationSettings`(16), `QuickCommandsAndBridge`(13), `UpnpExternalAccess`(6), `ServerManagementFeatures`(5), `ModernLauncherGui`(2), `UpnpCore`(1), `ManagedServerDashboard`(1). +- 문자열이 이어붙는 형태(`"...: " + key`)는 영어 쪽도 같은 값을 이어붙이도록 개별 변환했습니다. +- **남은 작업**: `ContentManagementServices`(60), `RuntimeCompatibility`(45) → 3차분. `decompiled/Launcher.decompiled.cs`(67) → 4차분(줄바꿈이 CRLF/CR/LF로 섞여 있어 바이트 단위 편집 필요). ## Claude Exception Message Localization - 2026-07-27 - **Current Version**: 1.18.0 (build 26.2.45.86) diff --git a/version.json b/version.json index 8808677..29e44ec 100644 --- a/version.json +++ b/version.json @@ -1,5 +1,5 @@ { - "productVersion": "1.18.0", - "buildNumber": "26.2.45.86", + "productVersion": "1.19.0", + "buildNumber": "26.2.45.87", "minimumSupportedVersion": "0.1.0" }