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
I Ended up using this solution. Solves all my problems:
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.
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:
Things to solve:
<trans translation="Hello {{name}}" name="{{name}}">
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
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