diff --git a/.gitignore b/.gitignore index c63e7348a8..20620faee3 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ cookie-jar.txt /addon/webextension/build /addon/webextension/.web-extension-id /addon/webextension/manifest.json +/addon/webextension/_locales diff --git a/Makefile b/Makefile index 042a56ffc9..277391b224 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,10 @@ 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 +.PHONY: addon_locales +addon_locales: $(l10n_dest) + ./bin/build-scripts/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 deleted file mode 100644 index 1db2994bbe..0000000000 --- a/addon/webextension/_locales/en_US/messages.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "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": { - "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?" } -} diff --git a/bin/build-scripts/pontoon-to-webext.js b/bin/build-scripts/pontoon-to-webext.js new file mode 100755 index 0000000000..a2042226fe --- /dev/null +++ b/bin/build-scripts/pontoon-to-webext.js @@ -0,0 +1,168 @@ +#! /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 fatal(...args) { + error(...args); + process.exit(1); +} + +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) => { + fatal(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) { + fatal('List of locales was undefined. Cannot run pontoon-to-webext.'); + } + if (locales.length === 0) { + fatal('Locale list is empty. Cannot run pontoon-to-webext.'); + } + 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..c350a77cf7 --- /dev/null +++ b/locales/en-US/webextension.properties @@ -0,0 +1,28 @@ +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 +# 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 in the shortcut (do not translate it): 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 d17eb8c333..c241734386 100644 --- a/package.json +++ b/package.json @@ -48,10 +48,14 @@ "eslint-plugin-react": "6.10.0", "fx-runner": "1.0.6", "geckodriver": "1.4.0", + "habitat": "3.1.2", + "minimist": "1.2.0", "mocha": "3.2.0", "node-sass": "4.5.0", "npm-run-all": "4.0.2", "nsp": "2.6.3", + "properties-parser": "0.3.1", + "q-io": "1.13.2", "sass-lint": "1.10.2", "selenium-webdriver": "3.3.0", "svgo": "0.7.2",