Contributing to ResXLocalization
Thank you for considering a contribution! This document explains how to propose changes and what a pull request needs to be merged.
This project has a Code of Conduct; by participating, you agree to abide by it.
Before you start
- Bugs: open an issue with a minimal reproduction — usually a small
.resxentry plus the XAML or C# lookup, and the culture you switched to. Say which package and which lookup mode is affected; the issue form asks for both. - Features and larger changes: open an issue first and describe what you want to change and why, so the approach can be discussed before you invest time in an implementation.
- Small fixes — typos, documentation corrections, obvious one-liners — can go straight to a pull request.
Prerequisites
| You need | For |
|---|---|
| .NET 10 SDK | Everything. It is pinned in global.json |
| .NET 8 SDK | Running the Core tests on net8.0, and reproducing the minimum-SDK consumer checks |
PowerShell 7 (pwsh) |
Every script in scripts/. They declare #requires -Version 7.0 and stop on the first line under an older host |
| Windows | The WPF projects, the WPF tests, and the full package and documentation build |
| A C++ toolchain | The Native AOT gate only. MSVC and the Windows SDK on Windows; clang and zlib1g-dev on Linux |
Restore the local tools once per clone:
dotnet tool restore --configfile NuGet.config
That installs CSharpier, DocFX, the ReSharper command-line tools and XamlStyler. No script in this repository installs a tool for you — a formatter that installs software behind your back is a worse problem than an unformatted file.
Setting up
# Windows: build everything.
dotnet build ResXLocalization.slnx -c Release
# Linux/macOS: build everything except the Windows-only WPF projects.
dotnet build ResXLocalization.NonWindows.slnf -c Release
# Test. One command; dotnet test discovers the suites and every target framework, so the Core suite
# runs on net8.0 and net10.0 from here.
dotnet test ResXLocalization.slnx -c Release # Windows
dotnet test ResXLocalization.NonWindows.slnf -c Release # Linux/macOS, without the WPF suite
There are four suites: the Core engine (on both target frameworks), the source generator, and one sample-driven suite per UI framework. The Avalonia suite runs headless; the WPF suite drives a real dispatcher on its own thread and is Windows-only.
One optional per-clone setting
git config --local blame.ignoreRevsFile .git-blame-ignore-revs
.git-blame-ignore-revs lists the repository-wide mechanical commits — a line-ending renormalization, a
formatting pass — so that git blame attributes a line to the change that wrote it rather than to the tool
that reformatted it. GitHub applies the file automatically; your local git does not until you tell it to.
It is optional, it is per clone, and no script sets it for you.
The two gates
The names say when to run them.
pwsh -File scripts/pre-commit-gate.ps1 # before every commit
pwsh -File scripts/pre-release-gate.ps1 # before a push you want CI to go green on
pre-commit-gate.ps1 is the per-commit loop: the public-API reminder, line endings, the full style,
formatting and ordering check, a Release build, and the test suites. By default it writes nothing but build
output — it does not edit your source files and it does not touch the git index; the tidiness step runs the
tools on a disposable copy of the tree and prints the diff that would fix it.
Pass -Fix to have it tidy your working tree first, then review what changed and include it in your commit.
Two checks are deliberately left out of it, because each takes minutes and neither applies to every change. The script names them, with their trigger and their command, in its own summary output:
| Run it when the change touches | Command |
|---|---|
| Resource lookup, culture fallback, satellite discovery, the generated keys, or the packaging that carries them | pwsh -File scripts/verify-package-aot.ps1 -Pack |
What the packages contain, the buildTransitive wiring, or a dependency version |
pwsh -File scripts/pre-release-gate.ps1 -SkipNativeAot -SkipDocumentation, which packs and then runs every consumer, on both of AvaloniaConsumer's target frameworks |
pre-release-gate.ps1 is everything CI checks that can honestly be checked on your machine, in CI's
order, stopping at the first failure: the ignored-revision check, line endings, tidiness, a Release build,
every test suite, the DocFX metadata and site build with --warningsAsErrors, the pack with package
validation, each Native AOT leg this host can run, and the package consumers against the packages just
packed. It ends in one line: PASSED: All checks passed. or FAILED: Check <name> failed. See output.
Its comment-based help lists every CI job it does not reproduce, and why. Read that before treating a green run as a promise that CI will be green.
-SkipNativeAot, -SkipConsumers and -SkipDocumentation cover the checks with heavy prerequisites.
Checking versus fixing
Every check in this repository reports by default and fixes only when asked. That is deliberate: a check that rewrites your tree makes it impossible to tell what you wrote from what a tool wrote.
pwsh -File scripts/tidy-code.ps1 # format the files git reports as changed
pwsh -File scripts/tidy-code.ps1 -Scope all # style, then member ordering, then formatting, everywhere
pwsh -File scripts/tidy-code.ps1 -Scope all -Check # report only - writes nothing, touches no index
-Check never writes to your working tree at any scope. Where a tool has no verify mode — XamlStyler's
passive check rejects every LF file on Windows, and ReSharper has none at all — the tools run for real on a
disposable copy outside the repository and the diff from there is what you see.
Style
Four tools, one concern each, and the C# ones are build errors rather than warnings:
| Concern | Tool |
|---|---|
| C# formatting | CSharpier |
| C# style | the Roslyn analyzers, through dotnet format style |
| C# member ordering | ReSharper applies it; NewStyleCop checks part of it |
| XAML and AXAML layout | XamlStyler |
Write string, not String. Use var for locals. Qualify instance members with this.. Use expression
bodies for single-expression members, file-scoped namespaces with the usings outside, and braces always.
Fields never begin with an underscore.
If the build is clean and tidy-code.ps1 -Scope all -Check passes, the style is fine. The details a tool
cannot tell you are in
.agents/references/code-style.md
— an absolute link, because the documentation site publishes this page and not that one.
Line endings
Every text file is LF, in the repository and in the working tree, on every OS. .gitattributes enforces this
whatever your core.autocrlf is set to, so there is nothing to configure, and CI fails if a wrongly stored
file lands anyway.
XamlStyler cannot write LF on Windows — it always writes the host newline — so scripts/tidy-code.ps1
rewrites exactly the XAML files it processed back to LF afterwards.
If some other tool writes CRLF, git still stores LF, but git status lists the file as modified while
git diff shows nothing. pwsh -File scripts/verify-line-endings.ps1 reports both what git stored and what
is on disk, and says what fixes each. If you have set core.safecrlf true, git refuses to add such a file
with "CRLF would be replaced by LF"; run the tidy script first.
Avalonia and WPF symmetry
src/ResXLocalization.Avalonia and src/ResXLocalization.WPF are deliberate mirrors: the same markup
extensions, the same converter, the same attached properties, the same names and defaults. A change to one
almost always needs the mirrored change in the other, and mirrored tests.
Where the frameworks genuinely differ the difference is expected to be local and explained — Avalonia binds
through an observable and weak events, WPF through a MultiBinding and its own weak binding-target
references, and WPF has no Native AOT. None of those is a reason for a different public API.
Anything that is not framework-specific belongs in ResXLocalization.Core, where both packages share it.
Native AOT
ResXLocalization.Avalonia and ResXLocalization.Core publish with PublishAot=true and produce no IL2xxx
or IL3xxx diagnostic, from anywhere. Keep it that way: no reflection over resources, no new suppressed IL
diagnostic, and no [RequiresUnreferencedCode] or [RequiresDynamicCode] on a public member.
Nothing is trimmed on the just-in-time compiler, so no test suite here can see trimming damage. A
satellite that is no longer loaded or a resource name that no longer resolves produces a binary that builds,
starts and answers wrongly. scripts/verify-package-aot.ps1 publishes a package-only consumer natively and
runs it, asserting exact strings; that is the only check that can see it.
The WPF package is exempt — WPF does not support Native AOT.
Public API
The three runtime projects track their public surface with
PublicApiAnalyzers. An
undeclared public member is RS0016 and a declared one that is gone is RS0017 — both build errors here.
pwsh -File scripts/update-public-api.ps1
That writes the missing entries into the project's PublicAPI.Unshipped.txt. Review that diff line by
line: it is the public-API change, and an entry starting with *REMOVED* is a break.
A public-surface change also needs XML documentation on the new members, the affected pages under docs/
updated, and a CHANGELOG.md entry.
Do not bump a version. The version in the repository-root Directory.Build.props, the release date,
promoting Unshipped to Shipped, and the tag are all the maintainer's, at release time. Describing the
change accurately under ## [Unreleased] is what lets them choose the number.
Commits and branches
Conventional Commits, with a lowercase, imperative summary:
feat: add dynamic format arguments to the localize extension
fix: stop the enum converter caching the previous culture
build: standardize repository tooling
Types in use: feat, fix, docs, test, build, ci, chore. A breaking change is feat!: or fix!:
plus a BREAKING CHANGE: footer saying what breaks and what to do about it — BREAKING CHANGE is a
footer, never a type.
Branches
<type>/issue-<number>-<slug> feature/issue-42-scoped-enum-lookup
<type>/<slug> chore/tidy-sample-resources
Types: feature, bugfix, hotfix, release, chore. Omit the issue segment when there is no issue. The
same policy applies to people and to AI agents.
Changelog
CHANGELOG.md follows Keep a Changelog. Add your entry under
## [Unreleased], in the right category (### Added, ### Changed, ### Fixed, …). Write a breaking
change as - **BREAKING:** ….
Internal formatting and tooling work needs no entry: nothing about it reaches a consumer.
Pull request checklist
The template in the repository states this as a conditional list. In short:
pwsh -File scripts/pre-commit-gate.ps1passes.- The Release build produces zero warnings.
- New behaviour and fixed bugs are covered by tests, on both UI sides where both apply.
- A change to one UI package is mirrored into the other, or the description says why not.
- Public API changes are declared and reviewed; documentation and the changelog are updated.
- The branch name follows the pattern above.
A maintainer will review your pull request, possibly request changes, and merge it once it is approved and CI is green.
For the maintainer: releasing
- Land everything for the release on
main, with the changelog entries under## [Unreleased]. - Move those entries into a new
## [x.y.z] - YYYY-MM-DDsection and add the link reference definition. - Set
<Version>in the repository-rootDirectory.Build.propsto the same number, and setPackageValidationBaselineVersioninsrc/Directory.Build.propsto the version being replaced. pwsh -File scripts/update-public-api.ps1 -MarkShipped— foldsUnshippedintoShippedfor each runtime project.pwsh -File scripts/pre-release-gate.ps1 -Version x.y.z, on Windows. The-Versionswitch adds the two checks CI runs immediately before it publishes: that the declared version matches, and that the changelog holds exactly one dated, non-empty section for it.- Commit, then push the tag
vx.y.z. Pushing the tag is what publishes: CI packs, verifies every gate, pushes to NuGet.org and creates the GitHub release from the changelog section.
Nothing about a release happens on a branch push, and nothing in this repository publishes anything locally.
Questions
Not sure about something? Open an issue or email info@rent-a-developer.de.