Giter Club home page Giter Club logo

i18n's Introduction

i18n (v2)

Build Status

Smart internationalization for ASP.NET

    PM> Install-Package i18N

Introduction

The i18n library is designed to replace the use of .NET resources in favor of an easier, globally recognized standard for localizing ASP.NET-based web applications.

Platforms Supported

i18n itself targets .NET Framework 4, and works with websites and web applications based on ASP.NET v4 and above, including:

  • ASP.NET MVC
  • ASP.NET Web API
  • ASP.NET WebMatrix / Web Pages
  • ASP.NET Web Forms

Features

  • Leverages the GetText / PO ecosystem: localize like the big kids
  • Localize everything: HTML, Razor, C#, VB, JavaScript, .NET attributes and data annotations, ...
  • SEO-friendly: language selection varies the URL, and Content-Language is set appropriately
  • Automatic: no URL/routing changes required in the app
  • High performance, minimal overhead and minimal heap allocations
  • Unit testing support
  • Smart: knows when to hold them, fold them, walk away, or run, based on i18n best practices

Project Configuration

The i18n library works by modifying your HTTP traffic to perform string replacement and patching of URLs with language tags (URL Localization). The work is done by an HttpModule called i18n.LocalizingModule which should be enabled in your web.config file as follows:

  <system.web>
    <httpModules>
      <add name="i18n.LocalizingModule" type="i18n.LocalizingModule, i18n" />
    </httpModules>
  </system.web>
  <system.webServer> <!-- IIS7 'Integrated Mode'-specific config -->
    <modules>
      <add name="i18n.LocalizingModule" type="i18n.LocalizingModule, i18n" />
    </modules>
  </system.webServer>

Note: The <system.web> element is added for completeness and may not be required.

The following <appSettings> are then required to specify the type and location of your application's source files:

  <appSettings>
    <add key="i18n.DirectoriesToScan" value=".." /> <!-- Rel to web.config file -->
    <add key="i18n.WhiteList" value="*.cs;*.cshtml;*.sitemap" />
    <add key="i18n.BlackList" value=".\js\kendo;.\js\angular;.\*\dist" />
  </appSettings>

The following configuration options are optional. i18n.DisableReferences allows you to generate lighter pot/po files by deleting references to your translation tokens (nuggets) and i18n.GenerateTemplatePerFile generates a pot file per file scanned and merges all po files into messages.po:

  <appSettings>
    <add key="i18n.DisableReferences" value="true" />
    <add key="i18n.GenerateTemplatePerFile" value="true" />
  </appSettings>

Certain behaviours of i18n may be altered at runtime on application startup. The following code shows the most common options:

    public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            // Change from the default of 'en'.
            i18n.LocalizedApplication.Current.DefaultLanguage = "fr";

            // Change from the default of 'i18n.langtag'.
            i18n.LocalizedApplication.Current.CookieName = "i18n_langtag";

            // Change from the of temporary redirects during URL localization
            i18n.LocalizedApplication.Current.PermanentRedirects = true;

            // This line can be used to disable URL Localization.
            //i18n.UrlLocalizer.UrlLocalizationScheme = i18n.UrlLocalizationScheme.Void;

            // Change the URL localization scheme from Scheme1.
            i18n.UrlLocalizer.UrlLocalizationScheme = i18n.UrlLocalizationScheme.Scheme2;

            // Change i18n's expectation for the ASP.NET application's virtual application root path on the server, 
            // used by Url Localization. Defaults to "/".
            //i18n.LocalizedApplication.Current.ApplicationPath = "/mysite";

            // Specifies whether the key for a message may be assumed to be the value for
            // the message in the default language. Defaults to true.
            //i18n.LocalizedApplication.Current.MessageKeyIsValueInDefaultLanguage = false;

            // Specifies a custom method called after a nugget has been translated
            // that allows the resulting message to be modified, for instance according to content type.
            // See [Issue #300](https://github.com/turquoiseowl/i18n/issues/300) for example usage case.
            i18n.LocalizedApplication.Current.TweakMessageTranslation = delegate(System.Web.HttpContextBase context, i18n.Helpers.Nugget nugget, i18n.LanguageTag langtag, string message)
            {
                switch (context.Response.ContentType)
                {
                    case "text/html":
                        return message.Replace("\'", "&apos;");
                }
                return message;
            };

            // Blacklist certain URLs from being 'localized' via a callback.
            i18n.UrlLocalizer.IncomingUrlFilters += delegate(Uri url) {
                if (url.LocalPath.EndsWith("sitemap.xml", StringComparison.OrdinalIgnoreCase)) {
                    return false; }
                return true;
            };

            // Extend (+=) or override (=) the default handler for Set-PAL event.
            // The default handler applies the setting to both the CurrentCulture and CurrentUICulture
            // settings of the thread, as shown below.
            i18n.LocalizedApplication.Current.SetPrincipalAppLanguageForRequestHandlers = delegate(System.Web.HttpContextBase context, ILanguageTag langtag)
            {
                // Do own stuff with the language tag.
                // The default handler does the following:
                if (langtag != null) {
                    Thread.CurrentThread.CurrentCulture = Thread.CurrentThread.CurrentUICulture = langtag.GetCultureInfo(); }
            };

            // Blacklist certain URLs from being translated using a regex pattern. The default setting is:
            //i18n.LocalizedApplication.Current.UrlsToExcludeFromProcessing = new Regex(@"(?:\.(?:less|css)(?:\?|$))|(?i:i18nSkip|glimpse|trace|elmah)");

            // Whitelist content types to translate. The default setting is:
            //i18n.LocalizedApplication.Current.ContentTypesToLocalize = new Regex(@"^(?:(?:(?:text|application)/(?:plain|html|xml|javascript|x-javascript|json|x-json))(?:\s*;.*)?)$");

            // Change the types of async postback blocks that are localized
            //i18n.LocalizedApplication.Current.AsyncPostbackTypesToTranslate = "updatePanel,scriptStartupBlock,pageTitle";

            // Change which languages are parsed from the request, like skipping  the "Accept-Language"-header value. The default setting is:
            //i18n.HttpContextExtensions.GetRequestUserLanguagesImplementation = (context) => LanguageItem.ParseHttpLanguageHeader(context.Request.Headers["Accept-Language"]);

            // Override the i18n service injection. See source code for more details!
            //i18n.LocalizedApplication.Current.RootServices = new Myi18nRootServices();
        }
    }

Usage

To localize text in your application, surround your strings with [[[ and ]]] markup characters to mark them as translatable. That's it. Here's an example of localizing text in a Razor view:

    <div id="content">
        <h2>[[[Welcome to my web app!]]]</h2>
        <h3><span>[[[Amazing slogan here]]]</span></h3>
        <p>[[[Ad copy that would make Hiten Shah fall off his chair!]]]</p>
        <span class="button" title="[[[Click to see plans and pricing]]]">
            <a href="@Url.Action("Plans", "Home", new { area = "" })">
                <strong>[[[SEE PLANS & PRICING]]]</strong>
                <span>[[[Free unicorn with all plans!]]]</span>
            </a>
        </span>
    </div>

And here's an example in an MVC controller:

    using i18n;
    
    namespace MyApplication
    {
        public class HomeController : Controller
        {
            public ActionResult Index()
            {
                ViewBag.Message = "[[[Welcome to ASP.NET MVC!]]]";

                return View();
            }
        }
    }

At last, you can localize your data annotations as easy as this:

    public class PasswordResetViewModel
    {
        [Required(ErrorMessage="[[[Please fill in this field]]]")]
        [Email(ErrorMessage = "[[[Email not yet correct]]]")]
        [Display(Name = "[[[Email Address]]]")]
        public string Email
        {
            get;
            set;
        }
    }

And localize arguments passed to MVC URL-Helpers or other functions that require a plain string:

@Html.LabelFor(m => m.Name, "[[[First Name]]]")

And for Javascript:

    <script type="text/javascript">
        $(function () {
            alert("[[[Hello world!]]]");
        });
    </script>

Nuggets

In PO terminology, strings you want to be translatable are known as messages. In i18n, messages are 'marked-up' in your source code as 'Nuggets'. The nugget markup allows i18n to filter the HTTP response looking for the message strings which are replaced with translated strings, where available. They also allow message strings to be located by the PostBuild PO file generator.

A simple nugget looks like this:

[[[translate me]]]

This defines a message with the key (aka msgid) of "translate me".

Nugget markup supports formated messages as follows:

string.Format("[[[welcome %1, today is %0|||{0}|||{1}]]]", day, name)

where the %0 and %1 tokens are replaced by the strings that replace the {0} and {1} items, respectively. (The reason for the extra level of redirection here is to facilitate the translator rearranging the order of the tokens for different languages.)

