2
votes

I am looking for a good way to share my translations between my Symfony2 App and my Angular.JS App, they are essentially the same app, however they act very differently.

First of all I have made a directive to allow for templating using [[ variable ]] in my angular js app, so I can use the variables from twig alongside my variables in angular.

So what I want to achieve is to translate my sentences into the localisation that the user prefers.

So my conclusion is that I want to use the translations features of Symfony, as there is no good support for that in Angular. I also want to use all the built in features that Symfony includes, which is like update the files automatically, support YAML etc. But I need to work out how to transfer them to my JS application and how I can detect them in Symfony, so both applications can use them.

So my initial idea:

Change the capturing of translations or at least add an additional one to the scope.

{% trans %}Hello [[name]]{% endtrans %}

<trans name="My Name" translation="Hello [[name]]" />

And also support all other functionalities with pluralisation etc.

This would then generate a file that has the translations, and pluralisation etc.

Request language through angular ajax call and save this into a local storage on the client side.

This would allow me to replace the directive above of trans with the proper value. This is not a problem to set up. However it needs to be exported from whatever format to JSON that angular can read.

Then there needs to be a matcher, and there needs to be support for pluralisation and all other features available.

Other Ideas

It might be better in general to not use the Symfony2 translations when you do an Angular.JS App, and thereby only use the angular translations, otherwise the text written in Symfony2 Twigs and is translated would probably not be translatable in Angular. But the generation of these files I find it to be better if Symfony could capture and spit out.

I think this needs to be a bit of work to be solid, but I feel that this needs to be solved. Any ideas and helpful comments are appreciated, I am considering to start a project for this on GitHub, to give proper support for this. But if there's such already it might just be better to work with that.

/Marcus

4
have you considered a custom filter in angular? Can use it a lot simpler than having to create directive <span>{{someVar | translate}}</span>.charlietfl
primitive demo of translate custom filter jsfiddle.net/4kuhF/1charlietfl
My problem is I want the translations to be able to update as well, so the solution below will render them once, then the page needs to be reloaded for them to change. The solution I provided works, however the one with filter below doesn't really do the job unfortunately.Oldek
don't understand what you are sayingcharlietfl
I happened to see this since I posted my filter ....might help you github.com/PascalPrecht/angular-translatecharlietfl

4 Answers

1
votes

Here's a suggestion using a custom angular filter to simplify markup

HTML:

<div ng-app="myApp" ng-controller="MainCtrl">
    {{ item |translate }}
</div>

JS

var words={
    'fr': {'Bus': "AutoBus"}  

};

var app = angular.module('myApp', []);
app.constant('lang','fr');

app.factory('wordService',function(lang){   
    return {
        getWord:function(val){
            return words[lang][val];
        }
    }
})
app.filter('translate', function(wordService){
    return function(val){
        return wordService.getWord(val)
    }
})

app.controller('MainCtrl', function($scope) {
  $scope.item = 'Bus';
});

You can ues a service to request the translation file(s) from either server or localStorage if they already exist. Just set language at run time.

You can reconfigure the words object any way that suits you to use it in both applications.

DEMO

1
votes

Further research

Here is my App:

'use strict';
var myApp = angular.module('myApp', []);

Here is my controller:

'use strict';
myApp.controller('PageController',
    function PageController($scope, translationService, $rootScope) {
        $rootScope.currentLanguage = 'en';
        $rootScope.translations = translationService.getTranslations($scope.currentLanguage);

        $scope.setLanguage = function(language) {
            if (language === $scope.currentLanguage) {
                 return;
            }
            $rootScope.currentLanguage = language;
            $rootScope.translations = translationService.getTranslations($scope.currentLanguage);
        }
    }
);

And here is the translationService:

'use strict';
myApp.service('translationService', function ($http, $q) {
    var translationsCache = {};
    return {
        getTranslations: function(language) {
            if (translationsCache[language]) {
                return translationsCache[language];
            }
            var deferred = $q.defer();
            // **** FAKE SOLUTION **** //
            // I just return a resolve here as it doesn't really matter for this test.
            if (language == 'sv') {
                deferred.resolve({
                    "My first text unit": "Detta är min första text unit",
                    "I am a Pirate": "Jag är en Pirat"
                });
            } else if (language == 'en') {
                deferred.resolve({
                    "My first text unit": "This is my first Text unit",
                    "I am a Pirate": "I'm a Pirate"
                });
            }

            translationsCache[language] = deferred.promise;
            return deferred.promise;
            // **** END FAKE SOLUTION **** //

            /* 
            // **** WORKING SOLUTION **** //
            The probable real solution to fetching language JSON generated by Symfony somewhere
            $http({method: 'GET', url: '/translations/'+language}).
                success(function (data, status, headers, config) {
                    deferred.resolve(data);
                }).
                error(function(data, status, headers, config) {
                    deferred.reject(status);
                });

            translationsCache[language] = deferred.promise;

            return deferred.promise;
            // **** END WORKING SOLUTION **** //
            */
        }
    }
});

