Go to content

Bulletcode.NET

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 project
  • filter — 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 string
  • string _n( string text, string pluralText, long n ) — translate the string with a plural form (selected depending on the n argument)
  • string _p( string context, string text ) — translate the string with given context
  • string _pn( string text, string pluralText, long n ) — translate the string with given context and a plural form (selected depending on the n 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 places
  • asNumber( value ) — format a number with localized decimal separator and a variable number of decimal places
  • asCurrency( value ) — format a number with two decimal places and a currency prefix or suffix
  • formatLocalDate( value ) — format a date using the localized format
  • formatLocalDatetime( value ) — format a date and time using the localized format
  • formatLocalLongDatetime( value ) — format a date and time (with seconds) using the localized format
  • formatLocalTime( 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 optional Context property can also be specified.
  • i18n:Format — convert a value to a string using the translated format string (with a {0} placeholder). An optional PluralText and Context can be specified. When PluralText 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). When PluralText is specified, the n-th value determined by the PluralIndex (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>