Best practice for simple i18n message substitution?

5,733 views
Skip to first unread message

jsawyer

unread,
Sep 9, 2011, 11:47:21 AM9/9/11
to AngularJS
There seem to be several ways to retrieving localized messages from a
locale-specified file. Is there a best practice for this?

I'm currently doing this:

<sometag ng:controller="Messages">
...
<p>{{msg('key1')}}</p>

The Messages controller is of course where the logic exists to load
the appropriate message bundle for the current locale. The msg()
function would also have provisions for parameter substitution for
messages that need it.

This works fine in the HTML. However, I anticipate that I occasionally
will have to look up a message in Javascript; for example, a service
may need to generate an error message. How would I do that?
Instantiate another instance of Messages? Or is there some way in
Javascript to invoke msg() in a way that the parent scopes will be
searched for the previously existing copy of Message?

Also, I'm thinking of using jquery.i18n.properties to find the
appropriate message bundle in my Messages class; or is there a built-
in AngularJS way of doing this?

js

Witold Szczerba

unread,
Sep 18, 2011, 7:28:04 AM9/18/11
to ang...@googlegroups.com
Hi,
what about such a solution: create an i18n service which will handle
translation logic and use it directly in controllers. For markup,
write i18n filter, so you could write templates like this:
<p>{{ MSG_KEY_1 | i18n }}</p>

Regards,
Witold Szczerba

> --
> You received this message because you are subscribed to the Google Groups "AngularJS" group.
> To post to this group, send email to ang...@googlegroups.com.
> To unsubscribe from this group, send email to angular+u...@googlegroups.com.
> For more options, visit this group at http://groups.google.com/group/angular?hl=en.
>
>

rur

unread,
Sep 18, 2011, 9:05:27 AM9/18/11
to AngularJS
Hey,

How do you make a filter work with a service, and by the sound of
things an async service at that? I've always thought of filters as
being relatively static.

rur




On Sep 18, 12:28 pm, Witold Szczerba <pljosh.m...@gmail.com> wrote:
> Hi,
> what about such a solution: create an i18n service which will handle
> translation logic and use it directly in controllers. For markup,
> write i18n filter, so you could write templates like this:
> <p>{{ MSG_KEY_1 | i18n }}</p>
>
> Regards,
> Witold Szczerba
>

Witold Szczerba

unread,
Sep 18, 2011, 10:10:02 AM9/18/11
to ang...@googlegroups.com
Hi,
The answer is: I don't. This was just a suggestion to use i18n filter
for markup and to delegate from filter to service, so controllers
could use i18n as well when needed. I cannot believe there could be
any problem accessing services from within filters.

Regards,
Witold Szczerba

Igor Minar

unread,
Sep 18, 2011, 10:53:28 AM9/18/11
to ang...@googlegroups.com
we don't have the dependency injection in filters yet, but you still can access a service in filter like this:

angular.filter('myFilter', function() {
  var myService = this.$service('myService');
});

you can also access filters from services if you really want to, but that is very unusual:

angular.service('foo', function() {
  var myFilter = angular.filter.myFilter;
  //myFilter is not bound here, so `this` is not the root scope, to fix this you'd have to call filter as:
  var myBoundFilter = angular.bind(this, angular.filter.myFilter);
});

/i

jsawyer

unread,
Sep 19, 2011, 12:37:04 AM9/19/11
to AngularJS
Hmmm. the idea of filters had occurred to me, but then how to do
easily do parametric substitution? Let's say I have an error message
like "The value must be between {0} and {1}."; I could bind to
"{{msg(MSG_ID, lowbound, hibound)}}" and it would do the substitution
for me. How would I do that with filters?

Another approach I was considering was adding the msg() function to
the global scope, so it would be available everywhere without having
to assign a controller at the top level. Any comments on whether
that's a good or bad idea?

js

On Sep 18, 8:53 am, Igor Minar <i...@angularjs.org> wrote:
> we don't have the dependency injection in filters yet, but you still can
> access a service in filter like this:
>
> angular.filter('myFilter', function() {
>   var myService = this.$service('myService');
>
> });
>
> you can also access filters from services if you really want to, but that is
> very unusual:
>
> angular.service('foo', function() {
>   var myFilter = angular.filter.myFilter;
>   //myFilter is not bound here, so `this` is not the root scope, to fix this
> you'd have to call filter as:
>   var myBoundFilter = angular.bind(this, angular.filter.myFilter);
>
> });
>
> /i
>
> On Sun, Sep 18, 2011 at 4:10 PM, Witold Szczerba <pljosh.m...@gmail.com>wrote:
>
>
>
>
>
>
>
> > Hi,
> > The answer is: I don't. This was just a suggestion to use i18n filter
> > for markup and to delegate from filter to service, so controllers
> > could use i18n as well when needed. I cannot believe there could be
> > any problem accessing services from within filters.
>
> > Regards,
> > Witold Szczerba
>

Elliott Sprehn

unread,
Sep 19, 2011, 3:54:12 AM9/19/11
to ang...@googlegroups.com
I think a widget is probably a better way to go about this.

<i:msg msgid="example">There are {{cats.length}} cats</i:msg>

and then have a table of locales and strings.

example: {
//...
es: 'hay $1 gatos',
//...
}

When angular compiles the contents of the i:msg it's going to transform {{cats.length}} into <span ng:bind="cats.length"></span>, so recursively compile the element, remove all the bare text nodes, and then swap in the components of the translations between the spans matching the positions of the $x place holders, possibly reordering the spans to match the order of the $x placeholders in the translation.