So here is my directive that I came up with after some trial and error:

myApp.directive('translation', function($rootScope) {
    return {
        restrict: 'A', // Restrict to attribute
        replace: true, // Replace current object by default, not for input though, see solution below
        link: function(scope, element, attrs, controller){
            // This will watch for changes in currentLanguage in your $rootScope
            scope.$watch(function() {
                return $rootScope.currentLanguage; // If this changes then trigger function (binding)
            }, function(newVal, oldVal) {
                // As we have translation as a promise this is how we do
                $rootScope.translations.then(function(translations) {
                    // Does it exist, then translate it, otherwise use default as fallback
                    if (translations[scope.translation]) {
                        // Just some extra I found could be useful, set value if it is a button or submit. Could be extended.
                        if (element.prop('tagName') === 'INPUT' && (element.prop('type') === 'button' || element.prop('type') === 'submit')) {
                            return angular.element(element).val(translations[scope.translation]);
                        }
                        // Else just change the object to be the new translation.
                        return element.html(translations[scope.translation]);
                    }
                    // This is the fallback, and same as above, button and submit
                    if (element.prop('tagName') === 'INPUT' && (element.prop('type') === 'button' || element.prop('type') === 'submit')) {
                        return element.val(scope.translation);
                    }
                    return element.html(scope.translation);
                });
            });
        },
        scope: {
            translation: "@" // Save the parameter to the scope as a string
        }
    }
});

And here are some examples of how to use it.

HTML:

<div class="container">
    <div class="nav">
        <button ng-click="setLanguage('en')">
            <trans translation="English" />
        </button>
        <button ng-click="setLanguage('sv')">
            <trans translation="Svenska" />
        </button>
    </div>
    <hr />
    <p><trans translation="I am a Pirate" /></p>
    <p><trans translation="A text unit that doesn't exist" /></p>
    <p><input type="button" translation="My button" /></p>
</div>

This would work as following using the jsFiddle: http://jsfiddle.net/Oldek/95AH3/4/

This this solves:

  • Fetch translations asynchronously
  • Cache translations so switch is really fast
  • Working with input / fields with value
  • Store language in Local Storage
  • Dynamically update the whole DOM on translation change
  • Really quick, at least the scale I have tried
  • Full and working solution to a common problem

Things to solve:

  • Move out code that checks if it is an input field somewhere else
  • Pluralisation
  • Input Placeholder
  • And other features a translation supports.
  • Support parameters, see example:

<trans translation="Hello {{name}}" name="{{name}}">

  • Scan the project with xgettext? and generate an YML or similar structure that can be translated in some software.
  • Remove the temporary solution and use the working one commented out.

Other Comments

Please feel free to ask questions if you have, I'll provide info, and probably a jsFiddle some time soon in deemed needed.

/Marcus

1
votes

I Ended up using this solution. Solves all my problems:

http://cliffmeyers.com/blog/2013/3/11/integration-angularjs

0
votes

So I have come a bit further, and after some research it makes much more sense to use a filter for this action, however I cannot seem to get it working as I intend.

So this is what I got for application:

var app = angular.module('app', []);

app.factory('translationsService', function($http, translations, $q) {
    return {
        getTranslations: function(lang) {
            var deferred = $q.defer();
            $http({method: 'GET', url: '/translations/'+lang}).
                success(function (data) {
                    deferred.resolve({
                        data: data,
                        getWord: function(word) {
                            return data[word] ? data[word] : word;
                        }
                    });
                });
            return deferred.promise;
        }
    }
});

app.factory('wordService', function(translationsService, $q){
    return {
        lang: 'en-us',
        getWord: function(val){
            var translations = translationsService.getTranslations(this.lang);

            var deferred = $q.defer();
            translations.then(function(data) {
                deferred.resolve(data.getWord(val));
            });

            return deferred.promise;
        }
    }
});

app.filter('translate', function(wordService){
    return function(val){
        return wordService.getWord(val);
    }
});

So if I now do this in an html page:

{{ "User" | translate }}

Then I end up in an endless loop. Have I got the whole $q / promise thing wrong? I need some assistance here please.

However, if I use this by assigning it to a value in a controller it works fine.

In Controller I do:

app.controller('PageController',
    function PageController($scope, wordService) {
         $scope.someValue = wordService.getWord("USER");
    }
);

And then use it in html:

{{ someValue }}

And it works just fine.

/Marcus