.NET wrapper for the LibLouis Braille translator and back-translator library.
- Batteries included. The native LibLouis 3.39.0 library and all 478 translation tables ship inside the package and are copied to your output directory — no separate download, no native build step, no
PATHjuggling. - Translation both ways, with emphasis. Print to Unicode Braille and back, optionally carrying per-character typeforms (bold, italic, underline and friends) in either direction.
- A searchable table catalog. Every table's metadata is parsed from a bundled
tables.jsoninto aTranslationTablerecord, andTableCollectionoffers a non-destructive fluent API to filter it by language, region, Braille type, grade or contraction level — enough to build a table picker without hard-coding a single file name. - Correct language matching. LibLouis declares table languages and regions as RFC 4647 extended language ranges (
*-IL,akk-Latn,*-fonipa), and SharpLouis matches them the way LibLouis itself does rather than by string comparison. - Cheap and thread-safe. A translator holds no unmanaged resource: create one per table, share it across threads, keep it for the lifetime of the app, and never think about disposal.
- Fails early and clearly. Tables are compiled at
Createtime, so a missing native library or a broken table throws a descriptive exception up front — withTryCreatefor when you would rather probe than catch. - Packaged properly. A plain
net10.0target, a RID-scoped native asset, XML docs for IntelliSense, and SourceLink-enabledsnupkgsymbols so you can step straight into the library source from GitHub.
Install via NuGet Package Manager:
dotnet add package AccessMind.SharpLouis
Or via the Package Manager Console in Visual Studio:
Install-Package AccessMind.SharpLouis
The package includes the native LibLouis DLL and all translation tables, which are automatically copied to your output directory.
using AccessMind.SharpLouis;
// Create a translator for a translation table. It owns no unmanaged resource, so there is
// nothing to dispose — just create one and use it. Create throws a descriptive exception if
// the native library or the table is missing (use BrailleTranslator.TryCreate for a non-throwing probe).
var translator = BrailleTranslator.Create("en-ueb-g1.ctb");
string braille = translator.TranslateString("Hello World");
Console.WriteLine(braille); // Outputs Unicode Braille: ⠠⠓⠑⠇⠇⠕⠀⠠⠺⠕⠗⠇⠙A translator is cheap and thread-safe: create as many as you like (one per table), share them across threads, and keep them for the lifetime of your app.
For a complete, runnable example see the samples/ directory: a small console app that translates print text to Braille and back, letting you pick the table, direction, and input string.
When working with Braille input and output, one needs to have a tool that ideally would take into account all the particularities and intricacies of the Braille code for various languages and needs (contracted Braille, Unicode Braille, on-the-fly translation and so on). The TL;DR is that there is no straightforward one-to-one way of translating a given message from print to Braille and vice-versa, without knowing the language used, the code variant (known as Braille table) inside that language, sometimes the context and so on, and so forth.
There are several software solutions dealing with this task, but most of them are proprietary and very expensive for use in derived products. The most widely known and used free (LGPL-licensed) open-source solution is LibLouis, a library written in C initially for the BRLTTY Linux screen reader but gone far beyond this. Now it is available for all popular operating systems and used in many open-source and proprietary products, including screen readers and Braille translating and embossing software.
However, for a long time there was no publicly available open-source wrapper for the .NET environment: everyone who wanted to use LibLouis in a .NET-based product had to wrap the C API by oneself. SharpLouis exists to close that gap. It has been published on NuGet since 2024, follows semantic versioning, keeps a change log, and tracks upstream LibLouis releases — bundled library, tables and metadata refreshed together.
SharpLouis has been shipping since 2024 and its API is stable — see the change log for what changed in each release. A few deliberate scoping choices remain, though, and they are worth knowing before you build on it.
- For now, only Windows is supported as we provide LibLouis DLL and tables inside the package. The managed assembly targets plain
net10.0, so a cross-platform project can reference it without a hard platform block, but any call into it will fail at runtime off Windows x64 (the compiler will also warn via[SupportedOSPlatform("windows")]); - Currently only 64-bit systems are supported (platform is restricted to x64 in the project file);
- The bundled LibLouis native library is version 3.39.0. You can confirm the exact version at runtime with
BrailleTranslator.GetVersion(); - The DLL is built with UTF-32 support (see LibLouis documentation if you don’t know what we are talking about);
- Translation tables are both bundled with the package and listed in a JSON file for displaying and filtering (see the section about translate table collection below). The utility that processes tables is called LLJT and is also open-source.
- Translation mode is fixed to
TranslationModes.NoUndefined | TranslationModes.UnicodeBraille | TranslationModes.DotsInputOutput, i.e., currently SharpLouis works only with Unicode Braille internally and no output for undefined characters is provided.
The main BrailleTranslator class exposes several public methods, most of which are directly wrapped C API methods provided by LibLouis. Every translator serializes its native calls internally, so a single instance is safe to share across threads.
static BrailleTranslator Create(string tableNames)— Creates the translator that can be subsequently used. The parameter, although stated in plural, is usually a single table name relative to the path where the translation tables are located, so usually it's something like"en-ueb-g1.ctb"(a comma-separated list is also accepted). The table is compiled up front, so a broken or missing table fails here rather than on the first translation:CreatethrowsArgumentException,DllNotFoundException,FileNotFoundException, orLouisExceptionas appropriate. A translator owns no unmanaged resource, so it is cheap to create, thread-safe to share, and there is nothing to dispose.static bool TryCreate(string tableNames, out BrailleTranslator? translator)— Non-throwing form ofCreate: returnsfalse(withtranslatorset tonull) instead of throwing when the native library, tables folder, or a requested table is unavailable. Useful for probing availability.string CharsToDots(string chars)— Equivalent of thelou_charToDotsfunction in LibLouis. Accepts characters as a string and returns the corresponding dot patterns. For more details about this and all subsequent methods see the LibLouis documentation. These methods return the result string and throwLouisExceptionif the native call fails.string DotsToChars(string dots)— Inverse of the previous method. Accepts dot patterns and returns characters according to the translation table being used.string TranslateString(string text)— Translates a string to Unicode Braille according to the translation table selected on translator instantiation.string TranslateStringWithTypeForms(string text, TypeForm[] typeForms)— Translates a string with emphasis styles. Accepts an array of emphasis typeforms as members of theTypeFormenum, indexed liketext. See the LibLouis documentation for more info on this.string BackTranslateString(string braille)— Translates a Braille representation back to text according to the translation table selected on translator instantiation. Note! Not every table is capable of back-translating from Braille to text, see below on translation tables filtering.(string Text, TypeForm[] TypeForms) BackTranslateStringWithTypeForms(string braille)— Same but also reports the per-character emphasis LibLouis inferred.static void ClearTableCache()— Releases LibLouis's process-global cache of compiled tables (the nativelou_free). This affects every translator in the process, not a single instance, and is normally unnecessary — the cache is cheap to keep and repopulates automatically on the next translation. Call it only to reclaim that memory or to force tables to be recompiled after their files change on disk.static string GetVersion()— Returns the version string of the underlying native LibLouis library, for example3.39.0.
A translation table is a way to represent print characters in Braille. As the Braille code consists of only 63 characters in traditional 6-dot Braille plus the space and of 255 characters in Computer 8-dot Braille plus the space, there is no one-on-one correspondence between print and Braille. For example, the character ⠝ (Braille dots 1345) can represent the Latin letter n, Cyrillic н, Hebrew נ/ן, Greek ν and many other letters usually having the value of N, and also the half note C in music. More than that, punctuation and even numbers are sometimes represented differently, depending on the language and the code used. That’s what translation tables are for.
In SharpLouis, a translation table is represented by a TranslationTable record that mirrors translation table metadata from LibLouis.
A word on why two of its properties are lists. LibLouis tables declare their metadata as #+key: value lines in the table header, and the LibLouis manual is explicit that the same key may appear multiple times in a table. This is not a corner case: he-IL.utb (Israeli braille) declares Hebrew, Arabic and English, ancient-languages-us.utb declares thirty-six languages, and the Swedish and Elfdalian 8-dot tables declare both a computer and a literary type. Languages and TableTypes therefore hold every declared value.
TranslationTable has the following properties:
FileName— The name of the table file in LibLouis. Example: en-ueb-g1.ctbDisplayName— A human-readable display name for using in user interfaces. Example: Unified English uncontracted brailleIndexName— A sort-friendly name written "Language, qualifiers". Example: English, U.S., computer, 8-dot. Ordering a table picker by this groups a language's tables together, whichDisplayNamedoes not. May benull.Languages— Every language the table declares, as a read-only list. These are RFC 4647 extended language ranges, not plain codes: alongside en and he you will find akk-Latn and *-fonipa. UseMatchesLanguage()rather than comparing strings.Region— The region the table is used in, also as an extended language range. Example: en-US, or *-IL for "Israel, whatever the language". May benull.TableTypes— The Braille types the table covers, as a read-only list. The values are defined in theBrailleTranslationTable/BrailleTypestruct: literary, computer or math Braille. Usually one entry, occasionally two, and empty when the table declares no type.ContractionType— Determines the level of contraction the table supports. The values are defined in theBrailleTranslationTable/BrailleContractionstruct. Can be one of not contracted, partially contracted or fully contracted. May benullwhen the table declares no contraction metadata (for example, computer Braille tables).Grade— The Braille grade the table implements, as a string rather than a number: alongside 0, 1, 2 and 3, LibLouis ships tables graded 1.2, 1.3, 1.4 and 1.5. May benull.DotsMode— the "dotness" of the Braille supported by the table. The values are defined in theBrailleTranslationTable/BrailleModestruct. Can be 8 (eight-dot Braille) or 6 (six-dot Braille), or 0 when the table declares no dots metadata.Direction— Translation direction supported by the table. The values are defined in theBrailleTranslationTable/TranslationDirectionstruct. Can be one of forward, backward or both. May benullwhen the table declares no direction metadata, in which case the table is treated as bidirectional.System— The Braille system or code the table implements. Example: ueb (Unified English Braille), ebae, ddp. Free-form; may benull.Variant— Distinguishes tables that would otherwise be alike. Example: detailed, compact, no-tone. May benull.Version— The edition of the Braille standard the table follows, usually a year such as 2025. Unrelated to the LibLouis version. May benull.Locale— The locale whose conventions the table assumes, where that differs fromLanguages: a Greek table transcribed for English-speaking students declares locale: en. May benull.UnicodeRange— The character range the table needs, ucs2 or ucs4. SharpLouis ships a UTF-32 build, so every bundled table is usable. May benull.
TranslationTable is a record with value semantics: two instances are equal when every field is equal, with Languages and TableTypes compared element by element.
This record also has some helper methods for filtering translation tables:
bool MatchesLanguage(string languageTag)— Returnstrueif any of the table's declared languages coverslanguageTag, per RFC 4647 extended filtering. Matching is asymmetric: a table declaring en matches the tag en-GB, but one declaring en-GB does not match the bare tag en.bool MatchesRegion(string regionTag)— Same, against the table'sRegion. Alwaysfalsefor a table that declares no region.bool IsOfType(string tableType)— Returnstrueif the table declares the given Braille type.bool IsLiteraryBraille()— Returnstrueif the current table is a literary Braille table.bool IsComputerBraille()— Returnstrueif the current table is a computer Braille table.bool IsMathBraille()— Returnstrueif the current table is a mathematical Braille table.bool IsGrade(string grade)— Returnstrueif the table declares the given Braille grade.bool IsUncontracted()— Returnstrueif the current translation table supports no contractions.bool IsPartiallyContracted()— Returnstrueif the current table supports partially contracted Braille. A good example of this is the German Vollschrift table (de-g1.ctb). This code has contractions for basic letter combinations but no sophisticated whole-word contractions.bool IsFullyContracted()— Returnstruewhen the current table supports contracted Braille, also commonly referred to as grade 2 in many languages.bool IsContracted()— Returnstrueif the current table supports either fully or partially contracted Braille.bool CanTranslate()— Returnstrueif the current table can translate print to Braille.bool CanBackTranslate()— Returnstrueif the current translation table can translate Braille back to print text.bool CanTranslateBothWays()— Returnstrueonly if the current table can translate print text to Braille and Braille to print text.bool IsEightDot()— Returnstrueif the current table is an eight-dot Braille translation table. Most of them are designed for computer Braille, but not all: there are languages that officially have characters with dots 7 and 8 in their literary Braille.bool IsSixDot()— Returnstrueif the current translation table is a six-dot Braille table.
The table collection class helps in filtering translation tables, selecting them and displaying various information about them. It uses the fluent interface. So, for example, to find all literary tables for the French language, you can do:
var frenchLiteraryTables = new TableCollection()
.PopulateFromJson()
.FindByLanguage("fr")
.FindLiterary();The filter methods are non-destructive — each returns a new collection and leaves the receiver unchanged — so a single populated collection can be reused for several independent queries (for example calling ListLanguages() on the full set and FindLiterary() on the same instance). A collection is also enumerable, so LINQ covers anything the built-in filters do not: collection.Where(t => t.System == "ueb").
It has the following methods:
TableCollection PopulateFromJson()— Parses the JSON file provided with the library and returns a table collection instance populated from this file.TableCollection FindByLanguage(string languageTag)— Returns a new collection of the tables usable for a given language, leaving the receiver unchanged. Tables declare their languages as RFC 4647 extended language ranges, and this matcheslanguageTagagainst them the way LibLouis itself does:FindByLanguage("en-GB")finds the tables that declare plain en, andFindByLanguage("he")finds he-IL.utb. A table declaring several languages is found under each of them, so Israeli braille answers to he, ar and en alike.TableCollection FindByRegion(string regionTag)— Returns a new collection of the tables used in a given region, matched the same way. Tables that declare no region are excluded.TableCollection FindLiterary()— Returns a new collection of the tables supporting literary Braille, leaving the receiver unchanged.TableCollection FindComputer()— Same for computer Braille tables.TableCollection FindByGrade(string grade)— Returns a new collection of the tables of a given Braille grade ("0","1","2","3", and the fractional grades such as"1.5").TranslationTable? FindByFileName(string fileName)— Accepts a file name and finds the corresponding translation table, ornullif no table with that file name exists.Dictionary<string, string> ListLanguages()— Searches all the tables and lists the languages supported by those tables. A table that declares several languages contributes each of them. Returns a dictionary where the key of each element is a language code and the value is its full English name.
Language and region matching is exposed on its own as the static LanguageRange class (Matches(range, tag) and MatchesAny(ranges, tag)), should you need to apply RFC 4647 extended filtering to something other than a table.
These answer two different questions, and picking the wrong one is the easiest mistake to make with this API.
FindByLanguage("en-GB") returns every English table — UEB, American, New Zealand, the lot. That is not a bug: no LibLouis table narrows its language to en-GB. Every English table declares plain language: en, and the country lives in a separate region field. So FindByLanguage is asking "which tables can a British reader use?", and the honest answer is all of them.
To ask "which tables are specifically British?", filter by region — and narrow to the language too:
var british = new TableCollection()
.PopulateFromJson()
.FindByLanguage("en")
.FindByRegion("en-GB");
// en-gb-comp8.ctb, en-gb-g1.utb, en_GB.tblThe FindByLanguage("en") step is not redundant. Regions are extended language ranges too, so a bare FindByRegion("en-GB") also matches the two Greek braille as used by English speakers tables, whose region is the broader range en. They really are used in Britain — they are just not English tables.
One thing to know before you build a country picker: Unified English Braille declares no region at all, because it is an international code rather than a national one. A region filter therefore drops en-ueb-g1.ctb and en-ueb-g2.ctb, which is exactly what the UK has used since 2011. For a UK table list you most likely want the region-en-GB tables plus the region-less UEB ones, something like:
var all = new TableCollection().PopulateFromJson();
var forTheUk = all.FindByLanguage("en")
.Where(t => t.MatchesRegion("en-GB") || t.Region is null);The same shape applies elsewhere: FindByRegion("he-IL") finds Israeli braille through its region: *-IL, and FindByRegion("en-NZ") finds the New Zealand tables through their region: *-NZ.
- .NET 10.0 SDK or later
- Windows x64 (currently the only supported platform)
git clone https://github.com/accessmind/sharp-louis.git
cd sharp-louis
dotnet build SharpLouis.sln- Debug: Full debug symbols, no optimization
- Release: Optimized build, no debug symbols
# Debug build
dotnet build SharpLouis.sln -c Debug
# Release build
dotnet build SharpLouis.sln -c Releasedotnet pack src/SharpLouis/SharpLouis.csproj -c ReleaseThe package will be created in the src/SharpLouis/bin/Release/ directory. The version is not set in the project file: it is derived from the git tags by GitVersion, so a release is cut by pushing a v* tag.
dotnet test SharpLouis.slnThe test project exercises the real native liblouis.dll — the tables and the DLL are copied into the test output exactly as a real consumer would get them — so it only runs on Windows x64.
At the repository root:
SharpLouis.sln— the solution, covering the library, the tests and the sample.README.md— this file.CHANGELOG.md— the release history, in Keep a Changelog format.CONTRIBUTING.md— how to contribute.GitVersion.yml— versioning configuration; the package version comes from git tags, not from the project file.
The library lives in src/SharpLouis/:
SharpLouis.csproj— the project file, holding all build and packaging configuration.BrailleTranslator.cs— the main translator class, including the P/Invoke declarations.TableCollection.cs— the fluent API for filtering translation tables.TranslationTable.cs— translation table metadata.LanguageRange.cs— RFC 4647 extended language range matching.TranslationModes.cs— translation mode flags.TypeForm.cs— the typeform enum for emphasis styles.NativeFunctions.cs— the native function enum.LouisException.cs— the exception raised when LibLouis rejects a table or a translation.BrailleTranslationTable/— the metadata constant structures:BrailleContraction.cs,BrailleMode.cs,BrailleType.csandTranslationDirection.cs.build/AccessMind.SharpLouis.targets— MSBuild targets that copy the native assets into a consuming project's output.LibLouis/— the native assets:liblouis.dll(Windows x64),tables.json(table metadata) andtables/(the translation tables themselves).
And alongside it:
tests/SharpLouis.Tests/— the xUnit suite, run against the real native library.samples/SharpLouis.Sample/— a runnable console demo.
All contributions, big or small, are welcome! Please read CONTRIBUTING.md and create an issue before submitting a pull request, thus it will be easier to track everyone's work. Let's improve SharpLouis together!
Copyright © 2024–2026 André Polykanine, AccessMind LLC., and contributors.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at [http://www.apache.org/licenses/LICENSE-2.0].
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and limitations under the License.
Inspired by LibLouis.NET by Leonard de Ruijter.
Heavily based on LibLouis.CSharpWrapper by Jens Jensen.