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" }