Skip to content

2.12.0 — a compact readout over the game, a frame counter that reports a number, and Ukrainian - #21

Merged
Anton-Babaskin merged 19 commits into
mainfrom
claude/windows-11-rewrite-j1tyfw
Sep 18, 2026
Merged

Anton-Babaskin merged 19 commits into
mainfrom
claude/windows-11-rewrite-j1tyfw

Conversation

@Anton-Babaskin

Copy link
Copy Markdown
Owner

What this changes

A small always-on-top window with the monitor's numbers, called up with Ctrl+Shift+M from
inside a game. The frame counter reports a number again — it never did, on any machine. Three new
tweaks, a third interface language, and two counters that were quietly wrong now read correctly.

Underneath, the SOLID audit is done: half of Core became reachable from a test, and the two
largest view models came apart into the cards they draw.

Why

Four things were wrong rather than missing.

The frame counter never worked. Frame timestamps come from Event Tracing in whatever epoch ETW
hands over, and the window aged them against the local wall clock. Where the two do not line up
every frame looks hours old on the first read, the window empties itself, and the counter reports
nothing — forever, and silently, because an empty window is an ordinary state. Every test passed
the same value for both roles, which is exactly why nothing caught it.

The tuning score was wrong on a translated Windows. A fifth of the score is "is a
high-performance scheme active". Where Windows ships without one, SysTuneX duplicates it —
powercfg /duplicatescheme gives the copy a fresh GUID and keeps the source scheme's name, which
on a Russian or Ukrainian machine is not the English string the check looked for. Every non-English
machine that needed the duplicate was reported as untuned and lost twenty points.

Restore All could skip a kind of change in silence. Restoring went through one method that knew
every recorded kind by name; a kind added later and not added there was simply not restored, and
nothing said so — the promise the whole application rests on, failing quietly.

Reading a power setting returned zero when it read nothing, and for most of those settings zero
means "off", so it reported a machine as tuned having read nothing at all.

The rest is the audit: running an external tool was a static call, so nothing that parses
powercfg, netsh or bcdedit output could be reached from a test — and parsing is the part that
goes wrong, because those tools print in the user's language. Both defects above came out of that
blind spot, found by reading rather than by testing.

How it was verified

  • dotnet build SysTuneX.sln -c Release — no warnings (TreatWarningsAsErrors is on)
  • dotnet test tests/SysTuneX.Core.Tests — 456 green, up from 400
  • dotnet test tests/SysTuneX.App.Tests — Windows only; CI runs it. It builds here, it cannot run
  • Not run on Windows. This branch was developed on Linux, so nothing below was seen on screen

Every behavioural claim was checked by breaking the code and confirming the right test failed,
not just that some test did:

Broken deliberately Caught by
Quick Optimize widened past safe tweaks Only_safe_tweaks_are_offered
Profile preview stopped filtering already-applied entries A_tweak_that_is_already_applied_is_counted_but_not_listed
ConvertTo-Json's single-object case A_single_package_comes_back_as_a_bare_object_and_is_still_read
A binding left naming the page instead of its new child view model the scoped binding check only — the app-wide one passes it, since the name does exist
The second step of Counters.CpuUsage the same check, following the whole path
A key dropped from the Ukrainian file, and a lost {0} both, each naming the language
An emptied English catalogue description Every_catalogue_entry_carries_its_English_text
A renamed tweak id four ways — missing in both translations, orphaned in both

Two WindowPlacementTests failed on their first run and both were real bugs in new code: vertical
reachability used the horizontal threshold, and a screen smaller than the window broke the default
corner.

What is not verified: the frame counter fix cannot be confirmed without a Windows machine and a
game. The failure it addresses is structural and the reasoning is in the changelog, but the fix
itself is unproven on hardware. The same applies to the compact window's placement across monitors.

Risk and rollback

  • Writes to the registry: yes — three new tweaks. Each records its previous value before the
    write, through the same journal as every existing tweak.
  • Changes service configuration: no new services; the catalogue is unchanged.
  • Needs a restart or sign-out to take effect: only where the individual tweak already said so.
  • Reversible from the change journal: yes, and more reliably than before — the PCIe and
    scheduled-task tweaks are the first two that are not plain registry writes, and they are the
    reason the restore path was rebuilt so it can no longer skip a kind without reporting it.