Nugget transformation supports translation of the arguments as follows:

[DisplayName("[[[CountryCode]]]")]
[MaxLength(20, ErrorMessage="[[[%0 must be %1 characters or less|||(((CountryCode)))|||20]]]")]
public string CountryCode { get; set; }

where the Nugget markup will first replace (((CountryCode)) with the translated text and then merge the translated value into the main message.

Nugget markup supports comments (extracted comments in PO terminology) to be passed to the translator like so:

[[[translate me///this is an extracted comment]]]

And if you need to include the markup characters themselves within a message, you can HTML-escape them, for example:

[[[Please don't forget to add GoogleAd tags: [googleadsmall&#93;]]]

where &#93; is the HTML escape sequence for ]. The relevant escape sequences are:

  • / = &#47;
  • [ = &#91;
  • ] = &#93;
  • | = &#124;

See Issue #50 for more on Nuggets and why we have chosen to replace the GetText / _() style of marking-up messages.

Nugget markup customization

The character sequences for marking-up nuggets ([[[, ]]], |||, (((, ))) and ///) were chosen on the basis that they were unlikely to clash with common character sequences in HTML markup while at the same time being convenient for the programmer to enter (on most keyboards).

However, recognizing that a clash remains possible and nuggets thereby being falsely detected in source code or the HTML response, i18n allows you to define your own sequences for the markup which you know are not going to clash. You can configure these in web.config as follows:

  <appSettings>
    ...
    <add key="i18n.NuggetBeginToken" value="[&[" />
    <add key="i18n.NuggetEndToken" value="]&]" />
    <add key="i18n.NuggetDelimiterToken" value="||||" />
    <add key="i18n.NuggetCommentToken" value="////" />
    <add key="i18n.NuggetParameterBeginToken" value="(((" />
    <add key="i18n.NuggetParameterEndToken" value=")))" />
    ...
  </appSettings>

Message Visualization

i18n can be configured to visualize all processed messages. This is useful when testing your app to verify that all messages are tagged correctly. To enable this feature:

  <appSettings>
    ...
    <add key="i18n.VisualizeMessages" value="true" />
    <add key="i18n.NuggetVisualizeToken" value="!" />
    ...
  </appSettings>

When VisualizeMessages is active the NuggetVisualizeToken will be inserted at start and end of each translated message.

Two more optional parameters can be used to further customize the message visualization. i18n.VisualizeLanguageSeparator This enables display of the language tag that was use to localize each message. The language tag will be shown before each message, separated from the message by this parameter value. If the value is a blank string or the parameter is not present then language tags are not shown in message visualizations. i18n.NuggetVisualizeEndToken This allows for using different start and end tokens for visualizing messages. When this value is specified then the NuggetVisualizeToken will be inserted at start of each translated message and the NuggetVisualizeEndToken will be inserted at end of each translated message.

For example, to display language tags separated from messages by a colon, and add brackets to enclose the visualized messages, use the following message visualization configuration.

  <appSettings>
    ...
    <add key="i18n.VisualizeMessages" value="true" />
    <add key="i18n.VisualizeLanguageSeparator" value=":" />
    <add key="i18n.NuggetVisualizeToken" value="![" />
    <add key="i18n.NuggetVisualizeEndToken" value="]!" />
    ...
  </appSettings>

Message Context Support

i18n allows you to assign a msgctxt value to each message. The value of the msgctxt is taken from any comment you have defined in the nugget. This feature is optional and disabled by default. To enable this feature:

  <appSettings>
    ...
    <add key="i18n.MessageContextEnabledFromComment" value="true" />
    ...
  </appSettings>

Note that note all PO editors support msgctxt and indeed may be thrown by the value when present in .PO files. See Issue #90 for more details.

Multi-line messages

The PO spec supports messages that span multiple lines. i18n provides full support for these, simply by spreading the nugget over several lines.

For example, the following nugget is perfectly legal and should appear in your PO editor as a multi-line message:

[[[This is a
message spread over
three lines]]]

Static File Compression and i18n

The i18n module localizes nuggets in the HTTP response by modifying the response stream using a response filter (see the .NET Framework documentation for more info about the HttpResponse.Filter property). If the response stream is compressed before it reaches the i18n module then the module does not modify the stream. Currently the module is not designed to intercept static file requests before compression happens.

Two checks are implemented to ensure that the module does not modify compressed response streams:

  1. In i18n.LocalizingModule there is a check to see if the response Content-Encoding header is set to "gzip" and if it is then the module does not install the response filter.
  2. In i18n.ResponseFilter the stream content is checked for the presence of the gzip file format magic number (the first two bytes of a gzip file are set to 1F 8B). If the magic number is found at the beginning of the stream then the content is passed through without modification by the filter.

Because of the way that static file compression works in IIS, some responses to static files requests do not get compressed, so if you have static file compression enabled (it is enabled by default) AND you have nuggets within the content of a static file, then the response received by a client will be localized when the response is not compressed and it will not be localized when the response is compressed. In order to prevent this, it is important that you decide whether or not you will localize static files on your site because you need to do one of the following:

  1. If you want to use nuggets and localize static files - disable static file compression. This means that you will not get the benefit of the bandwidth savings of compressing static files, but if you are localizing static files then you have essentially taken the decision to make the static files dynamic.
  2. If you do not need to use nuggets and localize static files - leave static file compression enabled. You will now get the benefit of the bandwidth savings of compressing static files, but it is important that you must not put nuggets in the static files.

Note: Refer to Issue #163 for more on IIS compression settings.

Note: in some scenarios, it might be desirable to localize Javascipt (.js) files, making your application case 1. To easily disable static file compression, use the IIS manager and click on site name, Compression and uncheck "Enable static content compression".

Note: The Microsoft ScriptManager compresses responses to requests for ScriptResource.axd so these responses will always be compressed and the script that is returned by the ScriptManager will not be localized even if you disable static file compression.

Building PO databases

To set up automatic PO database building, add the following post-build task to your project, after adding i18n.PostBuild.exe as a project reference:

    "$(TargetDir)i18n.PostBuild.exe" "$(ProjectDir)\web.config"

You can find i18n.PostBuild.exe file under packages folder:

    {your_solution}\packages\{package_version}\tools\i18n.PostBuild\i18n.PostBuild.exe

Alternatively, you may choose to install the i18n.POTGenerator.vsix Visual Studio extension (2012/2013). This installs an i18n button in the Solution Window for manual triggering of PO generation. Note that it is necessary to highlight the project in question within the Solution Window before pressing the button.

The PO generator will rip through your source code (as defined by the i18n.DirectoriesToScan and i18n.WhiteList settings in web.config), finding every nugget, and uses this to build a master .POT template file located at locale/messages.pot relative to your web application folder. After the new template is constructed, any locales that exist inside the locale folder (or as defined by the i18n.AvailableLanguages semi-colon-delimited web.config setting) are automatically merged with the template, so that new strings can be flagged for further translation.

From here, you can use any of the widely available PO editing tools (like POEdit) to provide locale-specific text and place them in your locale folder relative to the provided language, e.g. locale/fr. If you change a PO file on the fly, i18n will update accordingly; you do not need to restart your application. Note that the locale-specific file must be named messages.po. For example, your locale folder structure will be similar to (three languages, fr, es, and es-MX are defined):

locale/messages.pot
locale/fr/messages.po
locale/es/messages.po
locale/es-MX/messages.po

Custom Modifications To Translations

Nuggets translations can be modified at runtime as follows:

    protected void Application_Start()
    {
        ...
        // Specifies a custom method called after a nugget has been translated
        // that allows the resulting message to be modified, for instance according to content type.
        // See [Issue #300](https://github.com/turquoiseowl/i18n/issues/300) for example usage case.
        i18n.LocalizedApplication.Current.TweakMessageTranslation = delegate(System.Web.HttpContextBase context, i18n.Helpers.Nugget nugget, i18n.LanguageTag langtag, string message)
        {
            switch (context.Response.ContentType)
            {
                case "text/html":
                    return message.Replace("\'", "&apos;");
            }
            return message;
        };
    }

PO customization

i18n allows you to change the PO file name to use and use PO files from other sources (when working with multiple projects for example). To enable this feature, you can set :

  <appSettings>
    ...
    <add key="i18n.LocaleFilename" value="messages" />
    <add key="i18n.LocaleOtherFiles" value="external1;external2" /><!-- relative path from the directory of {LocaleFilename}.po-->
    ...
  </appSettings>

Note : i18n.LocaleOtherFiles paths are relative to the directory of the file {i18n.LocaleFilename}.po (messages.po by default).

URL Localization

In keeping with emerging standards for internationalized web applications, i18n provides support for localized URLs. For example, www.example.com/de or www.example.com/en-us/signin.

Out of the box, i18n will attempt to ensure the current language for any request is shown correctly in the address box of the user's browser, redirecting from any non-localized URL if necessary to a localized one. This is known as Early URL Localization. See also Principal Application Language.

While URLs from the user-agent perspective are localized, from the app's perspective they are nonlocalized. Thus you can write your app without worrying about the language tag in the URL.

The default URL Localization scheme (Scheme1) will show the language tag in the URL always; an alternative scheme, Scheme2, will show the language tag only if it is not the default.

Disabling URL Localization

URL localization can be disabled by setting the scheme to i18n.UrlLocalizationScheme.Void in Application_Start:

    protected void Application_Start()
    {
        ...
        // Disable URL Localization.
        i18n.UrlLocalizer.UrlLocalizationScheme = i18n.UrlLocalizationScheme.Void;
    }

Without URL localization, i18n will rely on the cookie "i18n.langtag" to determine the current language for each request. This means that the language change/setting feature on your site should change the cookie and set the new PrincipalAppLanguage:

  HttpCookie c = new HttpCookie("i18n.langtag") { 
    Value = Request.QueryString("newLanguage"), 
    HttpOnly = true, 
    Expires = DateTime.UtcNow.AddYears(1) 
    };
  Response.Cookies.Add(c);
  i18n.ILanguageTag p = default(i18n.ILanguageTag);
  p = i18n.LanguageTag.GetCachedInstance(Request.QueryString("newLanguage"));
  i18n.HttpContextExtensions.SetPrincipalAppLanguageForRequest(this.Context, p);

If you are experiencing problems with static content, maybe also related to browser caching and are having trouble getting the rules for URL exclusion in the following paragraphs to work, the Void scheme might we worth looking into. Please see Issue #385.

Exclude URLs from being localized

URLs to non-internationalized resources need not be localized. Typically, there is no harm in them being localized as i18n will route the request approriately either way. However, where the Principal Application Language for a request is not required, such as for when reading a CSS file or font file, it can save a redirection round trip by instructing i18n NOT to localize the URL.

There are two ways to instruct i18n NOT to localize a URL:

Firstly, you can set a RegEx pattern to match against the localpath part of the URLs to be excluded. For instance:

    protected void Application_Start()
    {
        ...
        // Blacklist certain URLs from being 'localized'.
        i18n.UrlLocalizer.QuickUrlExclusionFilter = new System.Text.RegularExpressions.Regex(@"(^\/api\/)|((sitemap\.xml|\.css|\.less|\.jpg|\.jpeg|\.png|\.gif|\.ico|\.svg|\.woff|\.woff2|\.ttf|\.eot)$)", RegexOptions.IgnoreCase);
    }

Indeed, the default value for the QuickUrlExclusionFilter settings is as shown above however feel free to override or set to null to disable.

For finer control, the second method is to define filter delegates that are passed the URL and return true if the URL is to be localized, otherwise false. For example:

    protected void Application_Start()
    {
        ...
        // Blacklist certain URLs from being 'localized'.
        i18n.UrlLocalizer.IncomingUrlFilters += delegate(Uri url) {
            if (url.LocalPath.EndsWith("sitemap.xml", StringComparison.OrdinalIgnoreCase)) {
                return false; }
            return true;
        };
        i18n.UrlLocalizer.OutgoingUrlFilters += delegate(string url, Uri currentRequestUrl) {
            Uri uri;
            if (Uri.TryCreate(url, UriKind.Absolute, out uri)
                || Uri.TryCreate(currentRequestUrl, url, out uri)) {
                if (uri.LocalPath.EndsWith("sitemap.xml", StringComparison.OrdinalIgnoreCase)) {
                    return false; }
            }
            return true;
        };
    }

Conditionally ignore localization for a specific URL

There are very rare cases where you need to conditionally bypass the URL localization for a specific URL. One example is when generating hreflang tags when using i18n with Scheme2.

You can do this by prefixing the URL like so:

    <link rel="alternate" hreflang="en" href="@(EarlyUrlLocalizer.IgnoreLocalizationUrlPrefix)http://mysite.com" />
    <link rel="alternate" hreflang="fr" href="http://mysite.com/fr" />
    <link rel="alternate" hreflang="es" href="http://mysite.com/es" />

When i18n goes through the process for localizing outgoing URLs, this prefix will be stripped and the rendered URL will be left non-localized.

Note that this method of ignoring URL localization should not be widespread and is included to address edge cases. Most use cases that require ignoring URL localization can be solved more eloquently by making use of the UrlLocalizer filters.

Principal Application Language

During startup of your ASP.NET application, i18n determines the set of application languages for which one or more translated messages exist.

Then, on each request, one of these languages is selected as the Principal Application Language (PAL) for the request.

The PAL for the request is determined by the first of the following conditions that is met:

For i18n.UrlLocalizationScheme.Scheme1:

  1. The path component of the URL is prefixed with a language tag that matches exactly one of the application languages. E.g. "example.com/fr/account/signup".

  2. The path component of the URL is prefixed with a language tag that matches loosely one of the application languages (see below).

  3. The request contains a cookie called "i18n.langtag" with a language tag that matches (exactly or loosely) one of the application languages.

  4. The request contains an Accept-Language header with a language that matches (exactly or loosely) one of the application languages.

  5. The default application language is selected (see also Per-Request Default Language Determination).

For i18n.UrlLocalizationScheme.Scheme2:

  1. The path component of the URL is prefixed with a language tag that matches exactly one of the application languages. E.g. "example.com/fr/account/signup".

  2. The path component of the URL is prefixed with a language tag that matches loosely one of the application languages (see below).

  3. The default application language is selected (see also Per-Request Default Language Determination).

Where a loose match is made above, the URL is updated with the matched application language tag and a redirect is issued. E.g. "example.com/fr-CA/account/signup" -> "example.com/fr/account/signup". By default this is a temporary 302 redirect, but you can choose for it to be a permanent 301 one by setting i18n.LocalizedApplication.Current.PermanentRedirects = true in Application_Start.

The GetPrincipalAppLanguageForRequest extension method to HttpContext can be called to access the PAL of the current request. For example, it may be called in a Razor view as follows to display the current langue to the user:

    @using i18n

    <div>
        <p id="lang_cur" title="@Context.GetPrincipalAppLanguageForRequest()">
            @Context.GetPrincipalAppLanguageForRequest().GetNativeNameTitleCase()
        </p>
    </div>

Similarly, the HTML lang attribute can be set as follows:

    @using i18n

    <html lang="@Context.GetPrincipalAppLanguageForRequest()">
        ...
    </html>

Per-Request Default Language Determination

When the PAL algorithm falls back on the default language for the application, i18n supports a simple delegate-based hook for providing the default language based on the current request, typically based on the URL.

For example, suppose you wish the default language to vary as follows:

  1. mydomain.co.uk -> 'en'
  2. mydomain.fr -> 'fr'

This can be achieved as follows:

    protected void Application_Start()
    {
        ...
        i18n.LocalizedApplication.Current.DefaultLanguage = "en";
        i18n.UrlLocalizer.UrlLocalizationScheme = i18n.UrlLocalizationScheme.Scheme2;
        i18n.UrlLocalizer.DetermineDefaultLanguageFromRequest = delegate(HttpContextBase context)
        {
            if (context != null && context.Request.Url.Host.EndsWith(".fr", StringComparison.OrdinalIgnoreCase)) {
                return i18n.LanguageTag.GetCachedInstance("fr"); }
            return i18n.LocalizedApplication.Current.DefaultLanguageTag;
        };
    }

Notice how the URL localization scheme has been switched to Scheme2 which allows the URL to be without any language tag. The default scheme (Scheme1) would enforce a redirection so that the URL always contains the current language tag.

Explicit User Language Selection

You can provide a language selection feature in your application using i18n. There are two parts to implementing this feature which revolve around the setting of a cookie called i18n.langtag.

Firstly, provide HTML that displays the current language and allows the user to explicitly select a language (from those application languages available).

An example of how to do that in ASP.NET MVC and Razor follows:

@using i18n
...
<div id="language">
  <div>
    <p id="lang_cur" title="@Context.GetPrincipalAppLanguageForRequest()">@Context.GetPrincipalAppLanguageForRequest().GetNativeNameTitleCase()</p>
  </div>
  <div id="lang_menu" style="display: none;">
    <table class="table_grid">
      <tbody>
        @{
          int i;
          int maxcols = 3;
          KeyValuePair<string, i18n.LanguageTag>[] langs = LanguageHelpers.GetAppLanguages().OrderBy(x => x.Key).ToArray();
          int cellcnt = langs.Length +1;
          for (i = 0; i < cellcnt;) {
            bool lastRow = i + maxcols >= cellcnt;
            <tr class="@(Html.Raw((i % 2) == 0 ? "even":"odd")) @(Html.Raw(lastRow ? "last":""))">
              @for (int j = 0; j < maxcols && i < cellcnt; ++i, ++j) {
                string langtag;
                string title;
                string nativelangname;
                if (i == 0) {
                  langtag = "";
                  title = "[[[Browser default language setting]]]";
                  nativelangname = "[[[Auto]]]";
                }
                else {
                  i18n.LanguageTag lt = langs[i -1].Value;
                  title = langtag = lt.ToString();
                  nativelangname = lt.NativeNameTitleCase;
                }
                <td>
                  @Html.ActionLink(
                    linkText: nativelangname, 
                    actionName: "SetLanguage", 
                    controllerName: "Account", 
                    routeValues: new { langtag = langtag, returnUrl = Request.Url },
                    htmlAttributes: new { title = title } )
                </td>
              }
              @* Fill last row with empty cells if ness, so that borders are added and balanced out. *@
              @if (lastRow) {
                for (; i % maxcols != 0; ++i) {
                  <td></td>
                }
              }
            </tr>
          }
        }
      </tbody>
    </table>
  </div>
</div>

On selection of a language in the above code, the AccountController.SetLanguage method is called. For example:

    using i18n;
    ...

    //
    // GET: /Account/SetLanguage

    [AllowAnonymous]
    public ActionResult SetLanguage(string langtag, string returnUrl)
    {
        // If valid 'langtag' passed.
        i18n.LanguageTag lt = i18n.LanguageTag.GetCachedInstance(langtag);
        if (lt.IsValid()) {
            // Set persistent cookie in the client to remember the language choice.
            Response.Cookies.Add(new HttpCookie("i18n.langtag")
            {
                Value = lt.ToString(),
                HttpOnly = true,
                Expires = DateTime.UtcNow.AddYears(1)
            });
        }
        // Owise...delete any 'language' cookie in the client.
        else {
            var cookie = Response.Cookies["i18n.langtag"];
            if (cookie != null) {
                cookie.Value = null;
                cookie.Expires = DateTime.UtcNow.AddMonths(-1);
            }
        }
        // Update PAL setting so that new language is reflected in any URL patched in the 
        // response (Late URL Localization).
        HttpContext.SetPrincipalAppLanguageForRequest(lt);
        // Patch in the new langtag into any return URL.
        if (returnUrl.IsSet())
        {
            if (LocalizedApplication.Current.UrlLocalizerForApp.FilterOutgoing(returnUrl, HttpContext.Request.Url)) // if url wants to be localized
            {
                returnUrl = LocalizedApplication.Current.UrlLocalizerForApp.SetLangTagInUrlPath(HttpContext, returnUrl, UriKind.RelativeOrAbsolute, lt?.ToString()).ToString();
            }
        }
        // Redirect user agent as approp.
        return this.Redirect(returnUrl);
    }

How to get a translation of a nugget in your C# code

With i18n you can access the translation for a given nugget msgid from any code that is handling an ASP.NET request. There is a GetText extension method to HttpContextBase provided for this.

For example, you can do the following from within an MVC controller action:

using System;
using System.Web.Mvc;
using i18n;

namespace MyWebSite.Controllers
{
    public class MyController : Controller
    {
        public ActionResult Welcome()
        {
            string welcomeMessage = HttpContext.GetText("Welcome to the my website.", "");

            // Do something with the string...

            return View();
        }
    }
}

Essentially, anywhere you have access to an HttpContextBase or HttpContext instance, you can get a correct translation for a given nugget msgid / msgcomment combo.

The msgcomment is relevant only when i18n.Domain.Concrete.i18nSettings.MessageContextEnabledFromComment is set to true; by default it is false and so msgcomment argument should be passed as null or empty.

Furthermore, you can access the translation of a complete body of text containing zero or more nuggets that require parsing using the ParseAndTranslate extension method to HttpContextBase, as follows:

    string entity = HttpContext.ParseAndTranslate("Hi - [[[Sign in]]]");

or if outside of an HttpContext, for example a background job running an emailing service task:

    string entity = i18n.LanguageHelpers.ParseAndTranslate("[[[Thank you for your payment]]]");

which will translate using the app's default language (i18n.LocalizedApplication.DefaultLanguage), or

    string entity = i18n.LanguageHelpers.ParseAndTranslate("[[[Thank you for your payment]]]", "fr-CA;q=1,fr;q=0.5");

which will use the app language that best matches those specified, or better still

    // During earlier HTTP request from user, save their language(s).
    string userLanguages = HttpContext.GetRequestUserLanguagesAsString();

    // Switch to background job.
    HostingEnvironment.QueueBackgroundWorkItem(ct => {

        // Translate using user's languages obtained earlier.
        string entity = i18n.LanguageHelpers.ParseAndTranslate("[[[Thank you for your payment]]]", userLanguages);
        ...

    });

which will match against the languages obtained from the user's browser at some point earlier.

Language Matching

Language matching is performed when a list of one or more user-preferred languages is matched against a list of one or more application languages, the goal being to choose the application languages which the user is most likely to understand. The algorithm for this is multi-facted and multi-pass and takes the Language, Script and Region subtags into account.

Matching is performed once per-request to determine the Principal Application Language for the request, and also once per message to be translated (aka GetText call). The multi-pass approach ensures a thorough attempt is made at matching a user's list of preferred languages (from their Accept-Language HTTP header). E.g. in the context of the following request:

User Languages: fr-CH, fr-CA  
Application Languages: fr-CA, fr, en

fr-CA will be matched first, and if no resource exists for that language, fr is tried, and failing that, the default language en is fallen back on.

In recognition of the potential bottleneck of the GetText call (which typically is called many times per-request), the matching algorithm is efficient for managed code (lock-free and essentially heap-allocation free).

Note that the following Chinese languages tags are normalized: zh-CN to zh-Hans, and zh-TW to zh-Hant. It is still safe to use zh-CN and zh-TW, but internally they will be treated as equivalent to their new forms.

Private Use Subtag

The w3c language tag spec includes a provision for an additional subtag for private use. This is now supported and can be used to provide a different translation for specific scenarios, such as a tenant on a multi-tenant application.

The format is: en-GB-x-Tenant123, en-x-Tenant99 etc.

Note the -x-, after which you can add four or more alphanumeric characters to specify your custom translation. There must be an exact match for all subtags for this translation to be returned. If the module can't find a translation for the tenant, it will match the remaining subtags according to the algorithm described above.

Microsoft Pseudo-Locales and App Testing

As an aid to testing the localization of you app, Microsoft have added some 'pseudo-locales' to Windows.

Specifically, these are identified by the following special language tags qps-ploc, qps-plocm and qps-ploa.

i18n supports the use of these special locales. See Issue #195 for further details.

Language Matching Update

The latest refinement to the language matching algoritm:

// Principle Application Language (PAL) Prioritization:
//   User has selected an explicit language in the webapp e.g. fr-CH (i.e. PAL is set to fr-CH).
//   Their browser is set to languages en-US, zh-Hans.
//   Therefore, UserLanguages[] equals fr-CH, en-US, zh-Hans.
//   We don't have a particular message in fr-CH, but have it in fr and fr-CA.
//   We also have message in en-US and zh-Hans.
//   We presume the message from fr or fr-CA is better match than en-US or zh-Hans.
//   However, without PAL prioritization, en-US is returned and failing that, zh-Hans.
//   Therefore, for the 1st entry in UserLanguages (i.e. explicit user selection in app)
//   we try all match grades first. Only if there is no match whatsoever for the PAL
//   do we move no to the other (browser) languages, where return to prioritizing match grade
//   i.e. loop through all the languages first at the strictest match grade before loosening 
//   to the next match grade, and so on.
// Refinement to PAL Prioritization:
//   UserLanguages (UL) = de-ch,de-at (PAL = de-ch)
//   AppLanguages  (AL) = de,de-at,en
//   There is no exact match for PAL in AppLanguages.
//   However:
//    1. the second UL (de-at) has an exact match with an AL
//    2. the parent of the PAL (de) has an exact match with an AL.
//   Normally, PAL Prioritization means that 2. takes preference.
//   However, that means choosing de over de-at, when the user
//   has said they understand de-at (it being preferable to be
//   more specific, esp. in the case of different scripts under 
//   the same language).
//   Therefore, as a refinement to PAL Prioritization, before selecting
//   'de' we run the full algorithm again (without PAL Prioritization) 
//   but only considering langtags related to the PAL.

UpdatePanel / Async Postbacks / Partial Page Rendering

Responses to UpdatePanel async postback requests are handled as a special case because the content of the response is a set of formatted blocks, which may or may not contain partial segments of text or HTML that need to be localized. Each formatted block has the following structure

length|type|id|content|

By default, only blocks with a type of updatePanel, scriptStartupBlock, or pageTitle get localized. You can localize segments in other block types by changing the value of AsyncPostbackTypesToTranslate in Application_Start. For example, to include the hiddenField blocks, add the following to Application_Start

i18n.LocalizedApplication.Current.AsyncPostbackTypesToTranslate = "updatePanel,scriptStartupBlock,pageTitle,hiddenField";

OWIN support

Support for OWIN is available to a limited extent. See issues #241 and #333 for more details. i18n is created based on HttpContextBase in System.Web assembly, which means the foundation was built on IIS pipeline. Currently we support OWIN hosted in IIS only, so it is still dependent on System.Web. Self-hosted OWIN is not supported.

Here is how to use i18n in OWIN Web API projects:

  • Add reference to i18n.Adapter.OwinSystemWeb (available on NuGet as well)
  • Add reference to Microsoft.Owin.Host.SystemWeb. If you add i18n.Adapter.OwinSystemWeb from NuGet it should automatically add this for you.
  • No need to register HttpModule in web.config file.
  • Add the following middleware registration into your startup sequence:
public partial class Startup
{
    public void Configuration(IAppBuilder app)
    {
        ...

        // i18n config
        i18n.LocalizedApplication.Current.DefaultLanguage = "en";

        // i18n middleware
        app.Use(typeof(i18n.Adapter.OwinSystemWeb.UrlLocalizationMiddleware));

        // i18n response filter installer for static files
        var staticFileOptions = new StaticFileOptions
        {
            OnPrepareResponse = (staticFileResponseContext) =>
            {
                if (staticFileResponseContext.File.Name.EndsWith(".js", StringComparison.OrdinalIgnoreCase))
                {
                    HttpContextBase context = staticFileResponseContext.OwinContext.Get<HttpContextBase>(typeof(HttpContextBase).FullName);
                    LocalizedApplication.InstallResponseFilter(context);
                }
            }
        };
        app.UseStaticFiles(staticFileOptions);

        ...
    }
}
  • Add the following handler to Global.asax:
    /// <summary>
    /// Handles the ReleaseRequestState event of the Application control.
    /// </summary>
    /// <param name="sender">The source of the event.</param>
    /// <param name="e">The <see cref="EventArgs"/> instance containing the event data.</param>
    protected void Application_ReleaseRequestState(object sender, EventArgs e)
    {
        HttpContextBase context = this.Request.GetOwinContext().Get<HttpContextBase>(typeof(HttpContextBase).FullName);
        i18n.LocalizedApplication.InstallResponseFilter(context);
    }

A reminder about folders in a web application

Your locale folder is exposed to HTTP requests as-is, just like a typical log directory, so remember to block all requests to this folder by adding a Web.config file.

    <?xml version="1.0"?>
    <configuration>    
        <system.web>
            <httpHandlers>
                <add path="*" verb="*" type="System.Web.HttpNotFoundHandler"/>
            </httpHandlers>
        </system.web>
        <system.webServer>
            <handlers>
                <remove name="BlockViewHandler"/>
                <add name="BlockViewHandler" path="*" verb="*" preCondition="integratedMode" type="System.Web.HttpNotFoundHandler"/>
            </handlers>
        </system.webServer>
    </configuration>

Unit Testing With i18n

i18n provides the i18n.ITranslateSvc interface that abstracts the basic operation of parsing and translating a string entity that may contain one or more nuggets:

    public interface ITranslateSvc
    {
        string ParseAndTranslate(string entity);
    }

The following stock implementations of i18n.ITranslateSvc are provided by the library:

  • TranslateSvc_Invariant - ITranslateSvc implementation that simply passes through the entity (useful for testing).
  • TranslateSvc_HttpContextBase - ITranslateSvc implementation based on an given HttpContextBase instance.
  • TranslateSvc_HttpContext - ITranslateSvc implementation based on an given HttpContext instance.
  • TranslateSvc_HttpContextCurrent - ITranslateSvc implementation based on the static HttpContext.Current instance (obtained at the time of calling the interface).

Contributing

There's lot of room for further enhancements and features to this library, and you are encouraged to fork it and contribute back anything new. Specifically, these would be great places to add more functionality:

  • Full OWIN support (see Issue #241)
  • Input and ideas on a safe universal nugget syntax (see issue #69).
  • Plurals support.
  • Help me fix the bugs! Chances are I don't ship in your language. Fix what hurts. Please?

Coding Style Guidlines

  • Pull Requests that add functionality to be accompanied with documentation added to this README.
  • Pull Request to be as granular as possible (e.g. limited to single features/enhancements).
  • All methods to be commented including helper routine.
  • 4-spaces used for tab indent.

Line Endings

The i18n project has adopted the GitHub recommendation with regard to standardized line endings in text files. Specifically, text files are stored in the Git index with line endings respresented by the single LF character (not CR/LF).

That means that, for Windows clients, you will probably want Git to convert line endings to CR/LF when checking text files out of the index, and converting them back to LF line endings when committing in. This behaviour is controlled via Git's core.autocrlf setting, which in this case would be set to true.

See Dealing with line endings for more information.

Build Notes

The i18n project at present targets .NET Framework 4 and later. To build i18n from source, Visual Studio 2019 or later is recommended.

Known Issues

  • MVC controller names must be more than 3 chars (#370).

Release History

2.1.17 (20201209)

  • FIX: Only every other nugget in messages.po is being translated (#413). Fix for a regression introduced in v2.1.16.

2.1.16 (20201126)

  • ADDED: Enhanced support for translations invoked by background jobs (LanguageHelpers.ParseAndTranslate + HttpContextExtensions.GetRequestUserLanguagesAsString + IBackgroundTranslateSvc).
  • ADDED: i18n project automated build and test (Continuous Integration) with github actions.
  • FIX: i18n.LanguageTag.ExtractLangTagFromUrl returns/outputs incorrect values when passed an absolute url.
  • FIX: Problem with HttpContext.ParseAndTranslate helper method (#338).
  • FIX: ArgumentNullException if the Content-Type header is not set in the response (#337).
  • FIX: Missing PO comment lines breaks translation (#351).

2.1.15 (20190814)

  • FIX: NullReferenceException caused by bad langtag (#387).
  • FIX: LangTag extraction logic broken by URL with query string immediately after lantag (#383).

2.1.14 (20180710)

  • FIX: Localization of outgoing URIs" feature issue in version 2.1.13 (#374).

2.1.13 (20180707)

  • FIX: performance issues related to translations in the default application language (#368).
  • FIX: URI fragments breaking localization of outgoing URIs (#372).

2.1.11 (20180528)

  • Improved support for wildcards in BlackList and WhileList settings (#319).
  • FIX: redundant updates to PO files (#329).
  • Modifications to OWIN support (#334) [BREAKING CHANGE].

2.1.10 (20161206)

  • New setting i18n.DisableReferences allows for the generation of lighter POT/PO files by excluding nugget references (#304).
  • New setting i18n.GenerateTemplatePerFile enables the breakdown of the POT template file into one POT file per scanned file (#314).
  • FIX: PostBuild bug introduced by release 2.1.9 (#316).
  • FIX: Duplicate message properties in POT/PO files.
  • Introduced support for publishing regular pre-release packages to NuGet.

2.1.9 (20161125)

  • Support for customization of PO filenames and sources (#305).
  • Support for modifying nugget translation at runtime (#300).
  • Support for changing default i18n cookie name (#296).
  • Added /api/ to default UrlLocalizer.QuickUrlExclusionFilter (#289).
  • Support for converting outgoing URLs where un-rooted paths into rooted paths (common in ASP.NET WebForms) (#286).

2.1.8 (20160807)

  • Support for ignoring Accept-Language request header (#278, #285).
  • Support for optionally showing source context next to reference paths & line numbers (#268)

Acknowledgments

Among the many contributors to the i18n library, a special acknowledgement is due to Daniel Crenna who originated this project.

i18n's People

Contributors

ajbeaven avatar allegromanontroppo avatar asapostolov avatar berniezhao avatar braunoeder avatar couraud avatar danielcrenna avatar jandev avatar jeancroy avatar joero4 avatar jonnybee avatar kesyn-duplicate avatar macote avatar mickelarsson avatar mmunchandersen avatar peters avatar raulvejar avatar rickardliljeberg avatar ryan1234 avatar sisve avatar stustephenson avatar tomoness avatar turquoiseowl avatar vhatuncev avatar whut avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

i18n's Issues

Default nugget has potential clashes with json

The default nuggets "[[[" and "]]]" are a somewhat common combination when building a json structure that has arrays within arrays.
Since most people will not change the default, we should try to make sure we pick one that is failry unique among the domains we are targeting (C#, Razor, HTML, Json, Css, XML and all other web-related techs for .net based websites)

I've opened this issue so we can track suggestions as well

I've been experimenting with '[||[' and ']||]', and they seem to work pretty well as '||' is generally used as an or operator that is binary which will make it fail because of the brackets so it is very unlikely we'll run into a case where this has a valid meaning in one of the targeted domains.

some feedback

Hi Daniel,
here is some initial feedback I collected:

  1. LocalizingService
    // Save cycles processing beyond the default; just return the original key
    if (culture.TwoLetterISOLanguageName.Equals(I18N.DefaultTwoLetterISOLanguageName, StringComparison.OrdinalIgnoreCase))
    {
    return key;
    }
    imho that is not totally correct. In many cases the key used could be something like "Home.WelcomeParagraph", and not just "Hi, and welcome to my brand new home page", so it will fail for the default language. I modified it to return the regional content found.

  2. gettext is a bit out of date, but I haven't been able to find an updated win32 version. In some cases it fails to export strings, eg: <img src="@href(_("myimg"))"/>

  3. some parsing issues: this is probably related to POEdit, but I'm having some issues when translations become obsolete or are not confirmed (they throws null reference exceptions).

That's all for now :-)

Ciao,
Petrhaus.

nuget package not installing

Hi,
nuget package does not install correctly, this is the error message I get:
Install failed. Rolling back...
Install-Package : Failed to add reference to 'envsubst'.
At line:1 char:16

  • Install-Package <<<< i18n
    • CategoryInfo : NotSpecified: (:) [Install-Package], InvalidOperationException
    • FullyQualifiedErrorId : NuGetCmdletUnhandledException,NuGet.PowerShell.Commands.InstallPackageCommand

Any idea why this happens?

msgctxt support?

In the app I'm working on I need to retain context for each translatable message, for example although the text "hammer" may appear multiple times, in some languages it may have to be translated into different words depending on the context of the page (this is a contrived example, but I hope this is clear).

Gettext has the idea of the msgctxt which will allow the disambiguation of the otherwise identical words, and editors like poedit can handle this. I don't see any way to handle this inside i18n, is this proposed for V2?

Unhandled exception when handling requests without 'Accept-Language' header even when the application has a default language defined

Using the following configuration:

        i18n.LocalizedApplication.Current.ContentTypesToLocalize = new Regex("^(?:text/html)$");
        i18n.LocalizedApplication.Current.DefaultLanguage = "en";
        i18n.LocalizedApplication.Current.PermanentRedirects = true;
        i18n.LocalizedApplication.Current.EarlyUrlLocalizerService = null;
        i18n.UrlLocalizer.UrlLocalizationScheme = i18n.UrlLocalizationScheme.Scheme2;
        i18n.UrlLocalizer.IncomingUrlFilters += delegate(Uri url)
            {
                if (url.LocalPath.EndsWith(".xml", StringComparison.InvariantCultureIgnoreCase))
                    return false;

                if(url.LocalPath.EndsWith(".css", StringComparison.InvariantCultureIgnoreCase))
                    return false;

                if (url.LocalPath.EndsWith(".js", StringComparison.InvariantCultureIgnoreCase))
                    return false;

                return true;
            };

When attempting to handle a request that has no accept-language header, the language item will through an unhandled exception. This causes a problem since web crawlers and some smartphone browsers don't have this tag.

Expected behavior: It should default to the application's default language and respond to these requests appropiately.

Root of website when using virtual folder cannot be found

When using a virtual folder configuration and the default early url localizer, for example 'localhost/MyApp/' there is a problem when trying to access the root. The error seems to indicate it's trying to find 'MyApp' as a controller when it should acknowledge that from the routing table that parameter is implicit for the default route.

Hi, when I try to run app in localhost, I got an error: System.ArgumentNullException Value cannot be null

When I debug code, I see error ocurred on this block:

                url = url.Substring(0, url.Length -suffix.Length);
                var originalRequest = new ClonedHttpRequest(context.Request, url);
                var originalContext = new ClonedHttpContext(context, originalRequest);

                result = _route.GetRouteData(originalContext);
                if (result != null)
                {
                    // Found the original non-decorated route
                    return result;
                }

result = route.GetRouteData(originalContext) always return null; seems like originalcontext never get route values (and language route value).

My site is running ASP.NET MVC 4, with Entity Framework using IIS Express with Visual Studio 2012

This is error details:

Source code error:

Línea 30: public override string AppRelativeCurrentExecutionFilePath
Línea 31: {
Línea 32: get { return VirtualPathUtility.ToAppRelative(_url); }
Línea 33: }
Línea 34: public override string CurrentExecutionFilePath

Stack Trace:

[ArgumentNullException: Value cannot be null.
Parameter name: virtualPath]
System.Web.VirtualPath.Create(String virtualPath, VirtualPathOptions options) +9800033
System.Web.VirtualPathUtility.ToAppRelative(String virtualPath) +13
i18n.ClonedHttpRequest.get_AppRelativeCurrentExecutionFilePath() in c:\Users\Kellerman\Documents\GitHub\i18n\src\i18n\ClonedHttpRequest.cs:32
System.Web.Routing.Route.GetRouteData(HttpContextBase httpContext) +49
i18n.LanguageRouteDecorator.GetRouteData(HttpContextBase context) in c:\Users\Kellerman\Documents\GitHub\i18n\src\i18n.MVC3\LanguageRouteDecorator.cs:42
System.Web.Routing.RouteCollection.GetRouteData(HttpContextBase httpContext) +233
System.Web.Routing.UrlRoutingModule.PostResolveRequestCache(HttpContextBase context) +60
System.Web.Routing.UrlRoutingModule.OnApplicationPostResolveRequestCache(Object sender, EventArgs e) +82
System.Web.SyncEventExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() +136
System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously) +69

Unable to install package

Hi I was just cleaning up my nuget references and went to reinstall i18n and i got:

PM> Install-Package i18n
Attempting to resolve dependency 'CommonServiceLocator (≥ 1.0)'.
Successfully installed 'CommonServiceLocator 1.0'.
Successfully installed 'I18N 1.0.5'.
Successfully added 'CommonServiceLocator 1.0' to SelogerCity.Web.
Successfully uninstalled 'I18N 1.0.5'.
Successfully uninstalled 'CommonServiceLocator 1.0'.
Install failed. Rolling back...
Install-Package : Failed to add reference to 'envsubst'.
At line:1 char:16

  • Install-Package <<<< i18n
    • CategoryInfo : NotSpecified: (:) [Install-Package], InvalidOperationException
    • FullyQualifiedErrorId : NuGetCmdletUnhandledException,NuGet.PowerShell.Commands.InstallPackageCommand

I'm running NuGet 1.62.21215.9133

Route Localization. Is this possible?

Hello,

Is it possible to have localized routes? For example:

/en-US/post/show/1/hello-world
/en-GB/post/show/1/hello-world
/pt-PT/artigo/ver/1/ola-mundo

The culture code is optional but useful to distinguish between en-US and en-GB when necessary.

Thank You,
Miguel

ILocalizing._ = VB. Net Line Continuation character

Not sure if anyone has mentioned this yet, but '_' is the line continuation character in VB.Net. I've been using my own compiled version of the source and changing the function name to '__', but I would say this is definitely an issue.

What about truly localized Routes?

I've started implementing some solution myself, but than i found yours. Now i'm thinking is it possible to make it happen using your framework.

Lets say that i want to have two totally different URLs for different languages, for instance:

for 'en': /en/lessons/
for 'he': /he/שיעורים/

Both of the URLs must be routed to the same controller and action.
In plain MVC Routing it'll look something like this:

routes.MapRoute("en_Lessons_List", "en/Lessons/", new { controller = "Lessons", action = "List", culture = "en" });
routes.MapRoute("he_Lessons_List", "he/שיעורים/", new { controller = "Lessons", action = "List", culture = "he" });

Is this approach possible under your framework?

Does the default language need a PO file?

Does the default language need a sub-folder and a PO-file within the locale directory?

I have the following setup:

  1. Default language is set to "de-de" in Global.asax.cs method Application_Start
    i18n.LocalizedApplication.Current.DefaultLanguage = "de-de";
  2. HttpContextExtensions.SetPrincipalAppLanguageForRequest is set to whatever the user has selected within the application ("de-de" by default) in the Begin_Request method of Global.asax.cs.
  3. The POT file (template for all other translations) and all keys on the site are in German. There is no sub-folder and PO-file for de-de.
  4. The user visitng the site has UserLanguages in browser set to (in this order): de-de; de; en-us; en

The following happens (I tracked this down via Visual Studio debugger):

GetText() loads the user languages correctly. But since the language de-de is not listed in the AcceptedLanguages collection (but en-us is) all texts are translated to English.

If I create a subfolder and a PO file for de-de, the problem is fixed, since now de-de is listed as accepted language.

Why is the library using an accepted language instead of the default language when the PAL (principal application language) is set to the default language?

Language matching algorithm

At present, it is possible for region-specific POs to be skipped by the language matching algorithm in LocalizingService.GetText.

For example, suppose the following:

Accept-Language: fr-CH, fr-CA
PO files: fr-CA, fr

What the client would expect here is that the fr-CA PO file would be used, and failing that the fr one.

However, what happens at the moment is fr-CH is matched first, and when that fails we immediately fall back to fr. Thus, fr-CA is never tried for which is a shame because it would have matched.

This can be solved by a two-phase language matching loop:

first iteration we only try for exact matches;
second iteration we allow for fall back from region-specific languages to region-neutral languages.

Code for this proposal to come shortly. Comment welcome on any implications of this, I can't think of any at present.

i18n.POTGenerator - not compatible with VS2012

I try to open v2.0 solution in VS2012 and I get this error:

Unsupported
This version of Visual Studio does not have the following project types installed or does not support them. You can still open these projects in the version of Visual Studio in which they were originally created.
- i18n.POTGenerator, "(mu user path)\Desktop\i18n-2.0\src\i18n.POTGenerator\i18n.POTGenerator.csproj"

No changes required
These projects can be opened in this version of Visual Studio without changing them. They will continue to open in Visual Studio 2010 SP1 and in this version of Visual Studio.
- i18n, "(my user path)\Desktop\i18n-2.0\src\i18n\i18n.csproj"
- Solution Items, "Solution Items"
- i18n.PostBuild, "(my user path)\Desktop\i18n-2.0\src\i18n.PostBuild\i18n.PostBuild.csproj"
- i18n.Tests, "(my user path)\Desktop\i18n-2.0\src\i18n.Tests\i18n.Tests.csproj"
- i18n.Domain, "(my user path)\Desktop\i18n-2.0\src\i18n.Domain\i18n.Domain.csproj"
- i18n.Domain.Tests, "(my user path)\Desktop\i18n-2.0\src\i18n.Domain.Tests\i18n.Domain.Tests.csproj"
- i18n, "(my user path)\Desktop\i18n-2.0\src\i18n.sln"

Programatically Change Language

In answer to @rickardliljeberg's question, this is how you can set the user's preferred language in your controller (using v2.0 branch of i18n):

//
// GET: /Account/SetLanguage

[AllowAnonymous]
public ActionResult SetLanguage(string langtag, string returnUrl)
{
    DebugHelpers.WriteLine("AccountController::SetLanguage -- GET -- langtag: {0}, returnUrl:{1}", 
        langtag,
        returnUrl);
    // If valid 'langtag' passed.
    i18n.LanguageTag lt = i18n.LanguageTag.GetCachedInstance(langtag);
    if (lt.IsValid()) {
        // Set persistent cookie in the client to remember the language choice.
        Response.Cookies.Add(new HttpCookie("i18n.langtag")
        {
            Value = lt.ToString(),
            HttpOnly = true,
            Expires = DateTime.UtcNow.AddYears(1)
        });
    }
    // Owise...delete any 'language' cookie in the client.
    else {
        Response.Cookies["i18n.langtag"].FlagForRemoval(); }
    // Patch in the new langtag into any return URL.
    if (returnUrl.IsSet()) {
        returnUrl = i18n.LanguageTag.SetLangTagInUrlPath(returnUrl, UriKind.RelativeOrAbsolute, lt == null ? null : lt.ToString()).ToString(); }
    // Redirect user agent as approp.
    return this.Redirect(returnUrl);
}

Automatic routing

When I add the call to I18N.Register() in the global.asax Application_Start() method after routes have been registered I get a resource not found exception. Looks like MVC is trying to redirect to the virtual directory /en-us

Is there any other requirement needed that is not specified in the readme file of this project to enable automatic localized routing?

Thanks a lot

Strings used for annotations are not included in POT file

Maybe I'm misunderstanding something but I would expect the strings put inside the Display attribute (for example) to be included in the POT file when it is built.
I understand the attribute will make the string pass through the localization filter but it is forcing people now to manually add these entries into the POT file and hence making it a lot harder to use for people.

Redirect makes browser forget values

I am doing a very simple jquery ajax call like this:

ajaxSearchCall = $.ajax({
            type: "POST",
            url: "@Url.Action("Search")",
            data: "searchString=" + $("#searchString").val(),
            dataType: "html"
        });

So I use Url.Action to get my path and it gives me url: "/AdminRoot/Search",
Which is correct but does not contain language string in path.

What then happens can be seen here in this screenshot from chrome
http://db.tt/seJksqpy

It sends the post (with the post data). What it gets back is a 302 (Moved temporarily) to the same path but with /en/ in front of it. So with language code.

But then as you can see chrome calls this path with GET not post any more, and it does not send in the post data again either.

I have not dugg into the code to see exactly where this happens, but it is i18n project because disabling i18n i web.config fixes the issue.
I think to fix this we need one of many solutions.

  1. If it's a post and data is coming in with it, then do not send a 302 but rather process request. Only send 302 on normal get (probably recomended)
  2. Make Url.Action include the language tag in the first place.

I am sure there are other solution as well but number one seems the easiest to me. We can still populate all the language data and run full i18n for the request. only that we don't redirect on post.

gettext only support ASCII

Sometimes we need a tool generate or migrate strings(unicode(e.g. Chinese words, Japanese words)) to pot file.

I18NSession redesign

I propose that the I18NSession class is in fact redundant.It has some virtual methods suggesting it was intended at some point to be extendable, yet instantiation of it is hard coded.

Any functionality can be better refactored to be extension methods of HttpContext/Base.

For extensibility we then focus on the LocalizingService interface and class.

Raising this as an issue in case I'm missing something?

Sticking to selected language vs mixing like now.

Once a language is selected (either by user interaction or automatic selection process) stick to it and rather display msgid than other languages.

cases for when current behavior is nice
a1. User like me speaks both swedish and english (selects english but can still see missing translations in swedish)
a2. User knows what he is doing and has set his browser language tags perfectly

cases where the other version is better
b1. Users who do not know or have energy to set browser languages
b2. Users who for any reason is on computer that returns language he does not speak (borrowed or his own)
b3. It can look bad with mixed languages on site (even if user speaks all)

Factor out route localization stuff from i18n.I18N class

At the moment we have the following call which enables localization of routes:

i18n.I18N.Register();

I propose we move the route localization stuff out of the I18N class into one called RouteLocalization, and replace the above call with:

i18n.RouteLocalization.Enable();

The RouteLocalization class will then become the focus for configuration and extensibility of this feature e.g. a IRouteLocalizer service and handling scheme (issue #31).

Stream processing

I've been trying to work out how to localize DataAnnotations, jQuery.validation stuff etc. and had an idea:

Is there scope in i18n for general processing of the HTTP response output and making translation there. E.g. we could scan for msgids wrapped in some markers like ###Translate me!###, lookup any corresponding message and, if found, swap it in.

Maybe this has already been thought of, or is not practical?

About to go away and investigate ASP.NET HttpModules/Handlers... I guess it would also require xgettext or equivalent to be able to locate these marked strings in the project source.

Confusion between interfaces name and concrete class names.

Quite a small point this but perhaps a good time to mention it: some classes such as I18n, I18NWebViewPage etc. look to be interfaces rather than classes. It took me some time to get my head round this naming.

Any objections to renaming these? e.g. i18NWebViewPage

Detect unlocalized strings in the application scope

Put this down as nice to have.
It would be great if the same tool that builds the POT file will also alert to static strings that are not localized.
This will be a common concern in larger products with several people will be involved in the development where it will be hard to enforce puting strings under localization

No way to internationalize webapi actions

Webapi methods should also be able to return results in the requested language, including error descriptions
Right now, nugget tokens are ignored and returned as is, so for example you will see the action returning a "[[[Success]]]" tag if your method was supposed to return an internationalized version of the string.
Before it was possible to implement the localized interfase in the controller and use that to internationalize the strings.

What is the ILocalizing interface for?

I'm updatnig the readme and it mentions ILocalizing a lot but can't work out what this is for. It is implemented liberally but not actually queried-for or called-through anywhere.

Furthermore, it only has the one method:

_();

and doesn't include the newer and overloaded one:

__();

Do we need it?

Parsing files in referenced projects when building POT file, allowing multiple po files for application

A very typical scenario involves putting the model or some other shared classes (like a base controller) in a shared library that then multiple applications can use.
In that scenario there are 2 things that would be desirable:

  1. Parse any projects that are depended upon to build their pot files as well
  2. Be able to include more than 1 po file so that the po files for shared projects can be reused as well. An alternative would be to parse depended projects into the single application POT file, but then you'll have replicated strings across the projects that use the shared library

i18n module will not work on Cassini

Works fine on IIS and IIS Express but for some reason in Cassini the translations are not happening which makes me think the module is not being loaded. I'll try to run more experiments later and see exactly what is happening but an initial internet search didn't turn up any reasons why Cassini might choose to ignore a module defined in the web.config

msgmerge parameters on PostBuild.exe breaks PostBuild.exe

Sorry, but I can't make it work... whenever I add a "msgmerge:-N" parameter to PostBuild.exe, even using directly from command line, it allways gives me the same error, which I paste:

This is the command I run:

C:\Users\javizcaino>"C:\Documents\Projects\MVC4Bootstrap\MVC4Bootstrap\bin\i18n.PostBuild.exe" "C:\Documents\Projects\MVC4Bootstrap\MVC4Bootstrap\" "msgmerge:-N"

And this is the exception thrown:

Unhandled Exception: System.ArgumentException: Illegal characters in path.
   at System.IO.Path.CheckInvalidPathChars(String path, Boolean checkAdditional)
   at System.IO.Path.NormalizePath(String path, Boolean fullCheck, Int32 maxPathLength)
   at System.IO.Path.GetFullPathInternal(String path)
   at System.IO.FileSystemEnumerableIterator`1..ctor(String path, String originalUserPath, String searchPattern, SearchOption searchOption, SearchResultHandler`1 resultHandler, Boolean checkHost)
   at System.IO.Directory.GetFiles(String path, String searchPattern, SearchOption searchOption)
   at i18n.PostBuildTask.BuildProjectFileManifest(String path) in c:\Users\Daniel\Desktop\Src\i18n\src\i18n\PostBuildTask.cs:line 84
   at i18n.PostBuildTask.Execute(String path, String gettext, String msgmerge) in c:\Users\Daniel\Desktop\Src\i18n\src\i18n\PostBuildTask.cs:line 22
   at i18n.PostBuild.Program.Main(String[] args) in c:\Users\Daniel\Desktop\Src\i18n\src\i18n.PostBuild\Program.cs:line 30

This happens with every project I have tried.
If I remove the "msgmerge-N" it all runs flawless

What I'm doing wrong?

string with parameters

I'm thinking of extending __() method adding the following method
public string __(string text, params object[] parameters)
{
return String.Format(_session.GetText(Context, text), parameters);
}

This method will help me to translate strings like __("Hello {0}!", "juan") or __("you've earned {0} point of {1}", 1, 10)

In spanish I want to get: "{0} bienvenido nuevamente" or "ganaste 1 punto de un total de 10"

What do you think about this?

Future Direction for i18n of Web Applications

No doubt we have all come to this i18n project looking for a better way to internationalize our web applications. We see that doing the old .NET resource look-up is backward. I expect we also see that leveraging the PO infrastructure for getting messages translated is the way forward.

Unfortunately, the PO infrastructure (i.e. the GNU Portable Object file format specification and the world of tools for translating the files) is very much tethered to GetText, the latter being very backward IMO.

Whoever invented GetText had a brain-wave: we can encode strings in our source code in such a way that A) they can be hooked at run-time, and B) we can find and extract those strings from the source code. A very nice duality! So he or she wrote a library of functions that can look-up and swap message strings, and a tool for scanning source code files for those function calls and extracting message strings to be translated. It therefore assumes that all your message strings are contained in source code files which it can parse, and that they can be encoded as an argument to a function call e.g. _("Translate me!");

For someone facing the problem of how to internationalize a GUI app written in C, GetText is a good approach. For a back-end server program (like a web application), I suggest it is also reasonable, but not the best. With a back-end application, we have access to the output stream, and with a web application it is very easy to get at the HTTP response and do our translations there.

Now, as soon as we drop one side of the duality, one might start to wonder about the other side.

The question is, why bother with all those _() functions when we only need them to mark the message strings (given that we can hook into the HTTP response body). The reason, of course, is that we still need to mark the message strings so they can be extracted into the PO file. Okay, but if we were going to choose a method for marking message strings for extraction, unhindered by any considerations other than it needs to be reliable, would we choose prefixing the string with _(" and suffixing with ")?

There must be a better way to mark message strings, so that they can be easily picked up in source code and the HTTP response. The same algorithm can be used for both. Better still would be compatibility with SQL LIKE so that they can be extracted from database tables too e.g. product descriptions.

The marking can be done in the string itself, so message strings can be written straight into source files without the need to call any helper functions. Very useful for const strings such as C# attributes and data annotations. They would be entirely language independent: C#, Razor, JavaScript, HTML. They can also be written straight into database fields. No need to think "how do I access that helper function?"

Performing the translations at the HTTP response layer has the advantage of confining message look-up and patching to a single place, hence efficiency gains. It reduces dependency on any particular web development platform; we can forget about MVC and drop down the stack to ASP.NET (or even lower).

So where are we with this? With Issue #37 I have taken a stab at defining a suitable message marking syntax, called the Nugget syntax. There will be scope for improvement on the syntax I have no doubt (and a better name). It would be great to have a discussion with you guys on this. I'm sure we can come up with a syntax that is easy to remember and use, and yet robust. Support for string formatting is essential (i.e. {0} substitution), and pluralization would be nice.

With the marking syntax defined, the only outstanding work is to swap out (or augment) the GetText-dependent post-build task with new logic for extracting the marked message strings and adding them to the PO output. My preference here would be to drop GetText altogether (along with the _() calls), but that would mean dropping backward compatibility for projects.

The v2.0 branch includes all the other support necessary for post-processing the HTTP response. At the moment it has support for processing the Nugget marking syntax, and changing that to support any new syntax would be trivial.

We then get to keep the best bits of the GNU translation project:

  • PO message file format
  • PO editor tools including collaborative ones

It has been a few months now that I have been developing a web app using i18n v2.0 branch, where there is the option to encode a message string as either _("Translate Me") or "[[[Translate Me]]]". Given the latter takes no extra thought other than including the [[[ and ]]] it wins every time.

Martin Connell

Configurable PostBuild

Hey.

In our project we need a few translation "packages" so the structure in output folder would be like:

OutputFolder

  • TranslationPackage1
    • Language1
    • Language2
    • Language3
  • TranslationPackage2
    • Language1
    • Language2
    • Language3

etc.
So we need more configurable solution than the one in PostBuild in v2.0 is.
Check this out:
https://github.com/ciscoheat/i18n/blob/master/src/i18n.PostBuild/app.config
This PostBuild is more configurable than yours. You should really consider using this solution - it is easier to use app.config and pass parameters there than putting parameters in post build action (i.e. I couldn't pass output folder path that has spaces).

Version 2 milestone (new parser)

So me and Martin has been chatting and agreed that the way forward here as we see it is by manipulating the output (as previously discussed)

The plan goes that it's time to remove gettext and msgmerge. instead parsing all files according to white list and the having a proper domain model, that allows saving translation data either to po file like today or database (repository pattern).

So here is the list that we plotted down on things to do. Feel free to come with ideas about this.
https://docs.google.com/document/d/15A-bhM5-24Y6K0lMfl2dDSr1jscFIRadyVjnAId3Da0/edit?usp=sharing

URL Handling Scheme

I can see 3 approaches for a site to URL handling and redirection so far (assuming the example.com/en pattern). Let us assume default app language is en. Then we could want:

  1. Everything to be explicit, so any URLs/routes not containing a language tag are patched and redirected, whether or not the language is the app-default. e.g. if selected language for the request is en then
    example.com -> example.com/en
    example.com/en -> example.com/en
  2. Everything to be explicit except the default language which MAY be implicit. e.g. if selected language for the request is en then
    example.com -> example.com
    example.com/en -> example.com/en
    and if selected language for the request is fr then
    example.com -> example.com/fr
    example.com/fr -> example.com/fr
  3. Everything to be explicit except the default language which MUST be implicit. e.g. if selected language for the request is en then
    example.com -> example.com
    example.com/en -> example.com

Trust Level on Shared Hostings

Hi guys I migrated from v.1.0.8 to v2.0.. Everything Works fine on localhost server, but when I tried to publish App to Hostgator Windows Shared we get SecurityException because something in httpmodule use a unsupported trust level for shared hostings.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.