diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9eae7c3 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,53 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +env: + DOTNET_NOLOGO: true + DOTNET_CLI_TELEMETRY_OPTOUT: true + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + +jobs: + build-and-test: + name: Build & Test (.NET 8, Windows) + runs-on: windows-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET 8 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Cache NuGet packages + uses: actions/cache@v4 + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('src/**/*.csproj') }} + restore-keys: | + ${{ runner.os }}-nuget- + + - name: Restore + run: dotnet restore src/SplitWireTurkey.sln + + - name: Build (Release) + run: dotnet build src/SplitWireTurkey.sln --configuration Release --no-restore + + - name: Test + run: dotnet test src/SplitWireTurkey.Tests/SplitWireTurkey.Tests.csproj --configuration Release --no-build --logger "trx;LogFileName=test-results.trx" --results-directory ./TestResults + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results + path: ./TestResults/**/*.trx + retention-days: 14 + if-no-files-found: ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ec215f4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +# Build outputs +[Bb]in/ +[Oo]bj/ +[Pp]ublish/ +[Dd]ebug/ +[Rr]elease/ +*.user +*.suo +*.userprefs + +# Test results +TestResults/ +coverage*.xml +*.trx +*.coverage +*.coveragexml + +# NuGet +.nuget/ +*.nupkg +*.snupkg + +# Visual Studio / Rider +.vs/ +.vscode/ +.idea/ +*.sln.docstates + +# OS +Thumbs.db +.DS_Store +desktop.ini + +# Logs +*.log diff --git a/README.md b/README.md index 7b78660..a583059 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,8 @@ [![RU](https://img.shields.io/badge/README-RU-blue.svg)](https://github.com/cagritaskn/SplitWire-Turkey/blob/main/.github/README_RU.md) [![ES](https://img.shields.io/badge/README-ES-blue.svg)](https://github.com/cagritaskn/SplitWire-Turkey/blob/main/.github/README_ES.md) +[![CI](https://github.com/cagritaskn/SplitWire-Turkey/actions/workflows/ci.yml/badge.svg)](https://github.com/cagritaskn/SplitWire-Turkey/actions/workflows/ci.yml) + # SplitWire-Turkey @@ -329,6 +331,18 @@ Gereksinimler: ..\build_simple.bat ``` +4. **Testleri Çalıştırın (opsiyonel)** + ```bash + # Çözüm dosyasından + dotnet test ..\SplitWireTurkey.sln -c Release + + # Sadece test projesi + dotnet test ..\SplitWireTurkey.Tests\SplitWireTurkey.Tests.csproj + ``` + `VersionHelper` ve `LanguageManager` davranışı için xUnit testleri + `src/SplitWireTurkey.Tests/` altındadır. Aynı paketler GitHub Actions'taki + CI workflow'u tarafından her PR'da otomatik olarak çalıştırılır. + ### InnoSetup Kullanarak Kurulum Yürütülebilirini Tekrar Derleme Gereksinimler: - **InnoSetup 6** diff --git a/src/SplitWireTurkey.Tests/LanguageManagerTests.cs b/src/SplitWireTurkey.Tests/LanguageManagerTests.cs new file mode 100644 index 0000000..42b391d --- /dev/null +++ b/src/SplitWireTurkey.Tests/LanguageManagerTests.cs @@ -0,0 +1,109 @@ +using SplitWireTurkey; +using Xunit; + +namespace SplitWireTurkey.Tests +{ + /// + /// LanguageManager is a static class — all tests live in a single + /// non-parallel collection so the shared state is deterministic. + /// + [Collection("LanguageManager")] + public class LanguageManagerTests + { + [Fact] + public void LoadLanguage_WithSupportedCode_ReturnsTrue() + { + Assert.True(LanguageManager.LoadLanguage("TR")); + Assert.Equal("TR", LanguageManager.CurrentLanguage); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void LoadLanguage_WithNullOrBlank_ReturnsFalseAndDoesNotThrow(string code) + { + // Must never throw NullReferenceException / ArgumentNullException. + var result = LanguageManager.LoadLanguage(code); + Assert.False(result); + } + + [Fact] + public void LoadLanguage_WithUnknownCode_FallsBackAndStillExposesKeys() + { + // Unknown language file does not exist on disk, but the + // Turkish fallback dictionary should still resolve common keys. + var ok = LanguageManager.LoadLanguage("XX"); + Assert.True(ok); + + var translated = LanguageManager.GetText("buttons", "exit"); + Assert.NotEqual("buttons.exit", translated); + Assert.False(string.IsNullOrWhiteSpace(translated)); + } + + [Fact] + public void GetText_TopLevelKey_ReturnsLocalizedString() + { + LanguageManager.LoadLanguage("EN"); + // No top-level scalar keys exist by default, so this also + // verifies that an unknown flat key returns the key itself. + Assert.Equal("nonexistent_top_level_key", + LanguageManager.GetText("nonexistent_top_level_key")); + } + + [Fact] + public void GetText_NestedKey_ReturnsLocalizedString_ForTurkish() + { + LanguageManager.LoadLanguage("TR"); + + Assert.Equal("WireSock", LanguageManager.GetText("tabs", "main")); + Assert.Equal("Çıkış", LanguageManager.GetText("buttons", "exit")); + } + + [Fact] + public void GetText_NestedKey_ReturnsLocalizedString_ForEnglish() + { + LanguageManager.LoadLanguage("EN"); + + Assert.Equal("Exit", LanguageManager.GetText("buttons", "exit")); + Assert.Equal("Administrator Privileges Required", + LanguageManager.GetText("messages", "admin_required_title")); + } + + [Fact] + public void GetText_UnknownNestedKey_ReturnsCategoryDotKey() + { + LanguageManager.LoadLanguage("TR"); + + Assert.Equal("buttons.does_not_exist", + LanguageManager.GetText("buttons", "does_not_exist")); + Assert.Equal("no_such_category.foo", + LanguageManager.GetText("no_such_category", "foo")); + } + + [Fact] + public void GetText_NullKey_ReturnsNull() + { + LanguageManager.LoadLanguage("TR"); + + Assert.Null(LanguageManager.GetText(key: null)); + Assert.Null(LanguageManager.GetText(category: null, key: "x")); + Assert.Null(LanguageManager.GetText(category: "x", key: null)); + } + + [Fact] + public void GetText_FormatsArguments() + { + // Use a key that exists in TR and contains a {0} placeholder. + // messages.unexpected_error_message contains "Beklenmeyen bir hata oluştu:\n{0}". + LanguageManager.LoadLanguage("TR"); + + var formatted = LanguageManager.GetText("messages", "unexpected_error_message", "boom"); + Assert.Contains("boom", formatted); + Assert.DoesNotContain("{0}", formatted); + } + } + + [CollectionDefinition("LanguageManager", DisableParallelization = true)] + public class LanguageManagerCollection { } +} diff --git a/src/SplitWireTurkey.Tests/SplitWireTurkey.Tests.csproj b/src/SplitWireTurkey.Tests/SplitWireTurkey.Tests.csproj new file mode 100644 index 0000000..984cf37 --- /dev/null +++ b/src/SplitWireTurkey.Tests/SplitWireTurkey.Tests.csproj @@ -0,0 +1,39 @@ + + + + net8.0-windows + true + true + false + true + SplitWireTurkey.Tests + SplitWireTurkey.Tests + disable + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + res\Languages\%(Filename)%(Extension) + PreserveNewest + + + + diff --git a/src/SplitWireTurkey.Tests/VersionHelperTests.cs b/src/SplitWireTurkey.Tests/VersionHelperTests.cs new file mode 100644 index 0000000..1e99cc2 --- /dev/null +++ b/src/SplitWireTurkey.Tests/VersionHelperTests.cs @@ -0,0 +1,47 @@ +using System; +using System.Text.RegularExpressions; +using SplitWireTurkey; +using Xunit; + +namespace SplitWireTurkey.Tests +{ + public class VersionHelperTests + { + private static readonly Regex VersionRegex = new(@"^\d+\.\d+\.\d+(\.\d+)?", RegexOptions.Compiled); + + [Fact] + public void GetAssemblyVersion_ReturnsParsableVersion() + { + var version = VersionHelper.GetAssemblyVersion(); + + Assert.False(string.IsNullOrWhiteSpace(version)); + Assert.True(Version.TryParse(version, out _), $"Not a valid Version: {version}"); + } + + [Fact] + public void GetFileVersion_ReturnsParsableVersion() + { + var version = VersionHelper.GetFileVersion(); + + Assert.False(string.IsNullOrWhiteSpace(version)); + Assert.True(Version.TryParse(version, out _), $"Not a valid Version: {version}"); + } + + [Fact] + public void GetProductVersion_StartsWithSemverLikePrefix() + { + var version = VersionHelper.GetProductVersion(); + + Assert.False(string.IsNullOrWhiteSpace(version)); + Assert.Matches(VersionRegex, version); + } + + [Fact] + public void AllAccessors_AreDeterministic() + { + Assert.Equal(VersionHelper.GetAssemblyVersion(), VersionHelper.GetAssemblyVersion()); + Assert.Equal(VersionHelper.GetFileVersion(), VersionHelper.GetFileVersion()); + Assert.Equal(VersionHelper.GetProductVersion(), VersionHelper.GetProductVersion()); + } + } +} diff --git a/src/SplitWireTurkey.Tests/xunit.runner.json b/src/SplitWireTurkey.Tests/xunit.runner.json new file mode 100644 index 0000000..4b7ee28 --- /dev/null +++ b/src/SplitWireTurkey.Tests/xunit.runner.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", + "parallelizeAssembly": false, + "parallelizeTestCollections": false, + "methodDisplay": "method" +} diff --git a/src/SplitWireTurkey.sln b/src/SplitWireTurkey.sln new file mode 100644 index 0000000..06114aa --- /dev/null +++ b/src/SplitWireTurkey.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SplitWireTurkey", "SplitWireTurkey\SplitWireTurkey.csproj", "{CDE4D0B2-5B4F-4257-A177-D2C0BBD47E9C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SplitWireTurkey.Tests", "SplitWireTurkey.Tests\SplitWireTurkey.Tests.csproj", "{E46300A8-529E-4A1F-A244-D88C80B3531A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {CDE4D0B2-5B4F-4257-A177-D2C0BBD47E9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CDE4D0B2-5B4F-4257-A177-D2C0BBD47E9C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CDE4D0B2-5B4F-4257-A177-D2C0BBD47E9C}.Debug|x64.ActiveCfg = Debug|Any CPU + {CDE4D0B2-5B4F-4257-A177-D2C0BBD47E9C}.Debug|x64.Build.0 = Debug|Any CPU + {CDE4D0B2-5B4F-4257-A177-D2C0BBD47E9C}.Debug|x86.ActiveCfg = Debug|Any CPU + {CDE4D0B2-5B4F-4257-A177-D2C0BBD47E9C}.Debug|x86.Build.0 = Debug|Any CPU + {CDE4D0B2-5B4F-4257-A177-D2C0BBD47E9C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CDE4D0B2-5B4F-4257-A177-D2C0BBD47E9C}.Release|Any CPU.Build.0 = Release|Any CPU + {CDE4D0B2-5B4F-4257-A177-D2C0BBD47E9C}.Release|x64.ActiveCfg = Release|Any CPU + {CDE4D0B2-5B4F-4257-A177-D2C0BBD47E9C}.Release|x64.Build.0 = Release|Any CPU + {CDE4D0B2-5B4F-4257-A177-D2C0BBD47E9C}.Release|x86.ActiveCfg = Release|Any CPU + {CDE4D0B2-5B4F-4257-A177-D2C0BBD47E9C}.Release|x86.Build.0 = Release|Any CPU + {E46300A8-529E-4A1F-A244-D88C80B3531A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E46300A8-529E-4A1F-A244-D88C80B3531A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E46300A8-529E-4A1F-A244-D88C80B3531A}.Debug|x64.ActiveCfg = Debug|Any CPU + {E46300A8-529E-4A1F-A244-D88C80B3531A}.Debug|x64.Build.0 = Debug|Any CPU + {E46300A8-529E-4A1F-A244-D88C80B3531A}.Debug|x86.ActiveCfg = Debug|Any CPU + {E46300A8-529E-4A1F-A244-D88C80B3531A}.Debug|x86.Build.0 = Debug|Any CPU + {E46300A8-529E-4A1F-A244-D88C80B3531A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E46300A8-529E-4A1F-A244-D88C80B3531A}.Release|Any CPU.Build.0 = Release|Any CPU + {E46300A8-529E-4A1F-A244-D88C80B3531A}.Release|x64.ActiveCfg = Release|Any CPU + {E46300A8-529E-4A1F-A244-D88C80B3531A}.Release|x64.Build.0 = Release|Any CPU + {E46300A8-529E-4A1F-A244-D88C80B3531A}.Release|x86.ActiveCfg = Release|Any CPU + {E46300A8-529E-4A1F-A244-D88C80B3531A}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/src/SplitWireTurkey/LanguageManager.cs b/src/SplitWireTurkey/LanguageManager.cs index f582e45..15dacda 100644 --- a/src/SplitWireTurkey/LanguageManager.cs +++ b/src/SplitWireTurkey/LanguageManager.cs @@ -10,8 +10,11 @@ namespace SplitWireTurkey /// public static class LanguageManager { + private const string FallbackLanguage = "TR"; + private static Dictionary _currentTranslations = new Dictionary(); - private static string _currentLanguage = "TR"; + private static Dictionary _fallbackTranslations = new Dictionary(); + private static string _currentLanguage = FallbackLanguage; /// /// Mevcut dil @@ -23,30 +26,32 @@ public static class LanguageManager /// public static bool LoadLanguage(string languageCode) { + if (string.IsNullOrWhiteSpace(languageCode)) + { + return false; + } + try { _currentLanguage = languageCode; - - var languagePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "res", "Languages", $"{languageCode.ToLower()}.json"); - - if (!File.Exists(languagePath)) + + // Always keep the Turkish dictionary in memory as a fallback for + // missing keys when a partial translation is loaded. + _fallbackTranslations = LoadDictionary(FallbackLanguage) ?? new Dictionary(); + + var loaded = LoadDictionary(languageCode); + if (loaded == null && !string.Equals(languageCode, FallbackLanguage, StringComparison.OrdinalIgnoreCase)) { - // Fallback olarak TR dilini dene - if (languageCode != "TR") - { - languagePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "res", "Languages", "tr.json"); - } - - if (!File.Exists(languagePath)) - { - return false; - } + loaded = _fallbackTranslations; } - var jsonContent = File.ReadAllText(languagePath); - _currentTranslations = JsonSerializer.Deserialize>(jsonContent); - - return _currentTranslations != null; + if (loaded == null) + { + return false; + } + + _currentTranslations = loaded; + return true; } catch (Exception ex) { @@ -55,41 +60,38 @@ public static bool LoadLanguage(string languageCode) } } + private static Dictionary LoadDictionary(string languageCode) + { + var path = Path.Combine( + AppDomain.CurrentDomain.BaseDirectory, + "res", + "Languages", + $"{languageCode.ToLower()}.json"); + + if (!File.Exists(path)) + { + return null; + } + + var json = File.ReadAllText(path); + return JsonSerializer.Deserialize>(json); + } + /// /// Çeviri metnini alır /// public static string GetText(string key, params object[] args) { - try + if (key == null) { - if (_currentTranslations == null || !_currentTranslations.ContainsKey(key)) - { - return key; // Anahtar bulunamazsa anahtarı döndür - } + return null; + } - var value = _currentTranslations[key]; - - if (value is JsonElement element) + try + { + if (TryResolve(_currentTranslations, key, args, out var text) + || TryResolve(_fallbackTranslations, key, args, out text)) { - var text = element.GetString(); - if (string.IsNullOrEmpty(text)) - { - return key; - } - - // String.Format benzeri işlem - if (args != null && args.Length > 0) - { - try - { - return string.Format(text, args); - } - catch - { - return text; - } - } - return text; } @@ -102,49 +104,66 @@ public static string GetText(string key, params object[] args) } } + private static bool TryResolve( + Dictionary dictionary, + string key, + object[] args, + out string text) + { + text = null; + if (dictionary == null || !dictionary.TryGetValue(key, out var value)) + { + return false; + } + + if (value is JsonElement element && element.ValueKind == JsonValueKind.String) + { + var raw = element.GetString(); + if (string.IsNullOrEmpty(raw)) + { + return false; + } + + text = FormatSafe(raw, args); + return true; + } + + return false; + } + + private static string FormatSafe(string text, object[] args) + { + if (args == null || args.Length == 0) + { + return text; + } + + try + { + return string.Format(text, args); + } + catch + { + return text; + } + } + /// /// İç içe geçmiş çeviri anahtarından metin alır (örn: "tabs.main") /// public static string GetText(string category, string key, params object[] args) { - try + if (category == null || key == null) { - if (_currentTranslations == null || !_currentTranslations.ContainsKey(category)) - { - return $"{category}.{key}"; - } + return null; + } - var categoryValue = _currentTranslations[category]; - if (categoryValue is JsonElement categoryElement) + try + { + if (TryResolveNested(_currentTranslations, category, key, args, out var text) + || TryResolveNested(_fallbackTranslations, category, key, args, out text)) { - var categoryDict = JsonSerializer.Deserialize>(categoryElement.GetRawText()); - if (categoryDict != null && categoryDict.ContainsKey(key)) - { - var value = categoryDict[key]; - if (value is JsonElement element) - { - var text = element.GetString(); - if (string.IsNullOrEmpty(text)) - { - return $"{category}.{key}"; - } - - // String.Format benzeri işlem - if (args != null && args.Length > 0) - { - try - { - return string.Format(text, args); - } - catch - { - return text; - } - } - - return text; - } - } + return text; } return $"{category}.{key}"; @@ -155,5 +174,35 @@ public static string GetText(string category, string key, params object[] args) return $"{category}.{key}"; } } + + private static bool TryResolveNested( + Dictionary dictionary, + string category, + string key, + object[] args, + out string text) + { + text = null; + if (dictionary == null || !dictionary.TryGetValue(category, out var value)) + { + return false; + } + + if (value is JsonElement element && element.ValueKind == JsonValueKind.Object + && element.TryGetProperty(key, out var leaf) + && leaf.ValueKind == JsonValueKind.String) + { + var raw = leaf.GetString(); + if (string.IsNullOrEmpty(raw)) + { + return false; + } + + text = FormatSafe(raw, args); + return true; + } + + return false; + } } } diff --git a/src/SplitWireTurkey/Resources/goodbyedpi/PLACHOLDER b/src/SplitWireTurkey/Resources/goodbyedpi/PLACEHOLDER similarity index 100% rename from src/SplitWireTurkey/Resources/goodbyedpi/PLACHOLDER rename to src/SplitWireTurkey/Resources/goodbyedpi/PLACEHOLDER diff --git a/src/SplitWireTurkey/query b/src/SplitWireTurkey/query deleted file mode 100644 index d123bb2..0000000 --- a/src/SplitWireTurkey/query +++ /dev/null @@ -1 +0,0 @@ -WireSockRefresh diff --git a/src/build_simple.bat b/src/build_simple.bat index 38b99ef..fc38d20 100644 --- a/src/build_simple.bat +++ b/src/build_simple.bat @@ -4,7 +4,7 @@ echo Building SplitWire-Turkey C# WPF Application (Simple Build)... REM Check if .NET SDK is installed dotnet --version >nul 2>&1 if errorlevel 1 ( - echo Error: .NET SDK not found. Please install .NET 6.0 SDK or later. + echo Error: .NET SDK not found. Please install .NET 8.0 SDK or later. pause exit /b 1 ) @@ -22,7 +22,7 @@ if exist "splitwire-logo-128.png" copy "splitwire-logo-128.png" "SplitWireTurkey if exist "splitwireturkeytext.png" copy "splitwireturkeytext.png" "SplitWireTurkey\Resources\" if exist "loading.gif" copy "loading.gif" "SplitWireTurkey\Resources\" if exist "wgcf.exe" copy "wgcf.exe" "SplitWireTurkey\Resources\" -if exist "wiresock-vpn-client-x64-1.4.7.1.msi" copy "SplitWireTurkey\Resources\" +if exist "wiresock-vpn-client-x64-1.4.7.1.msi" copy "wiresock-vpn-client-x64-1.4.7.1.msi" "SplitWireTurkey\Resources\" REM Copy font files if they exist in the main directory if exist "Poppins-Regular.ttf" copy "Poppins-Regular.ttf" "SplitWireTurkey\Resources\"