One tweak was deliberately not added. A "lower audio latency" MMCSS change came up while
writing the others; the stock Windows 11 values were checked and the only unset one is a trade
against the game, not a free win. There is a comment in the catalogue saying so, so it is not
"fixed" later by someone who only sees the gap.

Release

  • release.version bumped to v2.12.0, and CHANGELOG.md has a ## v2.12.0 section
  • Both README.md and README.ru.md updated — the Russian one had fallen behind on the
    compact readout in three places, and both now carry the language row
  • Every new user-visible string exists in Strings.resx, Strings.ru.resx and
    Strings.uk.resx

v2.11.0 shipped on 17 September with one section. Everything since had been accumulating under
that same heading, which the release page slices by and the workflow refuses to retag. The v2.11.0
section is restored to exactly what was published, byte for byte; everything else is v2.12.0.


Generated by Claude Code

The Monitor page is only useful while you are looking at it, and while you
are playing you are not. This adds a small window with the same readings
that stays above other windows, summoned with Ctrl+Shift+M from anywhere.

It is not an overlay and will not become one. Nothing is injected into the
game, nothing hooks its swap chain, no driver is loaded - it is an ordinary
top-level window with its chrome removed. So it draws over a borderless
windowed game and it does not draw over one in exclusive fullscreen. That
limit is real, it is stated in the README, in the changelog and in the
settings page, and it is the price of never tripping an anti-cheat.

Three pieces of logic went into Core rather than into the window, because
that is the only place a test can reach them:

  HotkeySpec parses and formats "Ctrl+Shift+M" in any order, spacing or
  case, refuses a modifier-free key - which would fire while the user is
  typing anywhere on the machine - and always hands back a working default
  rather than leaving the app with no hotkey at all.

  MonitorReadout turns a selection plus a tick of values into the rows to
  draw. A reading the machine cannot supply is left out rather than drawn
  empty; a frame reading with the counter running but nothing rendering
  shows "--", because the row's presence is what says the counter is
  watching. The settings panel's preview now draws from the same code, so
  what it promises is what the window shows.

  WindowPlacement decides where a remembered position should actually open.
  Drag the window onto a second monitor, unplug the monitor, and the saved
  position names a place no screen covers: the window opens with no title
  bar to alt-drag and no taskbar button to right-click. Two of these tests
  failed on the first implementation and both were bugs in the arithmetic.

The window and its view model are transient, not singletons. A singleton
would keep a timer and a sensor sample alive for the life of the app with
the window closed, which is exactly the background work this project exists
to remove from other people's machines.

A refused hotkey is reported rather than swallowed. The failure is a code,
not an English sentence, so the settings page can say it in the user's
language: "Ctrl+Shift+M is already taken by another program" is something
the user can act on, and a key that silently does nothing is not.
…rite-j1tyfw

# Conflicts:
#	CHANGELOG.md
#	src/SysTuneX.App/Resources/Strings.resx
#	src/SysTuneX.App/Resources/Strings.ru.resx
Reported from a real machine: the FPS counter does not work. It was two
clocks being compared against each other.

Frame timestamps come from the trace - whatever epoch ETW hands us. The
window then aged those timestamps against DateTime.Now, the local wall
clock. Where the two do not line up, every frame in the window looks hours
old on the first read, the window empties itself, and the counter reports
nothing. Forever, with no error anywhere: an empty window is a perfectly
ordinary state, so there was nothing to log and nothing to show.

Every test passed the same value for both roles, which is exactly why
nothing caught it. FrameTimeWindow now keeps two timestamps per frame and
says in the type why: the presentation time, only ever used as a difference
against another presentation time, and the arrival time on this process's
own monotonic clock, which is the only value compared against "now".
Environment.TickCount64 rather than a wall clock, so a time zone change or
an NTP correction cannot empty the window either.

Three tests, all of which fail against the old behaviour: one in the
direction that actually broke, one keeping the number from going stale after
a game closes, and one pinning that the rate is measured from presentation
times - ETW delivers events in bursts, and measuring the intervals between
arrivals would report an absurd rate and a meaningless 1% low.

