diff --git a/Makefile b/Makefile index 042a56ffc9..883dc5ced1 100644 --- a/Makefile +++ b/Makefile @@ -35,6 +35,9 @@ imgs_server_dest := $(imgs_source:%=build/server/%) raven_source := $(shell node -e 'console.log(require.resolve("raven-js/dist/raven.js"))') +l10n_source := $(wildcard locales/*) +l10n_dest := $(l10n_source:%/webextension.properties=addon/webextension/_locales/%/messages.json) + ## General transforms: # These cover standard ways of building files given a source @@ -87,7 +90,7 @@ build/%.html: %.html cp $< $@ .PHONY: addon -addon: npm set_backend set_sentry addon/webextension/manifest.json addon/webextension/build/shot.js addon/webextension/build/inlineSelectionCss.js addon/webextension/build/raven.js addon/webextension/build/defaultSentryDsn.js +addon: npm set_backend set_sentry addon/webextension/manifest.json addon_locales addon/webextension/build/shot.js addon/webextension/build/inlineSelectionCss.js addon/webextension/build/raven.js addon/webextension/build/defaultSentryDsn.js .PHONY: zip zip: addon @@ -103,6 +106,9 @@ signed_xpi: addon ./node_modules/.bin/web-ext sign --api-key=${AMO_USER} --api-secret=${AMO_SECRET} --source-dir addon/webextension/ mv web-ext-artifacts/*.xpi build/pageshot.xpi +addon_locales: $(l10n_dest) + ./bin/pontoon-to-webext.js --dest addon/webextension/_locales + addon/webextension/manifest.json: addon/webextension/manifest.json.template build/.backend.txt package.json ./bin/build-scripts/update_manifest $< $@ diff --git a/addon/webextension/_locales/en_US/messages.json b/addon/webextension/_locales/en_US/messages.json index 1db2994bbe..3cb9c366bf 100644 --- a/addon/webextension/_locales/en_US/messages.json +++ b/addon/webextension/_locales/en_US/messages.json @@ -1,33 +1,79 @@ { - "addonDescription": { "message": "Page Shot takes clips and screenshots from pages, and can save a permanent copy of a page." }, - "addonAuthorsList": { "message": "Ian Bicking, Donovan Preston, and Bram Pitoyo" }, - "toolbarButtonLabel": { "message": "Take a shot" }, - "contextMenuLabel": { "message": "Create Page Shot" }, - "myShotsLink": { "message": "My Shots" }, - "screenshotInstructions": { "message": "Drag or click on the page to select a region. Press ESC to cancel." }, - "saveScreenshotSelectedArea": { "message": "Save" }, - "saveScreenshotVisibleArea": { "message": "Save visible" }, - "saveScreenshotFullPage": { "message": "Save full page" }, - "cancelScreenshot": { "message": "Cancel" }, - "downloadScreenshot": { "message": "Download" }, - "notificationLinkCopiedTitle": { "message": "Link Copied" }, + "addonDescription": { + "message": "Page Shot takes clips and screenshots from pages, and can save a permanent copy of a page." + }, + "addonAuthorsList": { + "message": "Ian Bicking, Donovan Preston, and Bram Pitoyo" + }, + "toolbarButtonLabel": { + "message": "Take a shot" + }, + "contextMenuLabel": { + "message": "Create Page Shot" + }, + "myShotsLink": { + "message": "My Shots" + }, + "screenshotInstructions": { + "message": "Drag or click on the page to select a region. Press ESC to cancel." + }, + "saveScreenshotSelectedArea": { + "message": "Save" + }, + "saveScreenshotVisibleArea": { + "message": "Save visible" + }, + "saveScreenshotFullPage": { + "message": "Save full page" + }, + "cancelScreenshot": { + "message": "Cancel" + }, + "downloadScreenshot": { + "message": "Download" + }, + "notificationLinkCopiedTitle": { + "message": "Link Copied" + }, "notificationLinkCopiedDetails": { "message": "The link to your shot has been copied to the clipboard. Press $META_KEY$-V to paste.", "placeholders": { - "META_KEY": { + "meta_key": { "content": "$1" - } + } } }, - "requestErrorTitle": { "message": "Page Shot is out of order." }, - "requestErrorDetails": { "message": "Your shot was not saved. We apologize for the inconvenience. Try again soon." }, - "connectionErrorTitle": { "message": "Cannot connect to the Page Shot server." }, - "connectionErrorDetails": { "message": "There may be a problem with the service or with your network connection." }, - "loginErrorDetails": { "message": "Your shot was not saved. There was an error authenticating with the server." }, - "loginConnectionErrorDetails": { "message": "There may be a problem with the service or your network connection." }, - "unshootablePageErrorTitle": { "message": "Page cannot be screenshotted." }, - "unshootablePageErrorDetails": { "message": "This is not a normal web page, and Page Shot cannot capture screenshots from it." }, - "selfScreenshotErrorTitle": { "message": "You can’t take a shot of a Page Shot page!" }, - "genericErrorTitle": { "message": "Page Shot went haywire." }, - "genericErrorDetails": { "message": "Try again or take a shot on another page?" } -} + "requestErrorTitle": { + "message": "Page Shot is out of order." + }, + "requestErrorDetails": { + "message": "Your shot was not saved. We apologize for the inconvenience. Try again soon." + }, + "connectionErrorTitle": { + "message": "Cannot connect to the Page Shot server." + }, + "connectionErrorDetails": { + "message": "There may be a problem with the service or with your network connection." + }, + "loginErrorDetails": { + "message": "Your shot was not saved. There was an error authenticating with the server." + }, + "loginConnectionErrorDetails": { + "message": "There may be a problem with the service or your network connection." + }, + "unshootablePageErrorTitle": { + "message": "Page cannot be screenshotted." + }, + "unshootablePageErrorDetails": { + "message": "This is not a normal web page, and Page Shot cannot capture screenshots from it." + }, + "selfScreenshotErrorTitle": { + "message": "You can’t take a shot of a Page Shot page!" + }, + "genericErrorTitle": { + "message": "Page Shot went haywire." + }, + "genericErrorDetails": { + "message": "Try again or take a shot on another page?" + } +} \ No newline at end of file diff --git a/bin/pontoon-to-webext.js b/bin/pontoon-to-webext.js new file mode 100755 index 0000000000..b4504a32e5 --- /dev/null +++ b/bin/pontoon-to-webext.js @@ -0,0 +1,165 @@ +#! /usr/bin/env node + +/* eslint-disable promise/avoid-new */ + +const propertiesParser = require('properties-parser'); +const path = require('path'); +const FS = require('q-io/fs'); +const argv = require('minimist')(process.argv.slice(2)); + +const Habitat = require('habitat'); +Habitat.load(); + +const regexPlaceholders = /\{([A-Za-z0-9_@]*)\}/g; +let supportedLocales = process.env.SUPPORTED_LOCALES || '*'; + +const config = { + 'dest': argv.dest || 'dist/_locales', + 'src': argv.src || 'locales', + 'default_locale': argv.locale || 'en-US' +}; + +function log(...args) { + console.log(...args); // eslint-disable-line no-console +} + +function error(...args) { + console.error(...args); // eslint-disable-line no-console +} + +function getListLocales() { + return new Promise((resolve, reject) => { + if (supportedLocales === '*') { + FS.listDirectoryTree(path.join(process.cwd(), config.src)).then((dirTree) => { + const localeList = []; + + // Get rid of the top level, we're only interested with what's inside it + dirTree.splice(0,1); + dirTree.forEach((localeLocation) => { + // Get the locale code from the end of the path. We're expecting the structure of Pontoon's output here + const langcode = localeLocation.split(path.sep).slice(-1)[0]; + + if (langcode) { + localeList.push(langcode); + } + }); + return resolve(localeList); + }).catch((e) => { + reject(e); + }); + } else { + supportedLocales = supportedLocales.split(',').map(item => item.trim()); + resolve(supportedLocales); + } + }); +} + +function writeFiles(entries) { + for (const entry of entries) { + const publicPath = path.join(process.cwd(), config.dest, entry.locale.replace('-', '_')); + const localesPath = path.join(publicPath, 'messages.json'); + + FS.makeTree(publicPath).then(() => { + return FS.write(localesPath, JSON.stringify(entry.content, null, 2)); + }).then(() => { + log(`Done compiling locales at: ${localesPath}`); + }).catch((e) => { + error(e); + }); + } +} + +function readPropertiesFile(filePath) { + return new Promise((resolve, reject) => { + propertiesParser.read(filePath, (messageError, messageProperties) => { + if (messageError && messageError.code !== 'ENOENT') { + return reject(messageError); + } + resolve(messageProperties); + }); + }); +} + +function getContentPlaceholders() { + return new Promise((resolve, reject) => { + FS.listTree(path.join(process.cwd(), config.src, config.default_locale), (filePath) => { + return path.extname(filePath) === '.properties'; + }).then((files) => { + return Promise.all(files.map(readPropertiesFile)).then((properties) => { + const mergedPlaceholders = {}; + + properties.forEach(messages => { + const placeholders = {}; + Object.keys(messages).forEach(key => { + const message = messages[key]; + if (message.indexOf('{') !== -1) { + const placeholder = {}; + let index = 1; + message.replace(regexPlaceholders, (item, key) => { + placeholder[key.toLowerCase()] = { content: `$${index}` }; + index++; + }); + placeholders[key] = placeholder; + } + }); + Object.assign(mergedPlaceholders, placeholders); + }); + + resolve(mergedPlaceholders); + }); + }).catch((e) => { + reject(e); + }); + }); +} + +function getContentMessages(locale, placeholders) { + return new Promise((resolve, reject) => { + FS.listTree(path.join(process.cwd(), config.src, locale), (filePath) => { + return path.extname(filePath) === '.properties'; + }).then((files) => { + return Promise.all(files.map(readPropertiesFile)).then((properties) => { + const mergedProperties = {}; + + properties.forEach(messages => { + Object.keys(messages).forEach(key => { + let message = messages[key]; + messages[key] = { 'message': message }; + if (placeholders[key]) { + message = message.replace(regexPlaceholders, (item, key) => `\$${key.toUpperCase()}\$`); + messages[key] = { + 'message': message, + 'placeholders': placeholders[key] + }; + } + }); + Object.assign(mergedProperties, messages); + }); + + resolve({content: mergedProperties, locale: locale}); + }); + }).catch((e) => { + reject(e); + }); + }); +} + +function processMessageFiles(locales) { + if (!locales) { + error('List of locales was undefined. Cannot run pontoon-to-webext.'); + process.exit(1); + } + if (locales.length === 0) { + error('Locale list is empty. Cannot run pontoon-to-webext.'); + process.exit(1); + } + log(`processing the following locales: ${locales.toString()}`); + return getContentPlaceholders().then(placeholders => { + return Promise.all(locales.map(locale => getContentMessages(locale, placeholders))); + }); +} + +getListLocales().then(processMessageFiles) +.then(writeFiles).catch((err)=> { + error(err); +}); diff --git a/locales/en-US/webextension.properties b/locales/en-US/webextension.properties new file mode 100644 index 0000000000..9943554148 --- /dev/null +++ b/locales/en-US/webextension.properties @@ -0,0 +1,27 @@ +addonDescription = Page Shot takes clips and screenshots from pages, and can save a permanent copy of a page. +addonAuthorsList = Ian Bicking, Donovan Preston, and Bram Pitoyo +toolbarButtonLabel = Take a shot +contextMenuLabel = Create Page Shot +myShotsLink = My Shots +screenshotInstructions = Drag or click on the page to select a region. Press ESC to cancel. +saveScreenshotSelectedArea = Save +saveScreenshotVisibleArea = Save visible +saveScreenshotFullPage = Save full page +cancelScreenshot = Cancel +downloadScreenshot = Download +notificationLinkCopiedTitle = Link Copied +# LOCALIZATION NOTE(notificationLinkCopiedDetails): The string "{meta_key}-V" should be translated +# to the region-specific shorthand for the Paste keyboard shortcut. {meta_key} is a placeholder for +# the modifier key used to complete the paste: for example, Ctrl-V on Windows systems. +notificationLinkCopiedDetails = The link to your shot has been copied to the clipboard. Press {meta_key}-V to paste. +requestErrorTitle = Page Shot is out of order. +requestErrorDetails = Your shot was not saved. We apologize for the inconvenience. Try again soon. +connectionErrorTitle = Cannot connect to the Page Shot server. +connectionErrorDetails = There may be a problem with the service or with your network connection. +loginErrorDetails = Your shot was not saved. There was an error authenticating with the server. +loginConnectionErrorDetails = There may be a problem with the service or your network connection. +unshootablePageErrorTitle = Page cannot be screenshotted. +unshootablePageErrorDetails = This is not a normal web page, and Page Shot cannot capture screenshots from it. +selfScreenshotErrorTitle = You can’t take a shot of a Page Shot page! +genericErrorTitle = Page Shot went haywire. +genericErrorDetails = Try again or take a shot on another page? diff --git a/package.json b/package.json index 1d267f07f1..b3d012a500 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,10 @@ "envc": "2.4.1", "escape-html": "1.0.3", "express": "4.14.1", + "habitat": "^3.1.2", "jpm": "1.3.0", "keygrip": "1.0.1", + "minimist": "^1.2.0", "mobile-detect": "1.3.5", "morgan": "1.7.0", "mozlog": "2.0.6", @@ -26,6 +28,8 @@ "nodify-uuid": "0.0.1", "pg": "6.1.2", "pg-patcher": "0.3.0", + "properties-parser": "^0.3.1", + "q-io": "^1.13.2", "raven": "1.1.1", "raven-js": "3.10.0", "react": "15.4.2", @@ -52,6 +56,7 @@ "node-sass": "4.4.0", "npm-run-all": "4.0.1", "nsp": "2.6.2", + "properties-parser": "^0.3.1", "sass-lint": "1.10.2", "selenium-webdriver": "3.0.1", "svgo": "0.7.2",