FireMidge.Libraries.I18n
1.0.0-beta
dotnet add package FireMidge.Libraries.I18n --version 1.0.0-beta
NuGet\Install-Package FireMidge.Libraries.I18n -Version 1.0.0-beta
<PackageReference Include="FireMidge.Libraries.I18n" Version="1.0.0-beta" />
paket add FireMidge.Libraries.I18n --version 1.0.0-beta
#r "nuget: FireMidge.Libraries.I18n, 1.0.0-beta"
// Install FireMidge.Libraries.I18n as a Cake Addin #addin nuget:?package=FireMidge.Libraries.I18n&version=1.0.0-beta&prerelease // Install FireMidge.Libraries.I18n as a Cake Tool #tool nuget:?package=FireMidge.Libraries.I18n&version=1.0.0-beta&prerelease
Internationalisation
Translations
Typically, translations are added via Resource files in C#.NET. However, they have some limitations, e.g. you would add one file per controller, instead of having one file per theme. This means duplicated translations, and no easy way of managing these duplicates. They are also stored in XML rather than YAML or another easy-to-read format. For this solution, we're using a slightly more customised system by using Apache Gettext with PO files, for which you can download a free editor from https://poedit.net/download for any OS. POEdit stores an internal database of translations so you can easily see what you have translated before, and will suggest the same translation again, avoiding duplicated effort. (Supposedly, it can also inherit translations from parent locales.)
We're using the NGettext library for the Gettext functionality. You can read more about Apache Gettext here: https://www.gnu.org/software/gettext/manual
How to edit translations
- Make sure you have Poedit installed.
- Find the correct folder for the locale and domain you're editing. E.g. for the locale "es-ES" and the domain "ErrorMessages", you will find the .po file in:
I18n/es_ES/LC_MESSAGES/ErrorMessages.po
. Open it with Poedit. - Find the string you're looking for, edit, and save. That's it. Poedit will automatically update the .mo file on save and the new translations will be loaded (you may need to restart the service to take effect).
How to add a new translatable string
- Inject the
ITranslator
class into the class from which you need to translate a string. - Call the correct method, depending on the domain. E.g. to translate an error message, call the
ErrorMessage
method, e.g:throw new BadHttpRequestException(_translator.ErrorMessage("Account with ID \"{0}\" not found", accountId));
- Use sequential placeholders for parameters in the string to be translated, ie
{0}
for the first parameter,{1}
for the second, etc. Then pass the arguments in exactly that order. - Now, open the corresponding
.po
file and navigate toTranslation
→Update from source code
. If nothing happens, you may need to set it up first. Go toTranslation
→Properties
→Sources Keywords
. AddErrorMessage
to the list of keywords. The keyword needs to be the name of the method you're calling with the translatable string. So if your domain is "Titles", then you will have a method calledTitle
in theTranslator
, and you need to addTitle
to the list of source keywords. Also make sure that your path (Translation
→Properties
→Sources Paths
) is set to the root of theService
project. You can set it by dragging the folder from your browser into Poedit. Once you've updated the settings (if you had to), tryUpdate from source code
again. - You should see the new string and can set a translation, then Save. If you can't see it, double check your Preferences and the source path.
How to create a new translation domain
- Create a new file in each supported locale with the name of the new domain, e.g. "Titles". If the supported domains are "es-ES", "es-MX" and "de-AT", then you need to create the files
I18n/es_ES/LC_MESSAGES/Titles.po
,I18n/es_MX/LC_MESSAGES/Titles.po
andI18n/de_AT/LC_MESSAGES/Titles.po
. - Add a new method in
Translator
as well as inITranslator
, named after the new domain, which will return strings from the new translation domain. - Open
appsettings.json
, and add the new translation domain to the array underI18n
.TranslationDomains
. - Open Poedit and add a new source path (
Translation
→Properties
→Sources Paths
), named after the name of your new method (ie the new domain). - Use the new method, then in Poedit, go to
Translation
→Update from source code
to pull in the new strings and translate them, then Save. Done.
How to use this library
Add translation files
- In your Presentation layer (typically a project ending in
.Service
), add a folder for your translation files, e.g.I18n
. - Create a sub-folder for each locale you are supporting.
- Inside each locale folder, create another folder called
LC_MESSAGES
- the name of this is hardcoded into theNGettext
library that this library utilises. - Inside the
LC_MESSAGES
folder, create one.po
file for each translation domain, e.g.ErrorMessages.po
. It's important that the name matches the name of the translation domain exactly.
Configure POEdit
- Open the
.po
file with Poedit. InTranslation
→Properties
, set the correct language, depending on which file you're editing. - Under
Sources Paths
, make sure the base path is the root folder of your presentation layer project, not theI18n
folder, as it is by default. - Under
Sources Keywords
, create one entry per translation domain you are planning on using, e.g. for a domain ofErrorMessages
, add an entry namedErrorMessage
(note the singular form in the source path). - As soon as you are actually using a string in your project, you can then go to
Translation
→Update from source code
, and it should populate with all your used strings. If they don't show up, double-check that you are using the correct source path and the right source keyword(s).
Set up Startup
In Startup.cs
, add the following lines (adapting them as necessary):
services.AddTranslationServices(
defaultCulture,
Configuration.GetSection("I18n:SupportedLocales"),
Configuration.GetSection("I18n:TranslationDomains")
);
You can create defaultCulture
like so: new CultureInfo("en-GB");
.
I would recommend to get the default culture either from appsettings.json
or from an .env
file.
SupportedLocales
is an array of other supported locales. These should match the folders you created.
TranslationDomains
is an array of strings with your translation domains.
If you set these up in appsettings.json
, they may look like this:
{
"I18n": {
"SupportedLocales": [
"en-GB",
"en",
"de-AT",
"de",
"es-ES",
"es-MX",
"es"
],
"TranslationDomains": [
"ErrorMessages",
"Titles"
]
}
}
Still in Startup.cs
, in the Configure
method, add these lines (no need to adjust them):
app.UseRequestLocalization(
app.ApplicationServices.GetRequiredService<IOptions<RequestLocalizationOptions>>().Value
);
app.UseMiddleware<LocalisationHandler>();
The first utilises MS's request localisation middleware, which extracts the locale(s) from the Accept-Language
HTTP header, and cross-sections them against the supported locales you set up earlier. If there is an overlap, it will take the exact, or closest one it found and make it available via context.Features.Get<IRequestCultureFeature>()?.RequestCulture
in the next middleware (set up with the 2nd line) - the LocalisationHandler
.
The LocalisationHandler
takes the locale that had been extracted by the RequestLocalisation
middleware, and sends it to the scoped TranslationAdapter
implementation, which in turn loads the relevant translation files.
This allows all other instances in the code, which accept the ITranslationAdapter
implementation, to get strings translated into the requested locale, without having to have access to the locale itself.
Creating an ITranslator
If you were to use the ITranslationAdapter
implementation directly, you need to pass the relevant translation domain as a string every time you need a string translated.
To be more explicit, it is highly recommended to create an ITranslator
interface with one method per translation domain. If your translation domains are ErrorMessages
, SuccessMessages
and Titles
, you would create these three methods (note the singular in the method names):
public interface ITranslator
{
public string ErrorMessage(string text, params object[] args);
public string SuccessMessage(string text, params object[] args);
public string Title(string text, params object[] args);
}
Your concrete implementation would then take the ITranslationAdapter
through the constructor, and forward the calls for each translation domain by hardcoding the names there (or maybe you find another way) - this way, they are in only this one place, and you will have a relatively easy time renaming translation domains.
Remember the POEdit set-up for the source keywords? This refers to the methods that you call with the translation strings. So if you were to use the ITranslationAdapter
directly, the only source keyword you would need is GetString
, which is a standard one in Apache Gettext, which means you wouldn't have to set up any source keywords in POEdit.
Using the ITranslator
To then actually translate a string, simply inject the ITranslator
that you created (remember to tie up the interface with the concrete implementation) and pass a string!
To provide parameters, simply add them to the same method call. To create placeholders in the strings for the variables, use {0}
, {1}
, etc, e.g.:
throw new BadHttpRequestException(_translator.ErrorMessage("Account with ID \"{0}\" not found", accountId));
If, in another language, the parameter order changes, just use e.g. {1}
first and {0}
second. The number refers to the position of the argument you're passing to the method call.
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. |
-
net6.0
- Microsoft.AspNetCore.Localization (>= 2.2.0)
- Microsoft.Extensions.Configuration.Json (>= 6.0.0)
- Microsoft.Extensions.Options (>= 6.0.0)
- NGettext (>= 0.6.7)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
Version | Downloads | Last updated |
---|---|---|
1.0.0-beta | 180 | 4/13/2022 |