Two things the same report exposed, both of which would have made it
answerable in one round instead of several.

Direct3D 9 games were never counted. A D3D9 title does not call
IDXGISwapChain::Present, so with the DXGI provider alone the counter sat at
nothing for every game of that era and gave no hint why. The session now
listens to Microsoft-Windows-D3D9 as well, and checks the provider as well
as the event id - 42 means something different in every provider.

"No frame rate" said one thing for four different situations. The probe now
counts Present events seen and Present events attributed to the watched
process, so the page can tell them apart: the session would not start; the
session is up and nothing on this PC presents through DirectX at all, which
is what a Vulkan or OpenGL game looks like; frames are being presented but
by some other window; or this game was being counted and has stopped, which
is what alt-tabbing out of an exclusive fullscreen game does.
Three new tweaks, and one deliberately left out.

PCI Express link state power management, off. ASPM parks a PCIe link between
transfers and wakes it on the next one; waking takes microseconds, which
matters only when it lands between a mouse report and the frame that should
have used it, on the link carrying the drive a game streams from or the
network card a match is on. It costs power, and the description says so
rather than selling it as free. A machine whose firmware does not expose the
setting now reports Unknown instead of offering a switch that can do
nothing.

The accessibility key shortcuts, off. Five taps of Shift opens the Sticky
Keys dialogue, which in a shooter is crouch, crouch, crouch and a modal
window over the game. Only the shortcut flag is cleared, so the features
themselves stay available to anyone who uses them.

The data collection scheduled tasks, off. Seven of them: the Compatibility
Appraiser, which walks every installed program and can hold a core busy for
minutes, and six smaller Customer Experience Improvement Program tasks. Only
the ones actually running are switched off, and only those are switched back
on - a task the user disabled themselves has no journal entry and stays as
they left it, which is the rule the service tweaks already follow.

No audio latency tweak, though every optimizer has one. On stock Windows 11
the MMCSS Audio task is already at Priority 6 and Pro Audio already at
Scheduling Category High - the values those guides tell you to write. The
one value not already set is the Audio task's category, and raising it puts
the audio engine in the same scheduling band as the game. That is a trade,
not a win. There is a comment in the catalogue saying so, so the next person
to read a guide does not add it.

Two things the new tweaks needed, which were worth fixing anyway:

Reading a power setting meant scanning powercfg's output for a line with
"index" in it, in English or in Russian. That had never been run against
either, because reaching it needed powercfg on a Windows machine - and a
scan that matches nothing does not fail, it returns zero, which for most of
these settings means "off". It would report a machine as tuned having read
nothing at all. The parsing is now a function with twelve tests over real
powercfg output in both languages, and a setting the machine does not expose
reads as nothing rather than as zero. Core parking, which had this code
inline, is now one caller of it.

Scheduled tasks are read through PowerShell rather than schtasks, because
schtasks prints the state as a translated word: "Ready" is "Готово" on a
Russian install, so the parse would match nothing and report every task as
already disabled - claiming work it never did. Get-ScheduledTask returns a
.NET enum, which prints the same everywhere.
Four changes, one of which is a defect rather than a preference.

The metric style carried a comment saying it used tabular figures - every
digit the same width - and a setter that did nothing of the kind. So a
counter going 99 to 100 and back shoved the caption beside it about, twice a
second, on the two screens where that is worst: the monitor page and the
small window that sits over a game. It had done that since the page was
written, because nothing about a comment is enforced. Typography.Numeral
Alignment is now actually set.

Every page opens with the brand accent under its title. The palette has had
a gradient in it and nowhere that used it, so every screen started as grey
text on a grey background and looked like a settings dialogue. Four pixels
of colour, and unlike a drop shadow it costs nothing to draw - which matters
in a tool whose argument is that it does not waste the machine.

One type scale. Pages reached past the shared styles with hard-coded sizes,
13 here and 26 there, so two things that meant the same thing were set
differently on different pages and nothing said which was right. The sizes
that were off the scale are on it, and the two that had no style now have
one: DisplayValue for the one number a screen is about, AccentCaption for
the line on a card that is a verdict.

