Commit 340f9a7a authored by abirembaut's avatar abirembaut Committed by benjaminParisel

feat(community page build) Support localization file (#2511)

* feat(community build) Support localization file

- fix pbFileViewer unit tests
parent cb21d272
......@@ -20,7 +20,8 @@
"angular-sanitize": "1.3.18",
"bootstrap": "3.3.5",
"angular-messages": "1.3.18",
"angular-gettext": "2.1.0"
"angular-gettext": "2.1.0",
"angular-cookies": "1.3.11"
},
"resolutions": {
"angular": "1.3.18"
......
......@@ -13,7 +13,8 @@ var paths = {
'bower_components/angular/angular.min.js',
'bower_components/angular-sanitize/angular-sanitize.min.js',
'bower_components/angular-messages/angular-messages.min.js',
'bower_components/angular-gettext/dist/angular-gettext.min.js'
'bower_components/angular-gettext/dist/angular-gettext.min.js',
'bower_components/angular-cookies/angular-cookies.min.js'
],
fonts: [
'bower_components/bootstrap/dist/fonts/*.*'
......
/**
* Copyright (C) 2015 Bonitasoft S.A.
* Bonitasoft, 32 rue Gustave Eiffel - 38000 Grenoble
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2.0 of the License, or
* (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.bonitasoft.web.designer.localization;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.bonitasoft.web.designer.model.Assetable;
import org.bonitasoft.web.designer.model.Identifiable;
import org.bonitasoft.web.designer.model.page.Page;
import org.bonitasoft.web.designer.model.page.Previewable;
import org.bonitasoft.web.designer.rendering.TemplateEngine;
import org.bonitasoft.web.designer.repository.PageRepository;
import org.bonitasoft.web.designer.visitor.PageFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.Path;
import static java.lang.String.format;
@Named
public class LocalizationFactory implements PageFactory {
private PageRepository pageRepository;
private static final Logger logger = LoggerFactory.getLogger(LocalizationFactory.class);
@Inject
public LocalizationFactory(PageRepository pageRepository) {
this.pageRepository = pageRepository;
}
@Override
public <P extends Previewable & Identifiable> String generate(P previewable) {
if (previewable instanceof Assetable) {
return new TemplateEngine("localizationFactory.hbs.js")
.with("localization", getLocalization(previewable))
.build(new Object());
}
return "";
}
private <P extends Previewable & Identifiable> Object getLocalization(P previewable) {
if (previewable instanceof Page) {
try {
Path localizationPath = pageRepository.resolvePath(previewable.getId()).resolve("assets/json/localization.json");
return new ObjectMapper().readValue(localizationPath.toFile(), JsonNode.class);
} catch (FileNotFoundException e) {
logger.warn(format("%s <%s> has no localization.json file.", previewable.getType(), previewable.getId()));
return "{}";
} catch (IOException e) {
logger.error(format("An error occurred while loading assets/json/localization.json for page <%s>", previewable.getId()), e);
return "{}";
}
} else {
return "{}";
}
}
}
angular.module('bonitasoft.ui.services').factory('localizationFactory', function() {
return {
get: function() {
return {{{ localization }}};
}
};
});
/*
* AngularJS merge method polyfill (mandatory for AngularJS < 1.4)
* retrieved from https://gist.github.com/luckylooke/8433aa366bb680014cd0
*/
(() => {
if (!angular.merge) {
angular.merge = (function mergePollyfill() {
function setHashKey(obj, h) {
if (h) {
obj.$$hashKey = h;
} else {
delete obj.$$hashKey;
}
}
function baseExtend(dst, objs, deep) {
var h = dst.$$hashKey;
for (var i = 0, ii = objs.length; i < ii; ++i) {
var obj = objs[i];
if (!angular.isObject(obj) && !angular.isFunction(obj)) { continue; }
var keys = Object.keys(obj);
for (var j = 0, jj = keys.length; j < jj; j++) {
var key = keys[j];
var src = obj[key];
if (deep && angular.isObject(src)) {
if (angular.isDate(src)) {
dst[key] = new Date(src.valueOf());
} else {
if (!angular.isObject(dst[key])) { dst[key] = angular.isArray(src) ? [] : {}; }
baseExtend(dst[key], [src], true);
}
} else {
dst[key] = src;
}
}
}
setHashKey(dst, h);
return dst;
}
return function merge(dst) {
return baseExtend(dst, [].slice.call(arguments, 1), true);
};
})();
}
})();
(function() {
'use strict';
angular
.module('bonitasoft.ui.i18n')
.factory('i18nCodeMapping', i18nCodeMapping);
function i18nCodeMapping() {
return {
get
};
/**
* Matches first the exact code, then matches
* - fr <=> fr-FR || fr-fr || fr_FR || fr_fr
* - fr-FR <=> fr_FR || fr_fr
* - fr_FR <=> fr-FR || fr-fr
* - fr-BE <=> fr
* - fr_BE <=> fr
*/
function get(code, codes) {
return match(codes, code) ||
match(codes, code + '_' + code) ||
match(codes, code.replace('-', '_')) ||
match(codes, code.substring(0, code.indexOf('-'))) ||
match(codes, code.substring(0, code.indexOf('_'))) ||
code;
}
function match(codes, pattern) {
return (codes.filter((c) => c.toLowerCase().replace('_', '-') === pattern.toLowerCase().replace('_', '-')))[0];
}
}
})();
angular.module('bonitasoft.ui.i18n').factory('i18n', function(gettextCatalog, $cookies, $window, localizationFactory, i18nCodeMapping, $locale) {
return {
/*jshint camelcase:false*/
init() {
// jscs:disable requireCamelCaseOrUpperCaseIdentifiers
let language = $cookies.BOS_Locale ||
$window.navigator.language ||
$window.navigator.userLanguage || // IE
'en-US';
let localization = localizationFactory.get();
let languages = Object.keys(localization);
let currentCode = i18nCodeMapping.get(language, languages);
if (localization && localization[currentCode] && localization[currentCode].$locale) {
angular.merge($locale, localization[currentCode].$locale);
}
languages.forEach(function(language) {
gettextCatalog.setStrings(language, localization[language]);
});
gettextCatalog.setCurrentLanguage(currentCode);
}
};
});
......@@ -3,6 +3,7 @@
angular.module('bonitasoft.ui.constants', []);
angular.module('bonitasoft.ui.services', ['gettext']);
angular.module('bonitasoft.ui.i18n', ['ngCookies', 'gettext']);
angular.module('bonitasoft.ui.directives', ['gettext']);
angular.module('bonitasoft.ui.filters', ['gettext']);
angular.module('bonitasoft.ui.widgets', ['bonitasoft.ui.filters', 'bonitasoft.ui.services', 'ngSanitize']);
......@@ -16,6 +17,7 @@
'ngSanitize',
'ngMessages',
'bonitasoft.ui.templates',
'bonitasoft.ui.i18n',
'bonitasoft.ui.constants',
'bonitasoft.ui.services',
'bonitasoft.ui.directives',
......@@ -25,4 +27,3 @@
'pb.generator'
]);
})();
angular.module('bonitasoft.ui').run(function(i18n) {
i18n.init();
});
(function () {
'use strict';
angular.module('bonitasoft.ui.services').service('InterpolationBinding', (Binding, $interpolate) => (
angular.module('bonitasoft.ui.services').service('InterpolationBinding', (Binding, gettextCatalog) => (
class InterpolationBinding extends Binding {
getValue() {
return $interpolate(this.property.value || '')(this.context);
return gettextCatalog.getString(this.property.value || '', this.context);
}
}
));
......
/**
* Copyright (C) 2015 Bonitasoft S.A.
* Bonitasoft, 32 rue Gustave Eiffel - 38000 Grenoble
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2.0 of the License, or
* (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.bonitasoft.web.designer.localization;
import org.bonitasoft.web.designer.model.page.Page;
import org.bonitasoft.web.designer.repository.PageRepository;
import org.bonitasoft.web.designer.utils.rule.TemporaryFolder;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import java.io.File;
import static java.lang.String.format;
import static org.apache.commons.io.FileUtils.deleteQuietly;
import static org.apache.commons.io.FileUtils.writeByteArrayToFile;
import static org.assertj.core.api.Assertions.assertThat;
import static org.bonitasoft.web.designer.builder.PageBuilder.aPage;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class LocalizationFactoryTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Mock
PageRepository pageRepository;
@InjectMocks
LocalizationFactory localizationFactory;
String localizationFileContent = "{\"fr-FR\":{\"Hello\":\"Bonjour\"}}";
Page page = aPage().withId("page").build();
File localizationFile;
@Before
public void setUp() throws Exception {
File pageFolder = temporaryFolder.newFolder("page");
temporaryFolder.newFolder("page", "assets", "json");
localizationFile = temporaryFolder.newFile("page/assets/json/localization.json");
when(pageRepository.resolvePath("page")).thenReturn(pageFolder.toPath());
}
@Test
public void should_create_a_factory_which_contains_localizations() throws Exception {
writeByteArrayToFile(localizationFile, localizationFileContent.getBytes());
assertThat(localizationFactory.generate(page))
.isEqualTo(createFactory(localizationFileContent));
}
@Test
public void should_create_an_empty_factory_whenever_localization_file_is_not_valid_json() throws Exception {
writeByteArrayToFile(localizationFile, "invalid json".getBytes());
assertThat(localizationFactory.generate(page))
.isEqualTo(createFactory("{}"));
}
@Test
public void should_create_an_empty_factory_whenever_localization_file_does_not_exist() throws Exception {
deleteQuietly(localizationFile);
assertThat(localizationFactory.generate(page))
.isEqualTo(createFactory("{}"));
}
private String createFactory(String content) {
return format("angular.module('bonitasoft.ui.services').factory('localizationFactory', function() {" + System.lineSeparator() +
" return {" + System.lineSeparator() +
" get: function() {" + System.lineSeparator() +
" return %s;" + System.lineSeparator() +
" }" + System.lineSeparator() +
" };" + System.lineSeparator() +
"});" + System.lineSeparator(), content);
}
}
......@@ -25,6 +25,7 @@ module.exports = function (config) {
'node_modules/angular-mocks/angular-mocks.js',
'bower_components/angular-sanitize/angular-sanitize.min.js',
'bower_components/angular-gettext/dist/angular-gettext.min.js',
'bower_components/angular-cookies/angular-cookies.min.js',
'src/main/runtime/js/**/*.module.js',
'src/main/runtime/js/**/*.js',
......
describe('i18n code mapping', function () {
var i18nCodeMapping;
beforeEach(module('bonitasoft.ui.i18n'));
beforeEach(inject(function (_i18nCodeMapping_) {
i18nCodeMapping= _i18nCodeMapping_;
}));
it('should choose exact code first if exists', function() {
expect(i18nCodeMapping.get('fr', ['fr-FR', 'fr_FR', 'fr', 'en-US'])).toBe('fr');
});
it('should get corresponding hyphen separated code for two letter code ignoring case', function() {
expect(i18nCodeMapping.get('fr', ['fr-FR', 'fr_FR', 'en-US'])).toBe('fr-FR');
expect(i18nCodeMapping.get('fr', ['fr-fr', 'fr_FR', 'en-US'])).toBe('fr-fr');
});
it('should get corresponding underscore separated code for two letter code ignoring case', function() {
expect(i18nCodeMapping.get('fr', ['fr_FR', 'en-US'])).toBe('fr_FR');
expect(i18nCodeMapping.get('fr', ['fr_fr', 'en-US'])).toBe('fr_fr');
});
it('should get corresponding two letter code for hyphen/underscore separated code ignoring case', function() {
expect(i18nCodeMapping.get('fr-BE', ['fr', 'it'])).toBe('fr');
expect(i18nCodeMapping.get('fr_BE', ['fr', 'it'])).toBe('fr');
expect(i18nCodeMapping.get('fr_BE', ['us', 'it'])).toBe('fr_BE');
});
it('should get corresponding code for hyphen/underscore code', function() {
expect(i18nCodeMapping.get('fr-FR', ['fr_FR', 'en_US'])).toBe('fr_FR');
expect(i18nCodeMapping.get('fr_FR', ['fr-FR', 'en-US'])).toBe('fr-FR');
});
it('should get return code in argument otherwise', function() {
expect(i18nCodeMapping.get('unknown', ['fr_FR', 'en_US'])).toBe('unknown');
});
});
describe('i18n', function () {
var i18n, gettextCatalog, $cookies, $window, $q, localization, $locale;
beforeEach(module('bonitasoft.ui.i18n', function ($provide) {
localization = {
'fr-FR': {
'Hello': 'Bonjour',
'$locale': {
'DATETIME_FORMATS': {
'DAY': [
'dimanche',
'lundi',
'mardi',
'mercredi',
'jeudi',
'vendredi',
'samedi'
]
}
}
}
};
$provide.value('localizationFactory', {
get: function () {
return localization;
}
});
}));
beforeEach(inject(function ($injector, _$locale_) {
i18n = $injector.get('i18n');
gettextCatalog = $injector.get('gettextCatalog');
$cookies = $injector.get('$cookies');
$window = $injector.get('$window');
$q = $injector.get('$q');
$locale = _$locale_;
}));
it('should pick up BOS_Locale cookie value', function () {
$cookies.BOS_Locale = 'fr-FR';
i18n.init();
expect(gettextCatalog.getCurrentLanguage()).toBe('fr-FR');
expect($locale.DATETIME_FORMATS.DAY[0]).toEqual('dimanche');
});
it('should pick up navigator language value', function () {
$window.navigator = {
language: 'fr-FR'
};
i18n.init();
expect(gettextCatalog.getCurrentLanguage()).toBe('fr-FR');
});
it('should pick up ie language value', function () {
$window.navigator = {
userLanguage: 'fr-FR'
};
i18n.init();
expect(gettextCatalog.getCurrentLanguage()).toBe('fr-FR');
});
it('should default to en when no preference is defined', function () {
$window.navigator = {};
i18n.init();
expect(gettextCatalog.getCurrentLanguage()).toBe('en-US');
});
it('should normalize locale when coming from portal', function () {
$cookies.BOS_Locale = 'it';
localization = {'it-IT': {'Hello': 'Bonjourno'}};
i18n.init();
expect(gettextCatalog.getCurrentLanguage()).toBe('it-IT');
expect($locale.DATETIME_FORMATS.DAY[0]).toEqual('Sunday');
expect($locale.NUMBER_FORMATS).not.toBeUndefined();
});
it('should set localization strings', function () {
spyOn(gettextCatalog, 'setStrings');
i18n.init();
expect(gettextCatalog.setStrings).toHaveBeenCalledWith('fr-FR', localization['fr-FR']);
});
it('should merge $locale date time format with those of the localization ones', () => {
$cookies.BOS_Locale = 'fr';
localization = {
'fr-FR': {
'Hello': 'Bonjour',
'$locale': {
'DATETIME_FORMATS': {
'DAY': [
'dimanche',
'lundi',
'mardi',
'mercredi',
'jeudi',
'vendredi',
'samedi'
]
}
}
}
};
i18n.init();
expect($locale.DATETIME_FORMATS.longDate).toEqual('MMMM d, y');
expect($locale.DATETIME_FORMATS.DAY[0]).toEqual('dimanche');
});
it('should set localization strings', function () {
spyOn(gettextCatalog, 'setStrings');
i18n.init();
expect(gettextCatalog.setStrings).toHaveBeenCalledWith('fr-FR', localization['fr-FR']);
});
});
(function () {
'use strict';
describe('bonitasoft.ui module', function () {
beforeEach(function () {
//these modules are not loaded in karma conf
angular.module('ngMessages', []);
angular.module('ngUpload', []);
angular.module('bonitasoft.ui.templates', []);
});
beforeEach(module('bonitasoft.ui'));
beforeEach(function () {
angular.module('pb.generator').filter('someTimeAgo', function () {
return function someTimeAgo() {
return 'some time ago...';
};
});
angular.module('pb.widgets').directive('pbTestWidget', function () {
return {
restrict: 'AE',
template: '<div id="testWidget">{{ "Bonitasoft" | someTimeAgo }}</div>'
};
});
});
it('should still be compatible with pb.widgets', inject(function ($compile, $rootScope) {
var element = $compile('<pb-test-widget></pb-test-widget>')($rootScope);
$rootScope.$apply();
expect(element.find('#testWidget').text()).toEqual('some time ago...');
}));
});
})();
......@@ -2,10 +2,11 @@ describe('Service: bindingService', function () {
beforeEach(module('bonitasoft.ui.services'));
let bindingService, property, context;
let bindingService, property, context, gettextCatalog;
beforeEach(inject(function (_bindingService_) {
beforeEach(inject(function (_bindingService_, _gettextCatalog_) {
bindingService = _bindingService_;
gettextCatalog = _gettextCatalog_;
property = {};
context = {};
}));
......@@ -109,4 +110,19 @@ describe('Service: bindingService', function () {
bindingService.create(property, context);
}).toThrowError();
});
it('should translate an interpolated variable', function() {
gettextCatalog.setStrings('fr-FR', {
'Hello {{ userName }}': 'Bonjour {{ userName }}'
});
gettextCatalog.setCurrentLanguage('fr-FR');
property.type = 'interpolation';
property.value = 'Hello {{ userName }}';
context.userName = 'Vincent';
let binding = bindingService.create(property, context);
expect(binding.constructor.name).toBe('InterpolationBinding');
expect(binding.getValue()).toBe('Bonjour Vincent');
});
});
......@@ -35,6 +35,7 @@
'org.bonitasoft.dragAndDrop',
'gettext',
'ngUpload',
'ngCookies',
'angularMoment',
'angularResizable'
]);
......
......@@ -8,4 +8,5 @@ input.datepicker{
width: 1px;
height: @input-height-base;
position: absolute;
}
\ No newline at end of file
}
......@@ -25,6 +25,7 @@
"angular-ui-ace": "0.1.1",
"bonita-js-components": "bonitasoft/bonita-js-components#0.5.3",
"angular-gettext": "2.0.1",
"angular-cookies": "1.4.7",
"angular-ui-router": "0.2.13",
"sockjs": "0.3.4",
"stomp-websocket": "2.3.4",
......
......@@ -19,6 +19,7 @@ var paths = {
'bower_components/angular-bootstrap/ui-bootstrap-tpls.min.js',
'bower_components/angular-ui-validate/dist/validate.js',
'bower_components/angular-ui-ace/ui-ace.min.js',
'bower_components/angular-cookies/angular-cookies.min.js',
'bower_components/ngUpload/ng-upload.min.js',
'bower_components/bonita-js-components/dist/bonita-lib-tpl.min.js',
'bower_components/angular-gettext/dist/angular-gettext.min.js',
......
......@@ -36,6 +36,7 @@ module.exports = function(config) {
'bower_components/identicon.js/identicon.js',
'bower_components/jsSHA/src/sha1.js',
'bower_components/angular-sha/src/angular-sha.js',
'bower_components/angular-cookies/angular-cookies.min.js',
'node_modules/angular-switcher/dist/angular-switcher.min.js',
'node_modules/ngstorage/ngStorage.min.js',
'node_modules/angular-resizable/angular-resizable.min.js',
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment