This file provides guidance for Claude Code when working with the SharpLouis project.
SharpLouis is a .NET wrapper for LibLouis, the open-source Braille translator library. It provides P/Invoke bindings to the native LibLouis DLL and includes all translation tables.
# Build the solution
dotnet build SharpLouis.sln
# Build in Release mode
dotnet build SharpLouis.sln -c Release
# Create NuGet package
dotnet pack src/SharpLouis/SharpLouis.csproj -c Release
# Run the test suite
dotnet test SharpLouis.sln
# Verify formatting the way CI does (the .NET workflow fails on any diff)
dotnet format --verify-no-changesSharpLouis.sln- Solution file at repository rootREADME.md- The public face of the project: badges, a Features section, the full API reference and the "Language or region?" guidance. Treat it as user-facing documentation, not notes — when the API or the bundled LibLouis version changes, it changes tooCHANGELOG.md- Keep a Changelog history, one section per released version. Packed into the NuGet package alongside the README (see the csprojdocsPackagePath), so it must be updated before the release tag is pushed, not afterCONTRIBUTING.md- Contribution guidance, linked from the READMEGitVersion.yml- Tag-driven versioning config (ManualDeploymentmode; see the memory note on whybranches: main:stays as it is)src/SharpLouis/- Main project directorySharpLouis.csproj- Project file with all build configurationBrailleTranslator.cs- Main translator class with P/Invoke declarations and translation methodsTableCollection.cs- Fluent API for filtering translation tables (filters are non-destructive:FindByLanguage/FindLiteraryreturn a new collection and leave the receiver unchanged, so one populated collection can be reused for several independent queries)TranslationTable.cs- Translation table metadata (asealed recordwith hand-written value equality, becauseLanguagesandTableTypesare lists and record-synthesized equality would compare those by reference)LanguageRange.cs- RFC 4647 extended-filtering match for thelanguageandregionmetadata, which LibLouis declares as extended language ranges (*-IL,akk-Latn,*-fonipa)TranslationModes.cs- Translation mode flags enumTypeForm.cs- Typeform enum for emphasis stylesNativeFunctions.cs- Enum for native function selectionLouisException.cs- Thrown when LibLouis itself fails (a table it cannot compile, a translation it rejects). Missing prerequisites deliberately do not use it — they surface asDllNotFoundException,DirectoryNotFoundExceptionorFileNotFoundExceptionBrailleTranslationTable/- Metadata structuresBrailleContraction.cs- Contraction type constantsBrailleMode.cs- Dots mode constants (6-dot, 8-dot)BrailleType.cs- Braille type constantsTranslationDirection.cs- Translation direction constants
build/AccessMind.SharpLouis.targets- MSBuild targets for NuGet package consumersLibLouis/- Native assetsliblouis.dll- Windows x64 native librarytables.json- Metadata for all translation tablestables/- 400+ Braille translation table files (currently 478, from LibLouis 3.39.0)
tests/SharpLouis.Tests/- xUnit test project (net10.0-windows)BrailleTranslatorTests.cs- End-to-end translation tests against the real nativeliblouis.dllTableCollectionTests.cs-tables.jsonparsing and fluent filteringTranslationTableTests.cs- Pure metadata-predicate logicEnumTests.cs- GuardsTranslationModes/TypeFormvalues againstliblouis.hLanguageRangeTests.cs- RFC 4647 §3.3.2 matching, including the wildcard and singleton cases- The
.csprojcopiesliblouis.dll(to output root) andLibLouis/tables into the test output so the translator resolves them exactly as a real consumer would
samples/SharpLouis.Sample/- Runnable console demo (pick a table, direction and input string), linked from the README's Quick Start. It is part of the solution, so a breaking API change breaks the build here too — keep it compiling
- P/Invoke declarations are in the
#region DllImportinBrailleTranslator.cs - The native library is referenced by bare name (
liblouis) and located by the standard .NET native-library resolver (shipped as aruntimes/win-x64/nativeNuGet asset); it is not a hard-coded path, so it works next to the exe and under single-file publish - Uses
CallingConvention.StdCallwithCharSet.Unicode - Requires
AllowUnsafeBlocksfor pointer operations
- The package version is derived from git tags by GitVersion (
GitVersion.yml), not set in the csproj - Native DLL goes to
runtimes/win-x64/native/in the package - Tables go to
content/LibLouis/in the package build/AccessMind.SharpLouis.targetscopies files to consumer's output directory- Targets are included in both
build/andbuildTransitive/for transitive dependency support
Several build settings are deliberately scoped to CI via Condition="'$(GITHUB_ACTIONS)' == 'true'",
because CI (full git history, github.com remote) is the warning-clean source of truth while local
developer builds stay lenient. Do not remove this scoping to make local builds behave like CI.
TreatWarningsAsErrors— on only in CI, so environment-specific warnings don't hard-fail local builds.ContinuousIntegrationBuild— on only in CI (deterministic/normalized build paths).- Symbols & SourceLink — the package ships
snupkgsymbols (IncludeSymbols+SymbolPackageFormat) withMicrosoft.SourceLink.GitHubso consumers can step into library source from GitHub.EnableSourceLinkis turned off off CI: a developer's git remote may be an SSH host alias (e.g.github:) that SourceLink's GitHub provider can't resolve, which otherwise emits a benign "source control information is not available" warning. SourceLink only matters for the CI-published package, so local builds skip it.
.github/workflows/dotnet.yml runs on pushes to master and on pull requests: restore,
dotnet format --verify-no-changes, build, test. The formatting check is a hard gate, so run it
locally before pushing. The README's build-status badge is wired to this file by name
(actions/workflows/dotnet.yml/badge.svg?branch=master) — renaming the workflow file or the default
branch silently breaks the badge, so update the README in the same commit if you ever do.
Cutting a release is deliberate but fully automatic once started: push a v* git tag (GitVersion
resolves the version from it) and the .github/workflows/release.yml workflow does everything else,
publishing without further confirmation. Pushing the tag is the point of no return — nuget.org
versions are immutable. Two config files are involved — they share the name release.yml but are
unrelated:
.github/workflows/release.yml— the GitHub Actions workflow (only files underworkflows/run). Two jobs:build-test— runs automatically on the tag: restore, build, test, pack. Its Show packed package step writes the real.nupkgfilename (with its GitVersion-resolved version) to the run summary, and uploads the package as a build artifact sopublishreuses the identical bytes.publish—needs: build-testand declaresenvironment: nuget. It runs straight afterbuild-testwith no approval pause; theenvironment:key stays because the nuget.org trusted-publishing policy pins it (removing it breaks the OIDC exchange). It publishes to NuGet via Trusted Publishing (see below) and creates the GitHub release. It has no checkout;ghlocates the repo viaGH_REPOand generates notes server-side. A tag with a-suffix (e.g.v1.2.3-beta.1) is marked--prerelease.
.github/release.yml— not a workflow. GitHub reads this exact path to group the auto-generated release notes by PR label (--generate-notesconsumes it). Categories: Breaking Changes, New Features (feature), Enhancements (enhancement), Bug Fixes, Documentation, Dependencies, Other. Dependabot PRs are excluded by author, so the Dependencies category only collects manual dependency bumps (e.g. a LibLouis native DLL refresh) labeleddependencies.
Trusted Publishing (no stored API key): the publish job uses NuGet/login@v1 with
permissions: id-token: write to exchange a GitHub OIDC token for a short-lived (1 hour,
single-use) nuget.org API key at push time. There is deliberately no NUGET_API_KEY secret —
nothing long-lived to leak or rotate. The security comes from a trusted-publishing policy on
nuget.org bound to the repo owner ID, and it must be configured before the first tag or the push
step fails.
Required setup (one-time):
- A
nugetenvironment (Settings → Environments). It must exist because the trusted-publishing policy pins it by name, but it deliberately has no required reviewers — releases publish unattended. Configure:- Deployment branches and tags set to Selected branches and tags with a Tag rule
v*— defense-in-depth so the environment can only ever be entered from a version tag, even if a future workflow or trigger change tried to reachenvironment: nugetfrom a branch.*is a glob (matches any run of characters except/), not regex.
- Deployment branches and tags set to Selected branches and tags with a Tag rule
- A trusted-publishing policy on nuget.org (username → Trusted Publishing): Repository Owner,
Repository
sharp-louis, Workflow Filerelease.yml(filename only, no path), Environmentnuget. - A
NUGET_USERenvironment secret onnugetholding the nuget.org profile name (not the email). It is not a credential — the policy binding is what authorizes publishing — but a secret keeps it masked in the public repo's logs rather than a plaintext variable.
NuGet versions are immutable once pushed and nothing stands between the tag and the push, so verify the version GitVersion will resolve before creating the tag. The Show packed package summary is a record of what shipped, not a chance to stop it.
- Tables are loaded from
LibLouis\tables\relative to the DLL tables.jsoncontains metadata parsed byTableCollection.PopulateFromJson(). It is generated by the lljt tool in the siblingliblouis-jsonify-tablesrepo, so a schema change there and the matching change toTranslationTablemust land together- LibLouis metadata keys may repeat within one table (the manual says so explicitly): 22 bundled
tables declare several
language:values (he-IL.utbdeclares he/ar/en;ancient-languages-us.utbdeclares 36) and 8 declare twotype:values. Never model a metadata key as single-valued without checking the corpus first - Table files have extensions:
.ctb,.utb,.cti,.uti,.dis,.dic,.tbl - The bundled LibLouis version and the table count are written out in three places that must move together on an upgrade: this file (Project Structure), the README's Features section, and the README's Limitations and Particularities section. Currently 3.39.0 / 478 tables
- Windows x64 only (single
liblouis.dll) - Fixed translation mode:
NoUndefined | UnicodeBraille | DotsInputOutput - UTF-32 LibLouis build
- Platform is restricted to x64 in project file
- File-scoped namespaces
- C# latest language version
- Nullable reference types enabled
- Implicit usings enabled
- .NET 10.0 target framework (plain
net10.0, so any net10.0 consumer can reference it). The Windows-only native dependency is expressed via theruntimes/win-x64/nativeNuGet asset and an assembly-level[SupportedOSPlatform("windows")](declared as anAssemblyAttributeitem inSharpLouis.csproj, not in a separateAssemblyInfo.cs), which gives cross-platform callers a CA1416 hint rather than a hard reference block. The test project staysnet10.0-windowsbecause it exercises the native library and only runs on Windows.