Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
6dfdaab
chore(packaging): point the Homebrew formula at v7.0.21-beta.
justcoding121 Oct 9, 2026
47ae52c
fix(bypass): learn 403/429 decrypt bypass only from bot-challenge res…
justcoding121 Oct 9, 2026
2484084
fix(http2): accept zlib-wrapped deflate bodies and survive mismatched…
justcoding121 Oct 9, 2026
086b352
feat(inspector): hide a noisy host or process from the session list w…
justcoding121 Oct 9, 2026
a58f1e5
test(inspector): stop gating body spill on process working set
justcoding121 Oct 9, 2026
9982dae
fix(http3): stop wildcard binds that prompt Windows Defender for test…
justcoding121 Oct 9, 2026
75ce546
chore(packaging): point the Homebrew formula at v7.0.21.
justcoding121 Oct 10, 2026
6c23aa7
chore(packaging): point the winget stubs at the signed v7.0.21 assets.
justcoding121 Oct 10, 2026
2c2f73a
feat(inspector): show hidden hosts as chips and edit them in Filters
justcoding121 Oct 10, 2026
c02080c
feat(inspector): localize the UI and close Inspect when a filter drop…
justcoding121 Oct 10, 2026
b76c296
fix(sonar): clear develop code smells and harden 100-continue CI waits.
justcoding121 Oct 10, 2026
d3c4858
fix(ci): clear last Sonar smell and ease headless body-tab timing on …
justcoding121 Oct 10, 2026
aab2884
Merge pull request #1089 from justcoding121/fix/sonar-smells-and-ci-d…
justcoding121 Oct 10, 2026
aceff88
fix(sonar): clear localization-era smells and vulnerabilities on deve…
justcoding121 Oct 10, 2026
23017da
fix(sonar): keep macOS CFString interop via NOSONAR and satisfy CA182…
justcoding121 Oct 10, 2026
feb726f
test(inspector): cover ProxyLocalhostTip for the Sonar PR coverage gate.
justcoding121 Oct 10, 2026
1b901c4
Merge pull request #1090 from justcoding121/fix/sonar-localization-ga…
justcoding121 Oct 10, 2026
f127a41
chore(release): bump product version to 7.0.22 for the next beta cut.
justcoding121 Oct 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Titanium.Cli/Titanium.Cli.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<SignAssembly>false</SignAssembly>
<VersionPrefix>7.0.21</VersionPrefix>
<VersionPrefix>7.0.22</VersionPrefix>
<Authors>Jehonathan Thomas</Authors>
<Description>Titanium Web Proxy CLI (titanium / twp).</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
3 changes: 2 additions & 1 deletion src/Titanium.Inspector/App.axaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:loc="using:Titanium.Inspector.Localization"
xmlns:conv="using:Titanium.Inspector.Converters"
x:Class="Titanium.Inspector.App"
Name="Titanium Inspector"
Expand All @@ -8,7 +9,7 @@
replace the default "About Avalonia" item already pushed to AppKit. -->
<NativeMenu.Menu>
<NativeMenu>
<NativeMenuItem Header="About Titanium Inspector…" Click="OnMacAboutClick" />
<NativeMenuItem Header="{loc:Loc menu.about}" Click="OnMacAboutClick" />
</NativeMenu>
</NativeMenu.Menu>
<Application.Resources>
Expand Down
2 changes: 2 additions & 0 deletions src/Titanium.Inspector/App.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Titanium.Inspector.Localization;
using Titanium.Inspector.Services;
using Titanium.Inspector.ViewModels;
using Titanium.Inspector.Views;
Expand All @@ -15,6 +16,7 @@ public partial class App : Application
public override void OnFrameworkInitializationCompleted()
{
var settings = SettingsService.Load();
LanguageService.Apply(settings.Current.UiLanguage);
ThemeService.ApplyThemeMode(settings.Current.ThemeMode);
var sessions = new SessionRegistry(SessionStoreOptions.FromSettings(settings.Current));
var buffer = new SessionStreamBuffer();
Expand Down
17 changes: 17 additions & 0 deletions src/Titanium.Inspector/Converters/LanguageMatchConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System.Globalization;
using Avalonia.Data;
using Avalonia.Data.Converters;

namespace Titanium.Inspector.Converters;

/// <summary>Radio check for a language setting value (<c>auto</c> or a culture name).</summary>
public sealed class LanguageMatchConverter : IValueConverter
{
public static LanguageMatchConverter Instance { get; } = new();

public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) =>
string.Equals(value?.ToString(), parameter?.ToString(), StringComparison.OrdinalIgnoreCase);

public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) =>
BindingOperations.DoNothing;
}
2 changes: 2 additions & 0 deletions src/Titanium.Inspector/InspectorAppFactory.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Titanium.Inspector.Localization;
using Titanium.Inspector.Services;
using Titanium.Inspector.ViewModels;
using Titanium.Inspector.Views;
Expand All @@ -13,6 +14,7 @@ public static MainWindowViewModel CreateViewModel(InspectorViewModelServices ser
public static (MainWindowViewModel ViewModel, MainWindow Window) CreateMainWindow(
InspectorViewModelServices services)
{
LanguageService.Apply(services.Settings.Current.UiLanguage);
ThemeService.ApplyThemeMode(services.Settings.Current.ThemeMode);
var vm = CreateViewModel(services);
var window = new MainWindow { DataContext = vm };
Expand Down
281 changes: 281 additions & 0 deletions src/Titanium.Inspector/Localization/LanguageService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
using System.ComponentModel;
using System.Globalization;
using System.Text.Json;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;

namespace Titanium.Inspector.Localization;

/// <summary>
/// UI string catalog. Does not change <see cref="CultureInfo.CurrentCulture"/> or
/// <see cref="CultureInfo.CurrentUICulture"/>. Automatic mode reads the OS UI language
/// (<see cref="OsUiLanguage"/>), not the process locale alone.
/// </summary>
public sealed class LanguageService : INotifyPropertyChanged
{
public const string AutomaticSetting = "auto";
public const string English = "en";

private static readonly string[] Cultures =
[
"en", "ar", "zh-Hans", "zh-Hant", "cs", "da", "nl", "fi", "fr", "de",
"el", "he", "hi", "hu", "id", "it", "ja", "ko", "nb", "pl",
"pt-BR", "pt-PT", "ru", "es", "sv", "th", "tr", "uk", "vi", "fa",
];

/// <summary>Options menu rows: setting value, then the name shown in the menu.</summary>
public static readonly (string Setting, string Endonym)[] MenuLanguages =
[
(AutomaticSetting, ""),
("en", "English"),
("ar", "العربية"),
("zh-Hans", "简体中文"),
("zh-Hant", "繁體中文"),
("cs", "Čeština"),
("da", "Dansk"),
("nl", "Nederlands"),
("fi", "Suomi"),
("fr", "Français"),
("de", "Deutsch"),
("el", "Ελληνικά"),
("he", "עברית"),
("hi", "हिन्दी"),
("hu", "Magyar"),
("id", "Bahasa Indonesia"),
("it", "Italiano"),
("ja", "日本語"),
("ko", "한국어"),
("nb", "Norsk bokmål"),
("pl", "Polski"),
("pt-BR", "Português (Brasil)"),
("pt-PT", "Português (Portugal)"),
("ru", "Русский"),
("es", "Español"),
("sv", "Svenska"),
("th", "ไทย"),
("tr", "Türkçe"),
("uk", "Українська"),
("vi", "Tiếng Việt"),
("fa", "فارسی"),
];

private static readonly Dictionary<string, string> Aliases = new(StringComparer.OrdinalIgnoreCase)
{
["pt"] = "pt-BR",
["nn"] = "nb",
["no"] = "nb",
["zh"] = "zh-Hans",
["iw"] = "he",
["in"] = "id",
};

private readonly List<TargetBinding> _targets = new();
private Dictionary<string, string> _active = new(StringComparer.Ordinal);
private Dictionary<string, string> _english = new(StringComparer.Ordinal);

static LanguageService()
{
Instance.Load(English);
}

private LanguageService()
{
}

public static LanguageService Instance { get; } = new();

public static IReadOnlyList<string> ShippedCultures => Cultures;

/// <summary>When set, <see cref="Apply"/> always loads this culture (test host).</summary>
public static string? PinnedCulture { get; private set; }

public string ActiveCulture { get; private set; } = English;

public FlowDirection FlowDirection { get; private set; } = FlowDirection.LeftToRight;

public event PropertyChangedEventHandler? PropertyChanged;

public event EventHandler? LanguageChanged;

public string this[string key] => Get(key);

public static void PinForTests(string culture)
{
PinnedCulture = culture;
Instance.Load(culture);
}

public static void Apply(string? uiLanguageSetting)
{
var culture = PinnedCulture ?? ResolveCultureName(uiLanguageSetting);
Instance.Load(culture);
}

public static string Get(string key) => Instance.Lookup(key);

public static string Format(string key, params object?[] args) =>
string.Format(CultureInfo.InvariantCulture, Get(key), args);

/// <summary>
/// Map a settings value or OS UI culture onto a shipped catalog. Does not assign the thread culture.
/// </summary>
public static string ResolveCultureName(string? setting)
{
if (string.IsNullOrWhiteSpace(setting)
|| setting.Equals(AutomaticSetting, StringComparison.OrdinalIgnoreCase))
{
return MatchCulture(OsUiLanguage.Detect());
}

if (TryCatalog(setting, out var direct))
{
return direct;
}

try
{
return MatchCulture(CultureInfo.GetCultureInfo(setting));
}
catch (CultureNotFoundException)
{
return English;
}
}

public static bool IsRightToLeft(string cultureName)
{
try
{
return CultureInfo.GetCultureInfo(cultureName).TextInfo.IsRightToLeft;
}
catch (CultureNotFoundException)
{
return false;
}
}

public void Load(string cultureName)
{
var culture = TryCatalog(cultureName, out var matched) ? matched : English;
_english = ReadCatalog(English);
_active = culture == English
? _english
: ReadCatalog(culture);
ActiveCulture = culture;
FlowDirection = IsRightToLeft(culture) ? FlowDirection.RightToLeft : FlowDirection.LeftToRight;
RefreshTargets();
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ActiveCulture)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FlowDirection)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Item[]"));
LanguageChanged?.Invoke(this, EventArgs.Empty);
}

/// <summary>Keep a window's flow direction aligned with the active culture.</summary>
public static void AttachWindow(Window window)
{
window.FlowDirection = Instance.FlowDirection;
void OnLanguageChanged(object? sender, EventArgs e) =>
window.FlowDirection = Instance.FlowDirection;
Instance.LanguageChanged += OnLanguageChanged;
window.Closed += (_, _) => Instance.LanguageChanged -= OnLanguageChanged;
}

internal void Track(AvaloniaObject target, AvaloniaProperty property, string key)
{
_targets.Add(new TargetBinding(new WeakReference<AvaloniaObject>(target), property, key));
}

internal static Dictionary<string, string> ReadCatalog(string culture)
{
var stream = OpenCatalog(culture);
if (stream is null)
{
return new Dictionary<string, string>(StringComparer.Ordinal);
}

using (stream)
{
var map = JsonSerializer.Deserialize<Dictionary<string, string>>(stream);
return map is null
? new Dictionary<string, string>(StringComparer.Ordinal)
: new Dictionary<string, string>(map, StringComparer.Ordinal);
}
}

internal static Stream? OpenCatalog(string culture)
{
var assembly = typeof(LanguageService).Assembly;
var name = assembly.GetManifestResourceNames()
.FirstOrDefault(n => n.EndsWith($"strings.{culture}.json", StringComparison.OrdinalIgnoreCase));
return name is null ? null : assembly.GetManifestResourceStream(name);
}

private string Lookup(string key)
{
if (_active.TryGetValue(key, out var value) && value.Length > 0)
{
return value;
}

if (_english.TryGetValue(key, out var english) && english.Length > 0)
{
return english;
}

return key;
}

private void RefreshTargets()
{
for (var i = _targets.Count - 1; i >= 0; i--)
{
var binding = _targets[i];
if (!binding.Target.TryGetTarget(out var target))
{
_targets.RemoveAt(i);
continue;
}

target.SetCurrentValue(binding.Property, Lookup(binding.Key));
}
}

private static string MatchCulture(CultureInfo culture)
{
for (var current = culture; !string.IsNullOrEmpty(current.Name); current = current.Parent)
{
if (TryCatalog(current.Name, out var name))
{
return name;
}
}

return English;
}

private static bool TryCatalog(string name, out string canonical)
{
if (Aliases.TryGetValue(name, out var alias))
{
canonical = alias;
return true;
}

var match = Cultures.FirstOrDefault(culture =>
culture.Equals(name, StringComparison.OrdinalIgnoreCase));
if (match is not null)
{
canonical = match;
return true;
}

canonical = English;
return false;
}

private readonly record struct TargetBinding(
WeakReference<AvaloniaObject> Target,
AvaloniaProperty Property,
string Key);
}
31 changes: 31 additions & 0 deletions src/Titanium.Inspector/Localization/LocExtension.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Avalonia;
using Avalonia.Markup.Xaml;
using Avalonia.Metadata;

namespace Titanium.Inspector.Localization;

/// <summary>Looks up a catalog id and refreshes the target when the language changes.</summary>
public sealed class LocExtension : MarkupExtension
{
public LocExtension()
{
}

public LocExtension(string key) => Key = key;

[ConstructorArgument("key")]
public string Key { get; set; } = "";

public override object ProvideValue(IServiceProvider serviceProvider)
{
var text = LanguageService.Get(Key);
if (serviceProvider.GetService(typeof(IProvideValueTarget)) is IProvideValueTarget provide
&& provide.TargetObject is AvaloniaObject target
&& provide.TargetProperty is AvaloniaProperty property)
{
LanguageService.Instance.Track(target, property, Key);
}

return text;
}
}
Loading
Loading