Browse files

Added localization support to name/description elements

+Hijacked data2mxl node module in order to fix a bug in their code.

Fixes blackberry/BB10-Webworks-Packager#122

Reviewed By: Jeffrey Heifetz <jheifetz@rim.com>
Tested By: Tracy Li <tli@rim.com>
  • Loading branch information...
1 parent 087249b commit 30a3dece18067289305d8282340c945927daf3e4 @jkeshavarzi jkeshavarzi committed with nukulb Nov 12, 2012
View
49 lib/config-parser.js
@@ -418,9 +418,14 @@ function validateConfig(widgetConfig) {
check(widgetConfig.version, localize.translate("EXCEPTION_INVALID_VERSION"))
.notNull()
.regex("^[0-9]{1,3}([.][0-9]{1,3}){2,3}$");
- check(widgetConfig.name, localize.translate("EXCEPTION_INVALID_NAME")).notEmpty();
- check(widgetConfig.author, localize.translate("EXCEPTION_INVALID_AUTHOR")).notNull();
+ for (var prop in widgetConfig.name) {
+ if (widgetConfig.name.hasOwnProperty(prop)) {
+ check(widgetConfig.name[prop], localize.translate("EXCEPTION_INVALID_NAME")).notEmpty();
+ }
+ }
+
+ check(widgetConfig.author, localize.translate("EXCEPTION_INVALID_AUTHOR")).notNull();
check(widgetConfig.content, localize.translate("EXCEPTION_INVALID_CONTENT"))
.notNull()
.notEmpty();
@@ -492,6 +497,43 @@ function validateConfig(widgetConfig) {
}
}
+function processLocalizedText(tag, data, widgetConfig) {
+ var tagData = data[tag],
+ DEFAULT = 'default';
+
+ function processLanguage(tagElement) {
+ var attribs = tagElement['@'],
+ language;
+
+ if (attribs) {
+ language = attribs['xml:lang'] || DEFAULT;
+ widgetConfig[tag][language] = tagElement['#'];
+ } else {
+ widgetConfig[tag][DEFAULT] = tagElement;
+ }
+ }
+
+ if (Array.isArray(tagData)) {
+ //i.e. <element xml:lang="en">english value</element>
+ // <element xml:lang="fr">french value</element>
+ tagData.forEach(processLanguage);
+ } else if (tagData instanceof Object) {
+ //i.e. <element xml:lang="en">english value</element>
+ processLanguage(tagData);
+ } else {
+ //i.e <element>value</element>
+ widgetConfig[tag][DEFAULT] = tagData;
+ }
+}
+
+function processNameAndDescription(data, widgetConfig) {
+ widgetConfig.name = {};
+ widgetConfig.description = {};
+
+ processLocalizedText('name', data, widgetConfig);
+ processLocalizedText('description', data, widgetConfig);
+}
+
function processResult(data, session, extManager) {
var widgetConfig = {};
@@ -503,9 +545,8 @@ function processResult(data, session, extManager) {
processPermissionsData(data, widgetConfig);
processInvokeTargetsData(data, widgetConfig);
processSplashScreenData(data, widgetConfig);
+ processNameAndDescription(data, widgetConfig);
- widgetConfig.name = data.name;
- widgetConfig.description = data.description;
widgetConfig.configXML = "config.xml";
//validate the widgetConfig
View
30 lib/i18n-manager.js
@@ -87,6 +87,31 @@ function generateLocalizedMetadataForSplashScreenIcon(config, configKey, xmlObje
}
}
+function generateLocalizedText(session, config, xmlObject, key) {
+ var localizedText = config[key],
+ textElements = [],
+ locale;
+
+ for (locale in localizedText) {
+ if (localizedText.hasOwnProperty(locale)) {
+ //Don't add default locale and don't add locale if it already exists
+ if (locale !== 'default' && textElements && textElements.indexOf(locale) === -1) {
+ textElements.push({
+ _attr: {
+ "xml:lang": locale
+ },
+ _value: localizedText[locale]
+ });
+ }
+ }
+ }
+
+ xmlObject[key] = {
+ _value: localizedText['default'],
+ text: textElements
+ };
+}
+
function generateLocalizedMetadata(session, config, xmlObject, key) {
if (config.icon || config["rim:splash"]) {
var localeFiles,
@@ -111,5 +136,6 @@ function generateLocalizedMetadata(session, config, xmlObject, key) {
module.exports = {
LOCALES_DIR: LOCALES_DIR,
- generateLocalizedMetadata: generateLocalizedMetadata
-};
+ generateLocalizedMetadata: generateLocalizedMetadata,
+ generateLocalizedText: generateLocalizedText
+};
View
13 lib/native-packager.js
@@ -18,7 +18,7 @@ var childProcess = require("child_process"),
fs = require("fs"),
path = require("path"),
util = require("util"),
- data2xml = require("data2xml"),
+ data2xml = require("../third_party/data2xml/data2xml"),
wrench = require("wrench"),
conf = require("./conf"),
logger = require("./logger"),
@@ -31,7 +31,6 @@ function generateTabletXMLFile(session, config) {
var files = wrench.readdirSyncRecursive(session.sourceDir),
xmlObject = {
id : config.id,
- name : config.name,
versionNumber : config.version,
author : config.author,
asset : [{
@@ -83,6 +82,11 @@ function generateTabletXMLFile(session, config) {
});
}
+ i18nMgr.generateLocalizedText(session, config, xmlObject, "name");
+
+ if (config.description) {
+ i18nMgr.generateLocalizedText(session, config, xmlObject, "description");
+ }
i18nMgr.generateLocalizedMetadata(session, config, xmlObject, "icon");
i18nMgr.generateLocalizedMetadata(session, config, xmlObject, "rim:splash");
@@ -157,11 +161,6 @@ function generateTabletXMLFile(session, config) {
});
}
- //Add description element if specifed
- if (config.description) {
- xmlObject.description = config.description;
- }
-
//Add orientation mode
if (config.orientation) {
xmlObject.initialWindow.aspectRatio = config.orientation;
View
126 test/unit/lib/config-parser.js
@@ -46,8 +46,8 @@ describe("config parser", function () {
expect(configObj.authorURL).toEqual("http://www.rim.com/");
expect(configObj.copyright).toEqual("No Copyright");
expect(configObj.authorEmail).toEqual("author@rim.com");
- expect(configObj.name).toEqual("Demo");
- expect(configObj.description).toEqual("This app does everything.");
+ expect(configObj.name).toEqual({ default : 'Demo' });
+ expect(configObj.description).toEqual({ default : 'This app does everything.' });
expect(configObj.permissions).toContain('access_shared');
expect(configObj.permissions).toContain('read_geolocation');
expect(configObj.permissions).toContain('use_camera');
@@ -127,6 +127,128 @@ describe("config parser", function () {
});
});
+ it("Fails when no name was provided - single element", function () {
+ var data = testUtilities.cloneObj(testData.xml2jsConfig);
+ data.name = "";
+
+ mockParsing(data);
+
+ expect(function () {
+ configParser.parse(configPath, session, extManager, {});
+ }).toThrow(localize.translate("EXCEPTION_INVALID_NAME"));
+ });
+
+ it("Fails when no name was provided - multiple elements", function () {
+ var data = testUtilities.cloneObj(testData.xml2jsConfig);
+ data.name = ["",
+ { '#': 'API Smoke Test-FR', '@': { 'xml:lang': 'FR' } },
+ ];
+
+ mockParsing(data);
+
+ expect(function () {
+ configParser.parse(configPath, session, extManager, {});
+ }).toThrow(localize.translate("EXCEPTION_INVALID_NAME"));
+ });
+
+ it("Fails when localized name was provided but empty", function () {
+ var data = testUtilities.cloneObj(testData.xml2jsConfig);
+ data.name = ["API Smoke Test",
+ { '#': '', '@': { 'xml:lang': 'FR' } },
+ ];
+
+ mockParsing(data);
+
+ expect(function () {
+ configParser.parse(configPath, session, extManager, {});
+ }).toThrow(localize.translate("EXCEPTION_INVALID_NAME"));
+ });
+
+ it("Parses a name element - single element", function () {
+ var data = testUtilities.cloneObj(testData.xml2jsConfig);
+ data.name = "API Smoke Test";
+
+ mockParsing(data);
+
+ configParser.parse(configPath, session, extManager, function (configObj) {
+ expect(configObj.name).toEqual({"default": "API Smoke Test"});
+ });
+ });
+
+ it("Parses a name element with xml:lang - single element", function () {
+ var data = testUtilities.cloneObj(testData.xml2jsConfig);
+ data.name = { '#': 'EN VALUE', '@': { 'xml:lang': 'EN' } };
+
+ mockParsing(data);
+
+ configParser.parse(configPath, session, extManager, function (configObj) {
+ expect(configObj.name).toEqual({"EN": "EN VALUE"});
+ });
+ });
+
+ it("Parses a name element with xml:lang - multi element", function () {
+ var data = testUtilities.cloneObj(testData.xml2jsConfig);
+ data.name = ['API Smoke Test',
+ { '#': 'EN VALUE', '@': { 'xml:lang': 'EN' } },
+ { '#': 'FR VALUE', '@': { 'xml:lang': 'FR' } }
+
+ ];
+ mockParsing(data);
+
+ configParser.parse(configPath, session, extManager, function (configObj) {
+ expect(configObj.name).toEqual({"default": "API Smoke Test", "EN": "EN VALUE", "FR": "FR VALUE"});
+ });
+ });
+
+ it("Fails when localized name was provided but empty", function () {
+ var data = testUtilities.cloneObj(testData.xml2jsConfig);
+ data.name = ['API Smoke Test',
+ { '#': '', '@': { 'xml:lang': 'FR' } },
+ ];
+
+ mockParsing(data);
+
+ expect(function () {
+ configParser.parse(configPath, session, extManager, {});
+ }).toThrow(localize.translate("EXCEPTION_INVALID_NAME"));
+ });
+
+ it("Parses a description element - single element", function () {
+ var data = testUtilities.cloneObj(testData.xml2jsConfig);
+ data.description = "This is my app";
+
+ mockParsing(data);
+
+ configParser.parse(configPath, session, extManager, function (configObj) {
+ expect(configObj.description).toEqual({"default": "This is my app"});
+ });
+ });
+
+ it("Parses a description element with xml:lang - single element", function () {
+ var data = testUtilities.cloneObj(testData.xml2jsConfig);
+ data.description = { '#': 'EN VALUE', '@': { 'xml:lang': 'EN' } };
+
+ mockParsing(data);
+
+ configParser.parse(configPath, session, extManager, function (configObj) {
+ expect(configObj.description).toEqual({"EN": "EN VALUE"});
+ });
+ });
+
+ it("Parses a description element with xml:lang - multi element", function () {
+ var data = testUtilities.cloneObj(testData.xml2jsConfig);
+ data.description = ['This is my app',
+ { '#': 'EN VALUE', '@': { 'xml:lang': 'EN' } },
+ { '#': 'FR VALUE', '@': { 'xml:lang': 'FR' } }
+
+ ];
+ mockParsing(data);
+
+ configParser.parse(configPath, session, extManager, function (configObj) {
+ expect(configObj.description).toEqual({"default": "This is my app", "EN": "EN VALUE", "FR": "FR VALUE"});
+ });
+ });
+
it("Fails when missing content error is not shown", function () {
var data = testUtilities.cloneObj(testData.xml2jsConfig);
data.content = "";
View
80 test/unit/lib/native-packager.js
@@ -116,7 +116,6 @@ describe("Native packager", function () {
it("exec blackberry-nativepackager", function () {
var bbTabletXML = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" +
"<qnx><id>" + config.id + "</id>" +
- "<name>" + config.name + "</name>" +
"<versionNumber>" + config.version + "</versionNumber>" +
"<author>" + config.author + "</author>" +
"<asset entry=\"true\" type=\"qnx/elf\">wwe</asset>" +
@@ -127,7 +126,8 @@ describe("Native packager", function () {
"<env value=\"8\" var=\"WEBKIT_NUMBER_OF_BACKINGSTORE_TILES\"></env>" +
"<permission system=\"true\">run_native</permission>" +
"<permission system=\"false\">access_internet</permission>" +
- "<description>" + config.description + "</description></qnx>",
+ "<name>" + config.name['default'] + "</name>" +
+ "<description>" + config.description['default'] + "</description></qnx>",
cmd = path.normalize(session.conf.DEPENDENCIES_TOOLS + "/bin/blackberry-nativepackager" + (pkgrUtils.isWindows() ? ".bat" : ""));
spyOn(pkgrUtils, "writeFile");
@@ -139,6 +139,82 @@ describe("Native packager", function () {
expect(callback).toHaveBeenCalledWith(0);
});
+ it("can process application name", function () {
+ var config = testUtils.cloneObj(testData.config);
+ config.name = {"default": "API Smoke Test"};
+
+ spyOn(pkgrUtils, "writeFile").andCallFake(function (fileLocation, fileName, fileData) {
+ expect(fileData).toContain("<name>API Smoke Test</name>");
+ });
+
+ nativePkgr.exec(session, target, config, callback);
+
+ });
+
+ it("can process localized application name", function () {
+ var config = testUtils.cloneObj(testData.config);
+ config.name = {"FR": "API Smoke Test - FR"};
+
+ spyOn(pkgrUtils, "writeFile").andCallFake(function (fileLocation, fileName, fileData) {
+ expect(fileData).toContain('<name><text xml:lang="FR">API Smoke Test - FR</text></name>');
+ });
+
+ nativePkgr.exec(session, target, config, callback);
+ });
+
+ it("can process mutiple application names", function () {
+ var config = testUtils.cloneObj(testData.config);
+ config.name = {
+ "default": "API Smoke Test",
+ "EN": "API Smoke Test - EN",
+ "FR": "API Smoke Test - FR"
+ };
+
+ spyOn(pkgrUtils, "writeFile").andCallFake(function (fileLocation, fileName, fileData) {
+ expect(fileData).toContain('<name>API Smoke Test<text xml:lang="EN">API Smoke Test - EN</text><text xml:lang="FR">API Smoke Test - FR</text></name>');
+ });
+
+ nativePkgr.exec(session, target, config, callback);
+ });
+
+ it("can process application description", function () {
+ var config = testUtils.cloneObj(testData.config);
+ config.description = {"default": "My app description"};
+
+ spyOn(pkgrUtils, "writeFile").andCallFake(function (fileLocation, fileName, fileData) {
+ expect(fileData).toContain("<description>My app description</description>");
+ });
+
+ nativePkgr.exec(session, target, config, callback);
+
+ });
+
+ it("can process localized application description", function () {
+ var config = testUtils.cloneObj(testData.config);
+ config.description = {"FR": "My app description - FR"};
+
+ spyOn(pkgrUtils, "writeFile").andCallFake(function (fileLocation, fileName, fileData) {
+ expect(fileData).toContain('<description><text xml:lang="FR">My app description - FR</text></description>');
+ });
+
+ nativePkgr.exec(session, target, config, callback);
+ });
+
+ it("can process mutiple application descriptions", function () {
+ var config = testUtils.cloneObj(testData.config);
+ config.description = {
+ "default": "My app description",
+ "EN": "My app description - EN",
+ "FR": "My app description - FR"
+ };
+
+ spyOn(pkgrUtils, "writeFile").andCallFake(function (fileLocation, fileName, fileData) {
+ expect(fileData).toContain('<description>My app description<text xml:lang="EN">My app description - EN</text><text xml:lang="FR">My app description - FR</text></description>');
+ });
+
+ nativePkgr.exec(session, target, config, callback);
+ });
+
it("can process permissions with no attributes", function () {
var config = testUtils.cloneObj(testData.config);
config.permissions = ['read_device_identifying_information'];
View
4 test/unit/lib/test-data.js
@@ -32,10 +32,10 @@ module.exports = {
},
config: {
"id": 'Demo',
- "name": 'Demo',
+ "name": { 'default': 'Demo' },
"version": '1.0.0',
"author": 'Research In Motion Ltd.',
- "description": 'This is a test!',
+ "description": { 'default': 'This is a test!' },
"image": 'test.png',
"autoOrientation": true,
},
View
86 third_party/data2xml/data2xml.js
@@ -0,0 +1,86 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// data2xml.js - A data to XML converter with a nice interface (for NodeJS).
+//
+// Copyright (c) 2011 AppsAttic Ltd - http://www.appsattic.com/
+// Written by Andrew Chilton <chilts@appsattic.com>
+//
+// License: http://opensource.org/licenses/MIT
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+var xmlHeader = '<?xml version="1.0" encoding="utf-8"?>\n';
+
+function entitify(str) {
+ str = '' + str;
+ str = str
+ .replace(/&/g, '&amp;')
+ .replace(/</g,'&lt;')
+ .replace(/>/g,'&gt;')
+ .replace(/'/g, '&apos;')
+ .replace(/"/g, '&quot;');
+ return str;
+}
+
+function makeStartTag(name, attr) {
+ attr = attr || {};
+ var tag = '<' + name;
+ for(var a in attr) {
+ tag += ' ' + a + '="' + entitify(attr[a]) + '"';
+ }
+ tag += '>';
+ return tag;
+}
+
+function makeEndTag(name) {
+ return '</' + name + '>';
+}
+
+function makeElement(name, data) {
+ var element = '';
+ if ( Array.isArray(data) ) {
+ data.forEach(function(v) {
+ element += makeElement(name, v);
+ });
+ return element;
+ }
+ else if ( typeof data === 'object' ) {
+ element += makeStartTag(name, data._attr);
+ if ( data._value ) {
+ element += entitify(data._value);
+ }
+/************** MODIFICATION [always execute else condition] ***************/
+ for (var el in data) {
+ /**************** MODIFICATION {if condition altered} **********************/
+ if ( el === '_attr' || el === '_value') {
+ continue;
+ }
+ element += makeElement(el, data[el]);
+ }
+ element += makeEndTag(name);
+ return element;
+/***************************** END MODIFICATION ***************************/
+ }
+ else {
+ // a piece of data on it's own can't have attributes
+ return makeStartTag(name) + entitify(data) + makeEndTag(name);
+ }
+ throw "Unknown data " + data;
+}
+
+var data2xml = function(name, data) {
+ var xml = xmlHeader;
+ xml += makeElement(name, data);
+ return xml;
+};
+
+// --------------------------------------------------------------------------------------------------------------------
+
+data2xml.entitify = entitify;
+data2xml.makeStartTag = makeStartTag;
+data2xml.makeEndTag = makeEndTag;
+data2xml.makeElement = makeElement;
+
+module.exports = data2xml;
+
+// --------------------------------------------------------------------------------------------------------------------

0 comments on commit 30a3dec

Please sign in to comment.