And a test that reads the markup. A StaticResource naming a key that does
not exist is not a build error: it throws when the page is parsed, which is
when the user opens it, and takes the page down - this project has already
shipped exactly that once, as a pack URI to an image that was never compiled
in. Every resource key every page asks for is now resolved by a test that
needs neither Windows nor WPF, alongside a check that no page quietly loses
the accent rule and that every XAML file is well-formed. Both sabotage
checks fail the right tests.
The first two items from the SOLID audit, and they are the two that unblock
everything else.

IProcessRunner. Everything SysTuneX cannot do through an API it does by
running powercfg, netsh, bcdedit or PowerShell and reading what they print.
The reading is the part that goes wrong - those tools print in the user's
language and their output shape is not a contract - and the runner was
static, so none of it could be reached from a test. A comment in PowerService
said exactly that. Two real defects came out of that blind spot this week
alone, both found by reading rather than testing.

Twelve tests now cover what powercfg prints and what we make of it, in
English and in Russian, including the claim the scheme parser has carried
since it was written: that the labels are translated, so only the GUID and
the parenthesised name can be relied on. Never once run against a Russian
machine's output before now. It is correct. The power setting reader made
the same claim and was not.

One data directory. Half the services took an optional dataDirectory and
fell back to a static path when it was not given, and no test ever passed
it - so every test of the profile, snapshot, game mode and game watcher
services read and wrote the real %ProgramData%\SysTuneX on whatever machine
ran them, leaving state behind between runs and reading state the
developer's own installation had put there. The parameter is gone; the path
comes from IEnvironmentService. AppPaths keeps only what the file logger
needs before the container exists, and says so.

Two things the audit was right about that fell out of the same change:

Rebooting the machine left MainWindowViewModel. It was a one-line call to
the static runner from the class that describes a window - the most
consequential thing this application does, in the layer furthest from
anything that could test it. It is IEnvironmentService's business now,
beside RestartElevated and RestartExplorerAsync, and it reports a refusal
rather than failing silently.

Opening a folder left the view models too. Four commands called
Process.Start directly; they go through IShellLauncher, which additionally
refuses to hand Windows anything that is not plainly an http address -
UseShellExecute interprets whatever it is given, and every URL in the app
being a constant in our own source is the kind of thing that stops being
true one edit later.

Four architecture tests hold the line: nothing but the logger reads a static
data directory, nothing calls the process runner statically, no view model
starts a process, and a guard on each scan so a broken regex cannot make the
rules pass by finding nothing.
"Restore All" was a chain of if (active.Any(e => e.Kind == X)) blocks inside
ProfileService, one per kind of change. A new kind that nobody remembered to
add to that chain would be recorded faithfully, listed in the change
journal, and then quietly skipped by the button whose entire promise is that
it puts everything back - with nothing anywhere saying so. That is the worst
shape a bug can take in this application.

Restorers are resolved as a set now and each claims the entries it is
responsible for, in a declared order: a registry value written by a tweak
goes back through that tweak's own revert, which for a handler-backed tweak
is more than one write and may not be a registry write at all.

The extensibility is not the fix. The fix is the last step: whatever no
restorer claimed comes back in the report as an error, so the user is told
the machine is not fully restored rather than told it is. And a test asserts
that every BackupKind has a restorer - adding one without a restorer now
fails at the moment it is added, not on a user's machine at the moment they
press the button.

Two things fell out of it:

Materialize - turning a journal entry's recorded text back into a registry
value of the right type - was a private method inside TweakEngine. The
restorer that puts back a value nothing else owns had no way to reach it and
would have grown a second copy. It lives in RegistryValueComparer now, next
to Stringify, which is the function it is the inverse of.

The applied-profile record is its own store. ProfileService had eleven
dependencies and was also doing JSON persistence; its tests passed null! for
six of those eleven just to reach this. AppliedProfileStore is four methods
and testable on its own.

Also fixed while here: FakeEnvironment defaulted its data directory to the
shared %TEMP% root, so two tests that each built one wrote the same
gamemode.json and one read what the other left. It gets a fresh directory
per instance, and the test that deliberately wants two services to share a
machine - a crash and a relaunch - now says so instead of relying on the
collision.
Every one of these was a single interface that two unrelated callers took
because there was only one to take, and the proof was the test fakes:
FakeBackupService left six of eleven members empty, FakePowerService about
half.

IBackupService is a writer and a reader. Services that change the machine
record and retire entries; the history page and the diagnostics report read
what was changed and have no business recording anything. A fake for the
reader is three members.

IPowerService is schemes and settings. The settings page and game mode want
a scheme; a tweak wants one index inside whichever scheme happens to be
active and does not care which. Core parking sits with the settings, because
that is what it is - two indexes in the processor subgroup - and dressing it
up as its own pair of methods is why ASPM, which is the same shape, needed
the generic pair added separately later.

IGameWatcher is a list and a detector. The settings page edits the list and
never starts a poll; the automation watches for a game and never adds one.

IProcessService is IMemoryTrimmer, one method. SetPriority, SetAffinity and
GetTopProcessesByMemory had no caller anywhere in the application and were
implemented, translated and maintained for nobody. So had
IPowerService.SetHibernationAsync. They are in the history if a page ever
wants them; carrying them as a promise nothing kept was the worse option.

Each union interface survives for the implementation and the container,
which hands one instance out behind all of them - and a test now fails if
anything else takes the union back, because that would undo the split one
constructor at a time.

Three ProfileService dependencies went with the rollback that used them: the
change journal, the privacy service and the network service. Its preview
test passed null! for six of eleven parameters; it is three of ten now, and
the three are the ones apply genuinely needs.
Four seams, each one a place where a class needed a running Windows desktop
to exist at all.

Post-apply refreshes. TweakEngine ran a chain of HasFlag branches, each
calling a static P/Invoke helper, so the engine knew about every kind of
refresh there is and none of it could be reached from a test - reaching it
meant calling SystemParametersInfo on a live desktop. One IPostApplyEffect
per flag now, resolved as a set, and six tests cover what used to be
unobservable: that only the asked-for effects run, that reverting runs them
the other way round, that one throwing does not lose the next or fail the
tweak, and that every flag in the enum has an effect registered for it.

The CPU temperature. GPU sensors have been behind a probe interface since
the start and the composition around them is tested; the CPU side was a WMI
query inline in SensorService, so SensorCompositionTests ran a real query
against root\WMI on whatever machine it ran on - slow, dependent on that
machine's firmware, and unable to pin the one thing it should:
that a machine with no thermal zone reports no temperature rather than a
zero the dashboard would draw as real.

Application.Current. View models reached for the dispatcher, the main window
and Shutdown directly. IUiDispatcher, IAppLifetime and IWindowAppearance
replace them. The appearance one earns its place immediately: applying a
backdrop is WPF-UI's business and fails in an interesting way - it rebuilds
the window chrome, and replacing one that already carries an inheritance
context throws from inside WPF - so that refusal is handled in one place
instead of at each call site.

MainWindow.GoTo reached into the static container for a page although the
page provider is already a constructor parameter. It uses the parameter.

Caught by an existing test while doing this: moving the backdrop logic out
dropped the message that tells the user it will apply after a restart. The
string-coverage check noticed the resource had stopped being used, which is
the only reason it was not shipped as a button that silently does nothing.
ApplyBackdrop returns whether it took effect, and the settings page says so.
Two catalogues that promised "adding an entry is one line" and charged four.

Global search had three sources written into it, and decided which page owns
a tweak category with a switch whose default arm was the gaming page. A
category added to the enum and not to that switch sent the user to a page
that could not contain what they searched for - silently, with nothing to
say so. Sources are ISearchSource now, and the category-to-page mapping is
declared next to the page itself where it is registered. A category with no
page is simply not findable, which is visibly wrong rather than quietly
wrong, and a test fails the moment one is added without a page.

The monitor page had thirteen near-identical Display* properties, their
thirteen names repeated in the list of things to notify when a tick moved,
and a thirteen-case switch formatting the preview - while the catalogue they
all come from says adding a reading is one entry plus two resource strings.
One indexer replaces the properties, one notification replaces the list, and
the preview is built by the same code as the compact window, so what the
preview promises is what that window shows. That switch was a second copy of
a formatter I had already moved into Core and then not used.