Aliases would also probably help your translators.

<i:msg msgid="example" desc="Number of cats in the page.">There are <i:p desc="count">{{cats.length}}</i:p> cats</i:msg>

//...
es: 'hay $count gatos',
//...

Using a widget also has the advantage that it becomes easy for a server side process, or even a build rule, to create translated templates.

I'd discourage passing only the message id and params and not having the original text in the templates. This makes working with the UI much harder and is a pain for designers. All modern translation libraries keep the original string in there.

Witold Szczerba

unread,
Sep 19, 2011, 3:57:18 AM9/19/11
to ang...@googlegroups.com
You can parametrize filters the same way as regular functions:
expression | filter:param1:param2:paramN...

I am wondering if all this is a good approach though. See what Igor
said yesterday about this in another thread: "Non trivial
localizations".

Regards,
Witold Szczerba

Jehu

unread,
Oct 2, 2011, 3:52:07 PM10/2/11
to AngularJS
Hi,

i've used a simple translation method by using a JavaScript sprintf
[1] in a huge ExtJS4 project with success.
One can use a simple __('My Name ist %s and i am %d years old.',
['Max', 98])

Now for a new angular project i've written a filter which does the
same.
In angular templates one can translate strings like this:
{{'Hello World'|i18n}}
or
{{"My name is %s and i am %d years old." | i18n:"Max":"98"}}


angular.filter('i18n', function(string) {
var log_untranslated = false;
var placeholders = [];

for(var i=1; i < arguments.length; i++) {
placeholders.push(arguments[i]);
}

var translate = function(string, placeholders) {
var placeholders = placeholders || null;
var translated = lang[string]; // lang ist from the language
file, e.g. de_DE.js
if (translated === undefined) {
/*if (log_untranslated == true) {
// here we could track unreanslated strings by sending
them to the server...
}*/
return sprintf(string, placeholders);
}
return sprintf(translated, placeholders);
};

var translated = translate(string, placeholders);
return translated;
});

The needed language object (can be a file named "de_DE.js") is
something like this:

lang = {
'My name is %s and i am %d yeas old.': 'Mein Name ist %s, und ich
bin %d Jahre alt.'
}



[1] http://www.diveintojavascript.com/projects/javascript-sprintf



On 19 Sep., 09:57, Witold Szczerba <pljosh.m...@gmail.com> wrote:
> You can parametrize filters the same way as regular functions:
> expression | filter:param1:param2:paramN...
>
> I am wondering if all this is a good approach though. See what Igor
> said yesterday about this in another thread: "Non trivial
> localizations".
>
> Regards,
> Witold Szczerba
>
> >> > could usei18nas well when needed. I cannot believe there could be
> >> > any problem accessing services from within filters.
>
> >> > Regards,
> >> > Witold Szczerba
>
> >> > On 18 September 2011 15:05, rur <fluid...@gmail.com> wrote:
> >> > > Hey,
>
> >> > > How do you make a filter work with a service, and by the sound of
> >> > > things an async service at that? I've always thought of filters as
> >> > > being relatively static.
>
> >> > > rur
>
> >> > > On Sep 18, 12:28 pm, Witold Szczerba <pljosh.m...@gmail.com> wrote:
> >> > >> Hi,
> >> > >> what about such a solution: create ani18nservice which will handle
> >> > >> translation logic and use it directly in controllers. For markup,
> >> > >> writei18nfilter, so you could write templates like this:

deezer

unread,
Mar 9, 2012, 7:48:35 AM3/9/12
to ang...@googlegroups.com
All: What is the performance of each of these options?  Are they immediately available when the page renders? I'm concerned that too much JS needing to be run to even get the labels into the page could cause a slow or awkward load, especially on an older/slower system/browser (or a mobile device)?

Any experience with implementing any of these solutions - how did they work out?

Vojta Jína

unread,
Mar 10, 2012, 9:31:44 PM3/10/12
to ang...@googlegroups.com
I would generate different templates per language (on server).

V.

Pascal Precht

unread,
Apr 8, 2013, 4:37:45 PM4/8/13
to ang...@googlegroups.com
I published a translation module a few days ago. This could solve your problem:

Stephan Malek

unread,
May 22, 2013, 4:01:28 PM5/22/13
to ang...@googlegroups.com, jonrs...@gmail.com
Hello what aboout the first approuch:

filters.filter('i18n', ['$rootScope', function($rootScope) {

        return function (input) {
            var i18n  = $rootScope.i18n || null,
                lang  = $rootScope.activeLang || 'de',
                debug = true;


               if(angular.isObject(i18n)) {
                   var translated = i18n[input];

                   if(angular.isDefined(translated)) {

                    // lang string must replace arguments
                    if(arguments.length > 1) {

                        for(var i=1; i < arguments.length; i++) {
                            if(angular.isDefined(arguments[i])) {
                                var regexp = new RegExp('\\{'+i+'\\}', 'gi');
                                translated = translated.replace(regexp, arguments[i]);
                            }
                        }
                    }

                    return translated;
                }
                else {
                    if(debug === true) {
                        console.log('No translation of: ' + input);
                    }
                    return input;
                }
            }

            return input;
        };
    }]);

var debug is not imported yet. But its simply and good. Care that no {0} works, because the first argument is the string itself.
var i18n is a simple json file with translation messages.
{
  "hello": "hallo",
  "hello {1}": "hallo {1}"
}

use : {{"hello {1}" | i18n:"AngularJS Coder"}}

greets
Reply all
Reply to author
Forward
0 new messages