diff --git a/chrome/locale/en-US/zotero/zotero.ftl b/chrome/locale/en-US/zotero/zotero.ftl
index c430cc740e..8425b66583 100644
--- a/chrome/locale/en-US/zotero/zotero.ftl
+++ b/chrome/locale/en-US/zotero/zotero.ftl
@@ -55,8 +55,7 @@ file-interface-items-were-relinked = { $numRelinked ->
*[other] { $numRelinked } items were relinked
}
-import-mendeley-encrypted = The selected Mendeley database cannot be read, likely because it is encrypted.
- See How do I import a Mendeley library into Zotero? for more information.
+import-mendeley-encrypted = The selected Mendeley database cannot be read, likely because it is encrypted. See How do I import a Mendeley library into Zotero? for more information.
file-interface-import-error-translator = An error occurred importing the selected file with “{ $translator }”. Please ensure that the file is valid and try again.
diff --git a/js-build/ftl-to-json.mjs b/js-build/ftl-to-json.mjs
new file mode 100644
index 0000000000..db812d8603
--- /dev/null
+++ b/js-build/ftl-to-json.mjs
@@ -0,0 +1,38 @@
+import { ftlToJSON } from "ftl-tx";
+import fs from 'fs-extra';
+import { dirname, join } from 'path';
+import { fileURLToPath } from 'url';
+import { onError, onSuccess } from './utils.js';
+
+const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
+const JSONDir = join(ROOT, 'tmp', 'tx');
+
+async function getJSON() {
+ const t1 = performance.now();
+ await fs.mkdirp(JSONDir);
+ const sourcefile = join(ROOT, 'chrome', 'locale', 'en-US', 'zotero', 'zotero.ftl');
+ const destFile = join(JSONDir, 'zotero_en_US.json');
+ const ftl = await fs.readFile(sourcefile, 'utf8');
+ const json = ftlToJSON(ftl, { transformTerms: false, storeTermsInJSON: false });
+ await fs.outputJSON(destFile, json);
+ const t2 = performance.now();
+ return ({
+ action: 'ftl->json',
+ count: 1,
+ totalCount: 1,
+ processingTime: t2 - t1
+ });
+}
+
+if (process.argv[1] === fileURLToPath(import.meta.url)) {
+ (async () => {
+ try {
+ onSuccess(await getJSON());
+ }
+ catch (err) {
+ process.exitCode = 1;
+ global.isError = true;
+ onError(err);
+ }
+ })();
+}
diff --git a/js-build/localize-ftl.mjs b/js-build/localize-ftl.mjs
new file mode 100644
index 0000000000..3c06bfd430
--- /dev/null
+++ b/js-build/localize-ftl.mjs
@@ -0,0 +1,98 @@
+import { extractTerms, ftlToJSON, JSONToFtl } from 'ftl-tx';
+import fs from 'fs-extra';
+import { dirname, join } from 'path';
+import { fileURLToPath } from 'url';
+import { onError, onProgress, onSuccess } from './utils.js';
+import { exit } from 'process';
+
+const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
+const JSONDir = join(ROOT, 'tmp', 'tx');
+const localesDir = join(ROOT, 'chrome', 'locale');
+const sourceDir = join(localesDir, 'en-US', 'zotero');
+
+const termsSourceFTLPath = join(ROOT, 'app', 'assets', 'branding', 'locale', 'brand.ftl');
+const fallbackJSONPath = join(sourceDir, 'zotero.json');
+
+// don't override source zotero.ftl
+const localeToSkip = ['en-US'];
+
+async function getFTL() {
+ const t1 = performance.now();
+ if (!(await fs.pathExists(fallbackJSONPath))) {
+ console.error(`File ${fallbackJSONPath} does not exist, please run 'ftl-to-json' first`);
+ exit(1);
+ }
+
+ if (!(await fs.pathExists(termsSourceFTLPath))) {
+ console.error(`Required file ${termsSourceFTLPath} does not exist`);
+ exit(1);
+ }
+
+ const fallbackJSON = await fs.readJSON(fallbackJSONPath);
+ const terms = extractTerms(await fs.readFile(termsSourceFTLPath, 'utf-8'));
+
+ const expectedLocale = (await fs.readdir(localeDir, { withFileTypes: true }))
+ .filter(dirent => dirent.isDirectory())
+ .map(dirent => dirent.name);
+
+ const totalCount = expectedLocale.length;
+ let locale;
+ let count = 0;
+
+ while ((locale = expectedLocale.pop())) {
+ if (localeToSkip.includes(locale)) {
+ count++;
+ continue;
+ }
+
+ const ftlFilePath = join(ROOT, 'chrome', 'locale', locale, 'zotero', 'zotero.ftl');
+ let JSONFromLocalFTL = {};
+ try {
+ const ftl = await fs.readFile(ftlFilePath, 'utf8');
+ JSONFromLocalFTL = ftlToJSON(ftl, { transformTerms: false, storeTermsInJSON: false });
+ }
+ catch (e) {
+ // no local .ftl file
+ }
+
+ const JSONFilePath = join(JSONDir, `zotero_${locale.replace('-', '_')}.json`);
+ let JSONFromTransifex = {};
+ try {
+ const json = await fs.readJSON(JSONFilePath);
+ JSONFromTransifex = json;
+ }
+ catch (e) {
+ // no .json file from transifex
+ }
+
+ const mergedJSON = { ...fallbackJSON, ...JSONFromLocalFTL, ...JSONFromTransifex };
+ const ftl = JSONToFtl(mergedJSON, { addTermsToFTL: false, storeTermsInJSON: false, transformTerms: false, terms });
+
+ const outFtlPath = join(ROOT, 'chrome', 'locale', locale, 'zotero', 'zotero.ftl');
+ await fs.outputFile(outFtlPath, ftl);
+ onProgress(outFtlPath, outFtlPath, 'ftl');
+ count++;
+ }
+
+ const t2 = performance.now();
+ return ({
+ action: 'ftl',
+ count,
+ totalCount,
+ processingTime: t2 - t1
+ });
+}
+
+
+if (process.argv[1] === fileURLToPath(import.meta.url)) {
+ (async () => {
+ try {
+ onSuccess(await getFTL());
+ }
+ catch (err) {
+ process.exitCode = 1;
+ global.isError = true;
+ onError(err);
+ }
+ })();
+}
diff --git a/js-build/utils.js b/js-build/utils.js
index 3cb1480d6b..2474fcf161 100644
--- a/js-build/utils.js
+++ b/js-build/utils.js
@@ -21,7 +21,7 @@ function onSuccess(result) {
msg += ` | ${result.totalCount} checked`;
}
- msg += ` [${yellow(`${result.processingTime}ms`)}]`;
+ msg += ` [${yellow(`${result.processingTime.toFixed(2)}ms`)}]`;
console.log(msg);
}
diff --git a/package-lock.json b/package-lock.json
index 9bfa05ac6e..eae6924893 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "zotero",
- "version": "5.0.0",
+ "version": "7.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "zotero",
- "version": "5.0.0",
+ "version": "7.0.0",
"dependencies": {
"@citeproc-rs/wasm": "^0.2.0",
"ace-builds": "^1.4.12",
@@ -40,6 +40,7 @@
"eslint-plugin-react": "^7.28.0",
"eslint-plugin-react-hooks": "^4.0.4",
"fs-extra": "^3.0.1",
+ "ftl-tx": "^0.6.0",
"globby": "^6.1.0",
"jspath": "^0.4.0",
"mocha": "^3.5.3",
@@ -919,6 +920,16 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
+ "node_modules/@fluent/syntax": {
+ "version": "0.19.0",
+ "resolved": "https://registry.npmjs.org/@fluent/syntax/-/syntax-0.19.0.tgz",
+ "integrity": "sha512-5D2qVpZrgpjtqU4eNOcWGp1gnUCgjfM+vKGE2y03kKN6z5EBhtx0qdRFbg8QuNNj8wXNoX93KJoYb+NqoxswmQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=14.0.0",
+ "npm": ">=7.0.0"
+ }
+ },
"node_modules/@formatjs/intl-displaynames": {
"version": "1.2.10",
"resolved": "https://registry.npmjs.org/@formatjs/intl-displaynames/-/intl-displaynames-1.2.10.tgz",
@@ -3561,6 +3572,15 @@
"node": ">= 4.0"
}
},
+ "node_modules/ftl-tx": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/ftl-tx/-/ftl-tx-0.6.0.tgz",
+ "integrity": "sha512-w7s0p6RNsaUKpiuQZ5Q0KIyiX2JIDItqI4qRKt49u0aMqdGp0gDc5G5Qnubgu6l2ww5PDt4wsyL7A1iZo1VGMQ==",
+ "dev": true,
+ "dependencies": {
+ "@fluent/syntax": "^0.19.0"
+ }
+ },
"node_modules/function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
@@ -7533,6 +7553,12 @@
}
}
},
+ "@fluent/syntax": {
+ "version": "0.19.0",
+ "resolved": "https://registry.npmjs.org/@fluent/syntax/-/syntax-0.19.0.tgz",
+ "integrity": "sha512-5D2qVpZrgpjtqU4eNOcWGp1gnUCgjfM+vKGE2y03kKN6z5EBhtx0qdRFbg8QuNNj8wXNoX93KJoYb+NqoxswmQ==",
+ "dev": true
+ },
"@formatjs/intl-displaynames": {
"version": "1.2.10",
"resolved": "https://registry.npmjs.org/@formatjs/intl-displaynames/-/intl-displaynames-1.2.10.tgz",
@@ -9769,6 +9795,15 @@
"nan": "^2.12.1"
}
},
+ "ftl-tx": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/ftl-tx/-/ftl-tx-0.6.0.tgz",
+ "integrity": "sha512-w7s0p6RNsaUKpiuQZ5Q0KIyiX2JIDItqI4qRKt49u0aMqdGp0gDc5G5Qnubgu6l2ww5PDt4wsyL7A1iZo1VGMQ==",
+ "dev": true,
+ "requires": {
+ "@fluent/syntax": "^0.19.0"
+ }
+ },
"function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
diff --git a/package.json b/package.json
index 7442586c06..672ed97c1c 100644
--- a/package.json
+++ b/package.json
@@ -10,6 +10,8 @@
"sass": "node ./js-build/sass.js",
"js": "node ./js-build/js.js",
"clean": "node ./js-build/clean.js",
+ "ftl-to-json": "node ./js-build/ftl-to-json.mjs",
+ "localize-ftl": "node ./js-build/localize-ftl.mjs",
"clean-build": "node ./js-build/clean.js && node ./js-build/build.js",
"clean-start": "node ./js-build/clean.js && node ./js-build/build.js && node ./js-build/watch.js"
},
@@ -47,6 +49,7 @@
"eslint-plugin-react": "^7.28.0",
"eslint-plugin-react-hooks": "^4.0.4",
"fs-extra": "^3.0.1",
+ "ftl-tx": "^0.6.0",
"globby": "^6.1.0",
"jspath": "^0.4.0",
"mocha": "^3.5.3",