The rebinding found its own bug immediately: two of the thirteen were
renamed to names the enum does not have, which parses to nothing and hides
the tile forever with nothing anywhere saying why. There is now a check that
every {Binding [X]} names a real reading, and it fails on a deliberately
broken one.
SnapshotService held the comparison logic behind four constructor
dependencies, so testing "did these two snapshots differ" meant building a
service with four nulls. The comparison never needed any of them: it is two
records in, a list of differences out. SnapshotComparer is that function, and
the tests now call it directly.

CleanupService cleaned directories and also enumerated and removed Store
packages through PowerShell - two jobs with nothing in common but the page
they appear on. IAppPackageService is the second one.

Parsing what Get-AppxPackage prints is worth its own tested function because
of a detail that is invisible while developing: ConvertTo-Json emits a bare
object instead of an array when exactly one package matches. A parser that
assumed an array works on every developer's machine, where dozens match, and
returns nothing on a cleaned-up machine with one - the machine whose owner
opened this page in the first place.
The two halves of that page answer different questions. The change log is
what SysTuneX did and can undo; snapshots are what this machine looks like,
which works just as well for changes SysTuneX had nothing to do with. They
shared a class only because they share a screen.

They still share the spinner, so IBusyScope is the whole of what the panel
needs from the page that hosts it - not the page itself, with its eight
dependencies.

Scoping part of a page to a child view model is the construct WPF is quietest
about: under DataContext="{Binding Snapshots}" a name that is still perfectly
real on the page's own view model resolves against nothing, renders nothing,
and says nothing. The existing binding check could not see it, because it asks
only whether a name exists somewhere in the source. The second check added
here resolves each binding outside a template against the view model that
element actually reads, following base classes - four pages take nearly all
their content from a shared one.

Verified by putting the mistake back: restoring the old property name inside
the new scope fails only the new check, while an ordinary typo fails both.
The last screen before a dozen registry writes was ninety lines of WPF built
control by control inside the class that shows toasts. What it says and what
it looks like were the same ninety lines, and neither could be checked except
by applying a profile and reading the dialog - so the omission that would
matter most, a change the list quietly leaves out, was the one nothing could
see.

The decisions are content, not layout, so they are a function in Core now:
which entries appear, which are left out because the machine is already in
that state, and what a tweak with no registry values says instead of showing
an empty gap. Eleven tests cover it, including the one that reads the whole
split: a tweak already applied is counted but not listed.

One thing changed on the way through. A registry value the machine does not
have arrives as null rather than as an empty string, so "not set" stays
distinguishable from a value that is genuinely empty - flattening the two was
free while this was all one string.Format call, and wrong.
translated Windows

Fourteen dependencies and eight jobs in one class: hardware read once,
counters every second, sensors every fifth tick, the game mode card from
whichever thread the news arrived on, the tuning score, and four actions. They
are three view models now - Hardware, Counters, GameMode - and a page that
owns the tick, the score and the actions.

Two pieces moved out of the application entirely, because neither was about a
screen.

Quick Optimize is the one button that changes the machine without asking a
second time, and the rule that earns it that - safe tweaks only, never
moderate, never advanced - was a .Where in the middle of a WPF command
handler. Nothing could assert it there. One careless edit and a click labelled
"optimise" disables virtualisation-based security. It is IQuickOptimizer now,
with the rule under test.

Moving the "is this a high-performance scheme" heuristic onto PowerScheme
exposed a real defect. When Windows ships with neither built-in scheme
SysTuneX duplicates one; powercfg gives the copy a fresh GUID and keeps the
source scheme's name, which on a Russian or Ukrainian Windows is not the
English string the check looked for. So on every non-English machine that
needed the duplicate, the dashboard reported a tuned machine as untuned and
withheld a fifth of the score. The service already writes down which copy it
made, so the question goes to the service.

The binding check follows a whole path now, not just its first step. The three
cards interleave on screen, so they are reached as Counters.CpuUsage rather
than through a scoped subtree, and a scan that stopped at "Counters" would
have verified nothing about the thirty-odd names after the dot. Verified by
breaking one: the second step is caught.
Eight folded cards, and every field, handler and summary property of all
eight ran as one stripe through a single seven-hundred-line class with
fourteen dependencies. Five of them are their own view models now - the game
watch list, the schedule, the power scheme, the compact readout and
diagnostics - each scoped to the expander it lives in. What is left is the two
cards that stay open, the tray, the "about" block, and the loading flag.

