Internationalization
Although .NET provides mechanisms for internationalization, they rely on .resx
files which are hard to update and maintain. The Bulletcode.NET framework uses its own set of tools which work consistently in both web and desktop applications and support both back-end .NET code and front-end JavaScript code.
Those tools use the standard .po
file format to store translations during development and the .mo
binary format at runtime. The .po
files can be automatically generated and updated from source code, and can be conveniently edited using tools such as Poedit.
Unlike the GNU gettext tool, the parser used by Bulletcode.NET can handle .cshtml
files (used in ASP.NET Core MVC applications) and .xaml
files (used in WPF applications), in addition to .cs
and .js
source files. It can also extract translations from the DisplayAttribute
and validation attributes.
Updating and Building Translations
To use the automatic tools for updating and building translations, install the bc-tools-dotnet NPM package and create the languages.config.js
file containing information about supported languages and a list of projects for which translations should be generated:
export default {
languages: [
{
name: 'pl-PL',
nplurals: 3,
plural: 'n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2',
},
],
projects: [
{
name: 'App.Desktop',
domain: 'MyApp',
},
'App.Web',
],
};
For each language, the number of plural forms and the rule for selecting the plural form should be specified.
If only the project name is specified, the short form can be used. By default, the domain name is the same as the project name, but a different domain name can be specified. Additional options can also specified for each project:
translations
— an array of languages that are supported by a particular projectfilter
— a function which receives the dictionary of translations and the name of the language as parameters, and can be used to filter translations which are supported in that language (for example, only translations with a particular context)
The bc-i18n
command can be added to the scripts
section of the package.json
file, so that the .po
files can be updated from source files using the following command:
npm run i18n:update
and the .mo
files can be created from .po
files using the following command:
npm run i18n:build
The .po
files and .mo
files are created in the languages
subdirectory of each project. The name of each file contains the translation domain and the language. Translations extracted from JavaScript files are stored in separate files with a .js
suffix appended to the domain name.
In order for the application to be able to load the .mo
translation files, they must to be copied to the output directory. The following item should be added to the .csproj
file in order to do that:
<Content Include="languages\*.mo" CopyToOutputDirectory="PreserveNewest" PackageCopyToOutput="True" />
Using Translations
The main service used for translating strings is ITranslator<T>
, where T
is the class into which the translator is injected. The domain name for the translations is the same as the name of the assembly in which the T
class is defined. The interface contains the following methods:
string _( string text )
— translate the given stringstring _n( string text, string pluralText, long n )
— translate the string with a plural form (selected depending on then
argument)string _p( string context, string text )
— translate the string with given contextstring _pn( string text, string pluralText, long n )
— translate the string with given context and a plural form (selected depending on then
argument)
The context can be used to disambiguate strings which are the same in English but may have different translations depending on the place in which they appear.
Each method also has an overload which accepts additional arguments, which are inserted into the string using standard composite formatting rules. The n
argument is not automatically passed to the format string, so in order to include it, you need to pass it twice:
string label = _translator._p( "{0} item", "{0} items", n, n );
The ITranslatorFactory
service can be used to explicitly obtain a translator for the specific domain, and optionally, specific language.
In some cases it is necessary to translate strings in a context where injecting a service is not possible, for instance in static methods and static classes. In that case, the StaticTranslator<T>
helper class can be used. It contains static methods with the same signature as the ITranslator<T>
service. Note that it is slower than ITranslator<T>
, so it should be avoided if possible.
Bulletcode.NET automatically translates data annotations, including the Name
property of the DisplayAttribute
and the ErrorMessage
property of the attributes which inherit ValidationAttribute
. So the following strings will be automatically translated:
[Display( Name = "First Name" )]
[Required( ErrorMessage = "You must specify the first name." )]
public string FirstName { get; set; }
See the chapter about Validation for more details.
Web Applications
In ASP.NET Core MVC application, the language used when handling a request is automatically determined using built-in request culture providers, based on cookie, query string and Accept-Language
header. Bulletcode.NET simplifies the configuration of this mechanism.
Both the translation services and language options are automatically registered when the AddCoreMvcServices()
method is called (see the MVC chapter).
By default, the "en-US"
language is used and no other languages are available. To change the default settings, add the LanguageOptions
section to appsettings.json
, for example:
"LanguageOptions": {
"DefaultCulture": "pl-PL",
"SupportedCultures": "pl-PL;uk"
}
The SupportedCultures
option can be set to a semicolon-separated list of culture names which can be used by the auto-detection mechanism. If this option not specified, only the default culture is available.
You can also specify the CookieName
option to specify the name of the cookie used to select the language. The default value is ".BC.Language"
. Setting the cookie name to null
disables the cookie request culture provider. The cookie takes precedence over the Accept-Language
, and the query string provider is removed by Bulletcode.NET.
In order to translate strings used in controllers and other request-scoped services, inject the ITranslator<T>
service described above. In singleton services, use the ITranslatorFactory
to make sure that the current request’s culture is used.
Property names and validation errors used in models are automatically translated, as described above.
In Razor views (.cshtml
files), the translation methods (_
, _n
, _p
and _pn
) are available directly. They are defined in the BaseRazorPage
class which should be used as the base class for all views (see the MVC chapter for more details). For example:
<h1>@_( "Hello, world" )</h1>
Client-Side Code
In order to use translations in JavaScript, create a translator.js
file with the following contents:
import { translator } from '@bc-front/utils/i18n';
export const { _, _n, _p, _pn } = translator( 'App.Web.js' );
This creates translation functions for the specified domain (which always has a .js
suffix). You can then import and use those functions anywhere in your JavaScript code.
Note that the translation functions in JavaScript also support passing additional arguments which will replace placeholders in the translated strings. However, only simple numeric placeholders, such as {0}
, {1}
, etc., can be used, and formatting options are not supported.
The @bc-front/utils/i18n
module contains methods for formatting numbers, dates and time according to the current locale, such as:
asDecimal( value, decimals = 0 )
— format a number with thousand separators and a fixed number of decimal placesasNumber( value )
— format a number with localized decimal separator and a variable number of decimal placesasCurrency( value )
— format a number with two decimal places and a currency prefix or suffixformatLocalDate( value )
— format a date using the localized formatformatLocalDatetime( value )
— format a date and time using the localized formatformatLocalLongDatetime( value )
— format a date and time (with seconds) using the localized formatformatLocalTime( value )
— format a time using the localized format
Note that date and datetime values are passed as objects with year
, month
, day
, hours
, minutes
and seconds
properties. This structure is used by most functions exported by the @bc-front/utils/date
module, for example now()
and fromDate()
(which converts a JavaScript Date
into the object format). The @bc-front/utils/i18n
module also contains methods for converting strings in internal, locale-independent format (yyyy-MM-dd HH:mm:ss
) to date objects and vice-versa.
Internationalization data, including both locale (number and date formats, etc.), and translations for domains with the .js
suffix, is injected into every page as the i18nData
global variable. This is done automatically by the I18nAssetBundle
class which is part of Bulletcode.Web.Core. The application can also compile translations into separate JavaScript files in the asset bundle; see Assets for more information.
Desktop Applications
In desktop applications, the AddTranslations()
and AddLanguageOptions()
extension methods of the IServiceCollection
should be called explicitly in order to register translation services and language options.
By default, the "en-US"
language is used and no other languages are available. To change the default settings, add the LanguageOptions
section to appsettings.json
:
"LanguageOptions": {
"DefaultCulture": "pl-PL",
"SupportedCultures": "pl-PL;uk"
}
The SupportedCultures
setting can be set to a semicolon-separated list of culture names which can be used by the application. If it’s not specified, only the default culture is available. The actual language used by the application is determined at startup based on the Windows settings.
In order to translate strings used in view models and other services, inject the ITranslator<T>
service. Property names and validation errors in view models are automatically translated as described above.
In XAML files, you can use the following markup extensions to translate strings:
i18n:Translate
— translate the given string. An optionalContext
property can also be specified.i18n:Format
— convert a value to a string using the translated format string (with a{0}
placeholder). An optionalPluralText
andContext
can be specified. WhenPluralText
is specified, the converted value must be a number and is used to select the plural form.i18n:MultiFormat
— convert multiple values to a string using the translated format string (with{0}
,{1}
, etc. as placeholders). WhenPluralText
is specified, the n-th value determined by thePluralIndex
(which defaults to 0) must be a number and is used to select the plural form.
Note that when the text starts with a placeholder, such as {0}
, it must be preceded by an empty pair of brackets to avoid being interpreted as a XAML extension. Also note that the i18n
prefix must be mapped to the Bulletcode.Desktop.I18n
namespace in the Bulletcode.Desktop.Wpf assembly.
The domain name for the translations is the same as the name of the assembly in which the XAML file is defined.
The following example shows all supported markup extensions:
<UserControl
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:i18n="clr-namespace:Bulletcode.Desktop.I18n;assembly=Bulletcode.Desktop.Wpf"
>
<StackPanel>
<Label>
<i18n:Translate>Static text<i18n:Translate>
</Label>
<Label Content="{Binding Count, Converter={i18n:Format '{}{0} item', PluralText='{}{0} items'}}"/>
<Label>
<MultiBinding Converter="{i18n:MultiFormat '{}{0} h {1} min'}">
<MultiBinding.Bindings>
<Binding Path="Hours"/>
<Binding Path="Minutes"/>
</MultiBinding.Bindings>
</MultiBinding>
</Label>
</StackPanel>
</UserControl>