That flag is the only thing the sections share, so it is the whole of
ISettingsHost: the stored settings, a way to write them back, and whether the
page is still filling its controls in. Every handler checks the last one,
because assigning a property raises the same notification a user's click does -
without it, opening the page writes the file eight times and applies each
value as though it had just been chosen.

Verified by putting back the mistake this shape makes possible: a binding that
names a property which is still perfectly real on the page's own view model
but not on the section it now sits in. The app-wide check passes it, because
the name does exist; the scoped check catches it.
All 644 strings: the whole interface plus the catalogues. Every tweak,
service, profile and cleanup target carries its name and its description,
because a page that lists "Вимкнути Game Bar" next to an English paragraph
explaining it is not translated, it is half-translated.

The checks that hold the languages together had to stop naming them first.
They were hard-coded to compare exactly two files, so a third language would
have shipped with none of them - which is precisely the language that needs
them. They discover every Strings.<lang>.resx now and hold each one to the
English original: same keys, same placeholders, nothing blank, nothing twice.

One check is new. Catalogue names live in C# in English and in the resource
files translated, so a translation legitimately carries keys the English file
does not - which meant a second translation could silently miss a whole
catalogue and nothing would notice. The translations are held to each other
as well as to the original.

Verified by breaking the new file two ways: a dropped key and a lost
placeholder are both caught, and the failure names the language.
English is the one part of the interface that is not in a resource file.
Every tweak, service, profile and cleanup target carries its English in C# so
Core stays useful on its own, and CatalogText looks for a translation first,
falling back to that text. It is the right arrangement, and it has a blind
spot in both directions.

An entry with no English description is an empty paragraph for every English
user, and no check that compares resource files can see it, because that text
is not in one. A missing translation is one English line in the middle of a
translated page, and nothing reports it either - degrading to English is
correct on a user's machine and wrong on a developer's.

Both directions are held to the catalogue itself now: every entry must carry
finished English text, not blank and not a placeholder, and every entry must
be translated in every shipped language. The reverse as well - a translation
naming an id the catalogue no longer has is a renamed tweak's old name and
description, carried and re-reviewed in every language forever.

Nothing was wrong when these went in. The point is that nothing would have
said so. Verified by making it wrong: an emptied English description is
caught, and a renamed id fails four ways - missing in both translations,
orphaned in both.
v2.11.0 shipped on 17 September with one section - the profiles page saying
which profile is on - and everything since had been piling into that same
heading. The release page slices the changelog by heading and the workflow
refuses to retag, so this work had nowhere to go until the number moved.

The v2.11.0 section is restored to exactly what was published, byte for byte,
and everything else is v2.12.0.

Two pieces of the audit were worth writing up rather than leaving as commit
messages, because both are visible from outside: the tuning score was wrong
on a translated Windows, and Restore All could skip a kind of change in
silence.

The Russian README had fallen behind the English one on the compact readout -
in the feature list, the page table and the paragraph that admits there is no
overlay. A feature documented in one and not the other is half-documented.
Both now carry the language row as well.
The four translations were plain text links, which said nothing about which
page you were on. They are badges now, and the current page's own badge is
coloured while the rest stay grey - so the row answers "where am I" as well as
"where can I go". A UI-languages badge joins the others, because the interface
speaking three languages is a fact about the application rather than a line of
prose.

The rest is catching up with work already merged:

- The architecture tree was missing four directories that exist and one that
  matters - Resources now says what a language file is called, because that is
  the thing a contributor has to get right.
- Gaming and Privacy list the three tweaks added this release.
- The Ukrainian landing page says the interface is Ukrainian, which is the
  point of that page now.
- The Spanish one says the opposite, plainly: the page is translated and the
  application is not. Someone downloading it from that page would otherwise
  find out afterwards.
- Both landing pages gained the compact readout, which they had never heard of.

All four badge URLs were fetched and render the text they should, Cyrillic and
ñ included - an encoding mistake here is a broken image on the front page.
@Anton-Babaskin
Anton-Babaskin merged commit 3809942 into main Sep 18, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant