From 7549177cc5c6381f23c125504347eb6a445338f7 Mon Sep 17 00:00:00 2001
From: t11r <1674104+t11r@users.noreply.github.com>
Date: Sun, 1 Sep 2024 22:54:24 +0200
Subject: [PATCH 1/4] Fix setting language via API
When using `setLanguage`, the new language was only applied after the next render update due to missing reactivity.
---
src/plugins/i18n.js | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/src/plugins/i18n.js b/src/plugins/i18n.js
index ea36c5b1..92aea1c1 100644
--- a/src/plugins/i18n.js
+++ b/src/plugins/i18n.js
@@ -1,11 +1,13 @@
+import { ref } from 'vue';
+
export default {
install: (app) => {
- let translation = null;
+ const translation = ref(null);
// eslint-disable-next-line no-param-reassign
app.config.globalProperties.$translate = (string, fallback) => {
- if (translation && translation[string]) {
- return translation[string];
+ if (translation.value?.[string]) {
+ return translation.value[string];
}
if (import.meta.env.DEV && translation) {
@@ -21,7 +23,7 @@ export default {
// value is the translated string, e.g. { key: 'Schlüssel' }
// eslint-disable-next-line no-param-reassign
app.config.globalProperties.$translate.setTranslation = (translationObject) => {
- translation = translationObject;
+ translation.value = translationObject;
};
},
};
From 59b2ad988f48b4e85a52d563f64851f75228321e Mon Sep 17 00:00:00 2001
From: t11r <1674104+t11r@users.noreply.github.com>
Date: Wed, 1 Jan 2025 16:32:48 +0100
Subject: [PATCH 2/4] Add scripts for creating and testing translations
---
README.md | 8 ++
build/create-translation.js | 55 +++++++++
build/i18n.js | 223 ++++++++++++++++++++++++++++++++++++
build/test-translations.js | 62 ++++++++++
package-lock.json | 213 +++++++++++++++++++++++++++-------
package.json | 6 +-
6 files changed, 527 insertions(+), 40 deletions(-)
create mode 100644 build/create-translation.js
create mode 100644 build/i18n.js
create mode 100644 build/test-translations.js
diff --git a/README.md b/README.md
index 8aad3e33..908ad5e8 100644
--- a/README.md
+++ b/README.md
@@ -291,6 +291,14 @@ Run end-to-end tests:
- Development build: `npm run dev`
- Production build: `npm run build && npm run test:e2e`
+## Translations
+
+Translations reside in `public/translations`. Each language is represented by a JSON file, where the file name is the language’s [ISO 639 alpha-2 code](https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes). Each file consists of a single object of key-value pairs; the key is the English string, the value is the translation. The first string of each file with key `$language` contains its native name. There are a few other special keys starting with `$`; while all other keys are to be translated literally, these keys serve as placeholders for longer sections of text. Search the source files for these keys to reveal their corresponding English texts.
+
+To create a new empty translation, run `node build/create-translation.js` and follow the prompts.
+
+To check all translations for validity and completeness, use `npm run test:i18n` or `npm run test:i18n:fix`, the latter adding missing keys, removing unused keys, and sorting keys.
+
---
diff --git a/build/create-translation.js b/build/create-translation.js
new file mode 100644
index 00000000..2fe43613
--- /dev/null
+++ b/build/create-translation.js
@@ -0,0 +1,55 @@
+import fs from 'fs';
+import readline from 'readline';
+
+import chalk from 'chalk';
+
+import {
+ findTranslatedStrings,
+ indention,
+ rootDir,
+} from './i18n.js';
+
+const rl = readline.createInterface({
+ input: process.stdin,
+ output: process.stdout,
+});
+
+let langCode = await new Promise((resolve) => {
+ rl.question('Two-letter language code (ISO 639-1): ', resolve);
+});
+
+langCode = langCode.toLowerCase();
+
+if (!langCode.match(/^[a-z]{2}$/)) {
+ console.warn(chalk.redBright('Invalid language code'));
+ process.exit(1);
+}
+
+const fileName = `${rootDir}public/translations/${langCode}.json`;
+
+if (fs.existsSync(fileName)) {
+ console.warn(chalk.redBright(`${chalk.bold(fileName)} already exists`));
+ process.exit(1);
+}
+
+let language = await new Promise((resolve) => {
+ rl.question('Native language name: ', resolve);
+});
+
+rl.close();
+
+language = language.charAt(0).toUpperCase() + language.slice(1).toLowerCase();
+
+const translationObject = {
+ $language: language,
+};
+
+findTranslatedStrings(`${rootDir}/src`, '\\$translate').forEach((item) => {
+ translationObject[item.key] = '';
+});
+
+const json = JSON.stringify(translationObject, null, indention);
+
+fs.writeFileSync(fileName, `${json}\n`);
+
+console.log(`\nCreated ${chalk.dim('file://')}${chalk.bold(fileName)}`);
diff --git a/build/i18n.js b/build/i18n.js
new file mode 100644
index 00000000..e746453c
--- /dev/null
+++ b/build/i18n.js
@@ -0,0 +1,223 @@
+import fs from 'fs';
+import path from 'path';
+import url from 'url';
+
+import chalk from 'chalk';
+import * as Diff from 'diff';
+
+export const indention = '\t';
+
+export const rootDir = url.fileURLToPath(new URL('..', import.meta.url));
+
+function sortTranslation(translation) {
+ return Object.keys(translation)
+ .sort((a, b) => {
+ if (a === '$language') {
+ return -1;
+ }
+
+ if (b === '$language') {
+ return 1;
+ }
+
+ return a.toLowerCase().localeCompare(b.toLowerCase());
+ })
+ .reduce((acc, key) => {
+ acc[key] = translation[key];
+ return acc;
+ }, {});
+}
+
+export function checkTranslationFiles(
+ translationsDir,
+ referenceKeys,
+ options = { addMissing: false, removeUnused: false, sort: false },
+) {
+ const translationFiles = fs.readdirSync(translationsDir);
+
+ const results = [];
+ translationFiles.forEach((file) => {
+ const lang = path.parse(file).name;
+
+ const result = {
+ lang,
+ notes: [],
+ issues: [],
+ };
+
+ const json = fs.readFileSync(`${translationsDir}/${file}`);
+ const translation = JSON.parse(json);
+ const keys = Object.keys(translation);
+
+ const emptyKeys = keys.filter((key) => !translation[key]);
+ result.issues.push(...emptyKeys.map((key) => ({ type: 'empty', key })));
+
+ const missingKeys = referenceKeys.filter((key) => !keys.includes(key));
+ result.issues.push(...missingKeys.map((key) => ({ type: 'missing', key })));
+
+ const unusedKeys = keys.filter((key) => !referenceKeys.includes(key));
+ result.issues.push(...unusedKeys.map((key) => ({ type: 'unused', key })));
+
+ const keysStr = keys.join(', ');
+ const referenceKeysStr = referenceKeys.join(', ');
+ if (!missingKeys.length
+ && !unusedKeys.length
+ && keysStr !== referenceKeysStr
+ ) {
+ const diffs = Diff.diffChars(keysStr, referenceKeysStr);
+
+ const difference = diffs.map((part) => {
+ if (part.added) {
+ return chalk.green(part.value);
+ }
+
+ if (part.removed) {
+ return chalk.red(part.value);
+ }
+
+ return part.value;
+ }).join('');
+
+ result.notes.push(`Keys are not properly sorted: ${chalk.reset(difference)}`);
+ }
+
+ results.push(result);
+
+ if (options.addMissing && missingKeys.length) {
+ missingKeys.forEach((key) => {
+ translation[key] = '';
+ });
+ result.notes.push('Added missing keys, please add translations');
+ }
+
+ if (options.removeUnused && unusedKeys.length) {
+ unusedKeys.forEach((key) => {
+ delete translation[key];
+ });
+ result.notes.push('Removed unused keys');
+ }
+
+ const updatedTranslation = options.sort
+ ? sortTranslation(translation)
+ : translation;
+
+ if (options.addMissing || options.removeUnused || options.sort) {
+ const updatedJson = JSON.stringify(updatedTranslation, null, indention);
+ fs.writeFileSync(`${translationsDir}/${file}`, `${updatedJson}\n`);
+ }
+ });
+
+ return results;
+}
+
+/**
+ * Find all proper occurrences of $translate()
+ *
+ * $translate() should always fit into a single line and only have single-quoted
+ * strings as parameters. The function params may contain ternary operators, but
+ * translated strings must never be concatenated or otherwise "built" for the
+ * automated translation tests to work.
+ *
+ * Good examples:
+ *
+ * $translate('String');
+ * $translate('String', 'Fallback string');
+ * $translate(first ? 'String' : 'Other string');
+ *
+ * Bad examples (i.e. would not be found):
+ *
+ * $translate('String' + ' ' + 'another string');
+ * $translate(
+ * 'String',
+ * );
+ * $translate(1);
+ * $translate(
+ * '1',
+ * `2`,
+ * );
+ * $translate('',
+ * '2',
+ * );
+ */
+export function findTranslatedStrings(dir, functionName, fileName = '\\.(js|vue)$') {
+ const files = fs.readdirSync(dir, { withFileTypes: true });
+ const results = [];
+
+ const translationFunctionRegexp = new RegExp(`${functionName}\\s*\\(\\s*(.*)\\s*\\)?`, 'gs');
+ const fileNameRegexp = new RegExp(fileName);
+
+ files.forEach((file) => {
+ const filePath = path.join(dir, file.name);
+
+ if (file.isDirectory()) {
+ const dirResults = findTranslatedStrings(filePath, functionName);
+ results.push(...dirResults);
+ return;
+ }
+
+ if (!file.name.match(fileNameRegexp)) {
+ return;
+ }
+
+ let lineNumber = 1;
+
+ const fileContents = fs.readFileSync(filePath, 'utf8');
+
+ fileContents.split('\n').forEach((line) => {
+ const matches = [...line.matchAll(translationFunctionRegexp)];
+
+ if (!matches) {
+ return;
+ }
+
+ matches.forEach((match) => {
+ const strings = match[1]
+ .match(/'(.*?)'|"(.*?)"|`(.*?)`/g)
+ ?.map((string) => string.slice(1, -1)); // remove quotes
+
+ if (!strings) {
+ console.warn(chalk.redBright(
+ 'Found translation function, but could not determine parameters'
+ + ` in ${chalk.dim('file://')}${chalk.bold(filePath)}, line ${lineNumber}: ${chalk.bold(match[0])}\n`,
+ ));
+
+ return;
+ }
+
+ if (strings.some(((string) => !string))) {
+ console.warn(chalk.redBright(
+ 'Translation function uses an empty value'
+ + ` in ${chalk.dim('file://')}${chalk.bold(filePath)}, line ${lineNumber}: ${chalk.bold(match[0])}\n`,
+ ));
+
+ return;
+ }
+
+ const matchObject = {
+ file: filePath,
+ line: lineNumber,
+ match: match[0],
+ };
+
+ strings.forEach((string) => {
+ const index = results.findIndex((result) => result.key === string);
+
+ if (index >= 0) {
+ results[index].matches.push(matchObject);
+ } else {
+ results.push({
+ key: string,
+ matches: [matchObject],
+ });
+ }
+ });
+ });
+
+ lineNumber += 1;
+ });
+ });
+
+ results.sort((a, b) => a.key.localeCompare(b.key));
+
+ return results;
+}
diff --git a/build/test-translations.js b/build/test-translations.js
new file mode 100644
index 00000000..51ba9f81
--- /dev/null
+++ b/build/test-translations.js
@@ -0,0 +1,62 @@
+import chalk from 'chalk';
+
+import {
+ checkTranslationFiles,
+ findTranslatedStrings,
+ rootDir,
+} from './i18n.js';
+
+const translatedStrings = findTranslatedStrings(`${rootDir}/src`, '\\$translate')
+ .map((result) => result.key);
+
+if (!translatedStrings.length) {
+ console.log('No translated strings found');
+ process.exit(1);
+}
+
+// TODO: Alert on missing language name
+translatedStrings.unshift('$language');
+
+const options = {
+ addMissing: process.argv.includes('--add'),
+ removeUnused: process.argv.includes('--remove'),
+ sort: process.argv.includes('--sort'),
+};
+
+const translationsDir = `${rootDir}public/translations`;
+const results = checkTranslationFiles(translationsDir, translatedStrings, options);
+
+let translationsWithIssuesCount = 0;
+
+results.forEach((result) => {
+ console.log(`${chalk.dim('file://')}${translationsDir}/${chalk.bold(result.lang)}.json`);
+
+ ['empty', 'missing', 'unused'].forEach((type) => {
+ const issues = result.issues.filter((issue) => issue.type === type);
+ const label = `${type.charAt(0).toUpperCase() + type.slice(1)} keys`;
+ if (issues.length) {
+ console.log(` ${chalk.redBright(label)}`);
+ console.log(` ${chalk.red(issues.map((issue) => issue.key).join('\n '))}`);
+ }
+ });
+
+ result.notes.forEach((note) => {
+ console.log(` ${chalk.cyanBright(note)}`);
+ });
+
+ if (result.notes.length || result.issues.length) {
+ translationsWithIssuesCount += 1;
+ } else {
+ console.log(chalk.greenBright(' Shiny!'));
+ }
+
+ console.log();
+});
+
+console.log(`Checked ${results.length} languages, ${
+ translationsWithIssuesCount
+ ? chalk.redBright(`found issues with ${translationsWithIssuesCount}.`)
+ : chalk.greenBright('found no issues.')
+}`);
+
+process.exit(translationsWithIssuesCount ? 1 : 0);
diff --git a/package-lock.json b/package-lock.json
index 491fa49c..146204f9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,8 +16,10 @@
"@vitejs/plugin-vue": "^5.0.5",
"@vue/eslint-config-airbnb": "^8.0.0",
"@vue/test-utils": "^2.4.6",
+ "chalk": "^5.3.0",
"click-outside-vue3": "^4.0.1",
"cypress": "^13.11.0",
+ "diff": "^5.2.0",
"eslint": "^8.57.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-cypress": "^2.15.2",
@@ -1960,33 +1962,17 @@
}
},
"node_modules/chalk": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
- "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
+ "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
"dev": true,
- "dependencies": {
- "ansi-styles": "^4.1.0",
- "supports-color": "^7.1.0"
- },
"engines": {
- "node": ">=10"
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
- "node_modules/chalk/node_modules/supports-color": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
- "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
- "dev": true,
- "dependencies": {
- "has-flag": "^4.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/check-error": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz",
@@ -2345,6 +2331,34 @@
"node": "^16.0.0 || ^18.0.0 || >=20.0.0"
}
},
+ "node_modules/cypress/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/cypress/node_modules/chalk/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/damerau-levenshtein": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
@@ -2505,6 +2519,15 @@
"node": ">=8"
}
},
+ "node_modules/diff": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz",
+ "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
"node_modules/diff-sequences": {
"version": "29.6.3",
"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz",
@@ -3227,6 +3250,34 @@
"url": "https://opencollective.com/eslint"
}
},
+ "node_modules/eslint/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/eslint/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/espree": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
@@ -5055,6 +5106,34 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/log-symbols/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/log-symbols/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/log-update": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz",
@@ -9362,25 +9441,10 @@
}
},
"chalk": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
- "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
- "dev": true,
- "requires": {
- "ansi-styles": "^4.1.0",
- "supports-color": "^7.1.0"
- },
- "dependencies": {
- "supports-color": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
- "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
- "dev": true,
- "requires": {
- "has-flag": "^4.0.0"
- }
- }
- }
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
+ "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
+ "dev": true
},
"check-error": {
"version": "1.0.3",
@@ -9659,6 +9723,29 @@
"tmp": "~0.2.1",
"untildify": "^4.0.0",
"yauzl": "^2.10.0"
+ },
+ "dependencies": {
+ "chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "dependencies": {
+ "supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^4.0.0"
+ }
+ }
+ }
+ }
}
},
"damerau-levenshtein": {
@@ -9783,6 +9870,12 @@
"optional": true,
"peer": true
},
+ "diff": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz",
+ "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==",
+ "dev": true
+ },
"diff-sequences": {
"version": "29.6.3",
"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz",
@@ -10073,6 +10166,27 @@
"optionator": "^0.9.3",
"strip-ansi": "^6.0.1",
"text-table": "^0.2.0"
+ },
+ "dependencies": {
+ "chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ }
+ },
+ "supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^4.0.0"
+ }
+ }
}
},
"eslint-config-airbnb-base": {
@@ -11703,6 +11817,27 @@
"requires": {
"chalk": "^4.1.0",
"is-unicode-supported": "^0.1.0"
+ },
+ "dependencies": {
+ "chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ }
+ },
+ "supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^4.0.0"
+ }
+ }
}
},
"log-update": {
diff --git a/package.json b/package.json
index 8b98fedf..0705fccb 100644
--- a/package.json
+++ b/package.json
@@ -26,9 +26,11 @@
"postinstall": "node build/create-icons.js",
"preversion": "npm install-clean && npm run test:unit && npm run build && npm run test:e2e",
"preview": "vite preview",
- "test:unit": "vitest run",
"test:e2e": "start-server-and-test preview http://localhost:4173 'cypress run --e2e'",
"test:e2e:dev": "start-server-and-test 'vite dev --port 4173' http://localhost:4173 'cypress open --e2e'",
+ "test:i18n": "node build/test-translations.js",
+ "test:i18n:fix": "node build/test-translations.js --add --remove --sort",
+ "test:unit": "vitest run",
"version": "git add dist"
},
"devDependencies": {
@@ -38,8 +40,10 @@
"@vitejs/plugin-vue": "^5.0.5",
"@vue/eslint-config-airbnb": "^8.0.0",
"@vue/test-utils": "^2.4.6",
+ "chalk": "^5.3.0",
"click-outside-vue3": "^4.0.1",
"cypress": "^13.11.0",
+ "diff": "^5.2.0",
"eslint": "^8.57.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-cypress": "^2.15.2",
From 59bd5b27b198ebdf83d389d7a4f9a60cfae49285 Mon Sep 17 00:00:00 2001
From: t11r <1674104+t11r@users.noreply.github.com>
Date: Mon, 6 Jan 2025 16:56:08 +0100
Subject: [PATCH 3/4] Update copyright year
---
public/translations/de.json | 2 +-
public/translations/eo.json | 2 +-
public/translations/hr.json | 2 +-
src/components/ViewHelp.vue | 2 +-
4 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/public/translations/de.json b/public/translations/de.json
index de3ae122..adb808ed 100644
--- a/public/translations/de.json
+++ b/public/translations/de.json
@@ -1,6 +1,6 @@
{
"$language": "Deutsch",
- "$copyright": "Copyright © 2017–2022 Universität Göttingen / Staats- und Universitätsbibliothek Göttingen",
+ "$copyright": "Copyright © 2017–2025 Universität Göttingen / Staats- und Universitätsbibliothek Göttingen",
"$info": "TIFY ist ein schlanker und für Mobilgeräte optimierter IIIF-Dokumentenbetrachter, veröffentlicht unter der GNU Affero General Public License 3.0.",
"About TIFY": "Über TIFY",
"Brightness": "Helligkeit",
diff --git a/public/translations/eo.json b/public/translations/eo.json
index 65ca0682..191de738 100644
--- a/public/translations/eo.json
+++ b/public/translations/eo.json
@@ -1,6 +1,6 @@
{
"$language": "Esperanto",
- "$copyright": "Kopirajtoj © 2017–2022 Universitato Goettingen / Ŝtata kaj Universitata Biblioteko Goettingen",
+ "$copyright": "Kopirajtoj © 2017–2025 Universitato Goettingen / Ŝtata kaj Universitata Biblioteko Goettingen",
"$info": "TIFY estas pli svelta kaj pli movebla amika IIIF-dokumentrigardilo publikigita sub la Ĝenerala Publika Permesilo 3.0 de GNU Affero.",
"About TIFY": "Per TIFY",
"Brightness": "Helecon",
diff --git a/public/translations/hr.json b/public/translations/hr.json
index 8fb45ea8..abf5efc4 100644
--- a/public/translations/hr.json
+++ b/public/translations/hr.json
@@ -1,6 +1,6 @@
{
"$language": "Hrvatski",
- "$copyright": "Autorska prava © 2017–2022 Universität Göttingen / Staats- und Universitätsbibliothek Göttingen",
+ "$copyright": "Autorska prava © 2017–2025 Universität Göttingen / Staats- und Universitätsbibliothek Göttingen",
"$info": "TIFY je mali i optimiziran za mobilne uređaje preglednik IIIF dokumenata, otorenog koda prema GNU Affero General Public License 3.0.",
"About TIFY": "O TIFY-ju",
"Brightness": "Svjetlina",
diff --git a/src/components/ViewHelp.vue b/src/components/ViewHelp.vue
index 7f657a31..15dc3dc7 100644
--- a/src/components/ViewHelp.vue
+++ b/src/components/ViewHelp.vue
@@ -3,7 +3,7 @@ export default {
computed: {
// NOTE: If $t is returned directly, this text won’t update when the language is changed via API
copyrightHtml() {
- return 'Copyright © 2017–2022'
+ return 'Copyright © 2017–2025'
+ ' Göttingen University'
+ ' / '
+ 'Göttingen State and University Library';
From 9e5cbcc6832f9a3ab9fbac81900cce67f8d9aa9b Mon Sep 17 00:00:00 2001
From: t11r <1674104+t11r@users.noreply.github.com>
Date: Mon, 6 Jan 2025 16:56:30 +0100
Subject: [PATCH 4/4] Clean up and update all translations
Remove unused keys, add empty strings for missing keys, and fix key order. Complete German translation.
---
public/translations/de.json | 2 +-
public/translations/eo.json | 13 ++++++++++---
public/translations/fr.json | 10 +++++++---
public/translations/hr.json | 8 +++++---
public/translations/it.json | 22 +++++++++++++++++++---
public/translations/pl.json | 22 +++++++++++++++++++---
6 files changed, 61 insertions(+), 16 deletions(-)
diff --git a/public/translations/de.json b/public/translations/de.json
index adb808ed..3e9f5794 100644
--- a/public/translations/de.json
+++ b/public/translations/de.json
@@ -11,7 +11,7 @@
"Contents": "Inhalt",
"Contrast": "Kontrast",
"Contributors": "Beitragende",
- "Could not load child manifest": "Konnte Kind-Manifest nicht laden",
+ "Could not load child manifest": "Kind-Manifest konnte nicht geladen werden",
"Current Element": "Aktuelles Element",
"Current page:": "Aktuelle Seite:",
"Description": "Beschreibung",
diff --git a/public/translations/eo.json b/public/translations/eo.json
index 191de738..849e9b51 100644
--- a/public/translations/eo.json
+++ b/public/translations/eo.json
@@ -5,28 +5,33 @@
"About TIFY": "Per TIFY",
"Brightness": "Helecon",
"Close PDF list": "Fermu PDF-liston",
- "Collapse all": "Kolapu ĉion",
"Collapse": "Kolapso",
+ "Collapse all": "Kolapu ĉion",
"Collection": "Kolekto",
"Contents": "Enhavojn",
"Contrast": "Kontrasto",
"Contributors": "Kontribuanto",
+ "Could not load child manifest": "",
"Current Element": "Nuna ero",
"Current page:": "Nuna paĝo:",
"Description": "Priskribo",
+ "Dismiss": "",
"Document": "Dokumento",
"Download Individual Images": "Elŝutu unuopajn bildojn",
"Exit fullscreen": "Eliru plenekranan reĝimon",
- "Expand all": "Plivastigu ĉion",
"Expand": "Disfaldas",
+ "Expand all": "Plivastigu ĉion",
"Export": "Eksporti",
+ "Filter collection": "",
"Filter pages": "Filtrilaj paĝoj",
"First page": "Unua paĝo",
"Fullscreen": "Plena ekrana reĝimo",
- "Fulltext not available for this page": "Plena teksto ne havebla",
"Fulltext": "Plena teksto",
+ "Fulltext not available for this page": "Plena teksto ne havebla",
"Help": "Helpu",
"IIIF manifest": "IIIF-Manifesto",
+ "IIIF manifest (collection)": "",
+ "IIIF manifest (current document)": "",
"Image filters": "Bilda filtrilo",
"Info": "Info",
"Last page": "Lasta paĝo",
@@ -36,6 +41,7 @@
"Metadata": "Metadatenoj",
"Next page": "Sekva paĝo",
"Next section": "Sekva sekcio",
+ "No results": "",
"Other Formats": "Aliaj formatoj",
"page": "paĝo",
"Page": "Paĝo",
@@ -56,6 +62,7 @@
"Title": "Titolo",
"Toggle double-page": "Ŝaltu duoblan paĝon",
"Toggle image filters": "Ŝaltu bildfiltrilojn",
+ "Toggle page select": "",
"User guide": "Uzant-gvidilo",
"Version": "Versio",
"View": "Vido",
diff --git a/public/translations/fr.json b/public/translations/fr.json
index 6d27a02e..c47b6a5f 100644
--- a/public/translations/fr.json
+++ b/public/translations/fr.json
@@ -1,14 +1,17 @@
{
"$language": "Français",
+ "$copyright": "",
+ "$info": "",
"About TIFY": "À propos de TIFY",
"Brightness": "Luminosité",
"Close PDF list": "Fermer la liste de PDFs",
- "Collapse all": "Tout replier",
"Collapse": "Replier",
+ "Collapse all": "Tout replier",
"Collection": "Collection",
"Contents": "Contenu",
"Contrast": "Contraste",
"Contributors": "Contributeurs",
+ "Could not load child manifest": "",
"Current Element": "Élement actuel",
"Current page:": "Page actuelle :",
"Description": "Description",
@@ -16,15 +19,15 @@
"Document": "Document",
"Download Individual Images": "Télécharger les images individuellement",
"Exit fullscreen": "Quitter le mode plein écran",
- "Expand all": "Tout déplier",
"Expand": "Déplier",
+ "Expand all": "Tout déplier",
"Export": "Exporter",
"Filter collection": "Filtrer la collection",
"Filter pages": "Filtrer les pages",
"First page": "Première page",
"Fullscreen": "Plein écran",
- "Fulltext not available for this page": "Texte intégral non disponible pour cette page",
"Fulltext": "Texte intégral",
+ "Fulltext not available for this page": "Texte intégral non disponible pour cette page",
"Help": "Aide",
"IIIF manifest": "Manifeste IIIF",
"IIIF manifest (collection)": "Manifeste IIIF (collection)",
@@ -59,6 +62,7 @@
"Title": "Titre",
"Toggle double-page": "Basculer en mode double page",
"Toggle image filters": "Appliquer les filtres visuels",
+ "Toggle page select": "",
"User guide": "Guide d’utilisation",
"Version": "Version",
"View": "Vue",
diff --git a/public/translations/hr.json b/public/translations/hr.json
index abf5efc4..ad219c6f 100644
--- a/public/translations/hr.json
+++ b/public/translations/hr.json
@@ -5,12 +5,13 @@
"About TIFY": "O TIFY-ju",
"Brightness": "Svjetlina",
"Close PDF list": "Zatvori popis PDF-a",
- "Collapse all": "Smanji sve",
"Collapse": "Smanji",
+ "Collapse all": "Smanji sve",
"Collection": "Zbirka",
"Contents": "Sadržaj",
"Contrast": "Kontrast",
"Contributors": "Suradnici",
+ "Could not load child manifest": "",
"Current Element": "Trenutni element",
"Current page:": "Trenutna stranica:",
"Description": "Opis",
@@ -18,15 +19,15 @@
"Document": "Dokument",
"Download Individual Images": "Skini pojedine slike",
"Exit fullscreen": "Isključi preko cijelog ekrana",
- "Expand all": "Proširi sve",
"Expand": "Proširi",
+ "Expand all": "Proširi sve",
"Export": "Izvoz",
"Filter collection": "Filtriraj zbirku",
"Filter pages": "Filtriraj stranice",
"First page": "Prva stranica",
"Fullscreen": "Preko cijelog ekrana",
- "Fulltext not available for this page": "Cjeloviti tekst nije dostupan za ovu stranicu",
"Fulltext": "Cjeloviti tekst",
+ "Fulltext not available for this page": "Cjeloviti tekst nije dostupan za ovu stranicu",
"Help": "Pomoć",
"IIIF manifest": "IIIF-Manifest",
"IIIF manifest (collection)": "IIIF-Manifest (zbirka)",
@@ -61,6 +62,7 @@
"Title": "Naslov",
"Toggle double-page": "Dvije stranice na stranici",
"Toggle image filters": "Filtri slika",
+ "Toggle page select": "",
"User guide": "Korisnički vodič",
"Version": "Verzija",
"View": "Pogled",
diff --git a/public/translations/it.json b/public/translations/it.json
index 77544540..17864f86 100644
--- a/public/translations/it.json
+++ b/public/translations/it.json
@@ -1,35 +1,47 @@
{
"$language": "Italiano",
+ "$copyright": "",
+ "$info": "",
"About TIFY": "Informazioni su TIFY",
"Brightness": "Luminosità",
"Close PDF list": "Chiudi l’elenco dei PDF",
- "Collapse all": "Comprimi tutto",
"Collapse": "Comprimi",
+ "Collapse all": "Comprimi tutto",
"Collection": "Collezione",
"Contents": "Contenuto",
"Contrast": "Contrasto",
+ "Contributors": "",
+ "Could not load child manifest": "",
"Current Element": "Elemento corrente",
"Current page:": "Pagina corrente:",
"Description": "Descrizione",
+ "Dismiss": "",
"Document": "Documento",
"Download Individual Images": "Scarica le singole immagini",
"Exit fullscreen": "Esci dalla modalità a schermo intero",
- "Expand all": "Espandi tutto",
"Expand": "Espandi",
+ "Expand all": "Espandi tutto",
"Export": "Esporta",
+ "Filter collection": "",
+ "Filter pages": "",
"First page": "Prima pagina",
"Fullscreen": "Schermo intero",
- "Fulltext not available for this page": "Testo integrale non disponibile per questa pagina",
"Fulltext": "Testo integrale",
+ "Fulltext not available for this page": "Testo integrale non disponibile per questa pagina",
"Help": "Aiuto",
"IIIF manifest": "Manifest IIIF",
+ "IIIF manifest (collection)": "",
+ "IIIF manifest (current document)": "",
+ "Image filters": "",
"Info": "Informazioni",
"Last page": "Ultima pagina",
"License": "Licenza",
"Loading": "Caricamento",
+ "Logo": "",
"Metadata": "Metadati",
"Next page": "Pagina successiva",
"Next section": "Sezione successiva",
+ "No results": "",
"Other Formats": "Altri formati",
"page": "pagina",
"Page": "Pagina",
@@ -40,6 +52,7 @@
"Previous section": "Sezione precedente",
"Related Resources": "Risorse correlate",
"Renderings": "Rendering",
+ "Report a bug": "",
"Reset": "Ripristina",
"Rotate": "Ruota",
"Saturation": "Saturazione",
@@ -49,6 +62,9 @@
"Title": "Titolo",
"Toggle double-page": "Attiva/disattiva doppia pagina",
"Toggle image filters": "Attiva/disattiva filtri immagine",
+ "Toggle page select": "",
+ "User guide": "",
+ "Version": "",
"View": "Vista",
"Zoom in": "Zoom in",
"Zoom out": "Zoom out"
diff --git a/public/translations/pl.json b/public/translations/pl.json
index 5d3b117b..40a14525 100644
--- a/public/translations/pl.json
+++ b/public/translations/pl.json
@@ -1,35 +1,47 @@
{
"$language": "Polski",
+ "$copyright": "",
+ "$info": "",
"About TIFY": "Więcej o TIFY",
"Brightness": "Jasność",
"Close PDF list": "Zamknij liste plików PDF",
- "Collapse all": "Zwiń wszystko",
"Collapse": "Zwiń",
+ "Collapse all": "Zwiń wszystko",
"Collection": "Kolekcja",
"Contents": "Zawartość",
"Contrast": "Kontrast",
+ "Contributors": "",
+ "Could not load child manifest": "",
"Current Element": "Obecny element",
"Current page:": "Obecna strona:",
"Description": "Opis",
+ "Dismiss": "",
"Document": "Dokument",
"Download Individual Images": "Pobierz obrazy indywidualnie",
"Exit fullscreen": "Wyjdź z trybu pełnoekranowego",
- "Expand all": "Rozwiń wszystko",
"Expand": "Rozwiń",
+ "Expand all": "Rozwiń wszystko",
"Export": "Eksportuj",
+ "Filter collection": "",
+ "Filter pages": "",
"First page": "Pierwsza Strona",
"Fullscreen": "Widok pełnoekranowy",
- "Fulltext not available for this page": "Pełen tekst niedostępny dla tej strony",
"Fulltext": "Pełen tekst",
+ "Fulltext not available for this page": "Pełen tekst niedostępny dla tej strony",
"Help": "Pomoc",
"IIIF manifest": "Manifest IIIF",
+ "IIIF manifest (collection)": "",
+ "IIIF manifest (current document)": "",
+ "Image filters": "",
"Info": "Informacje",
"Last page": "Ostatnia strona",
"License": "Licencja",
"Loading": "Ładowanie",
+ "Logo": "",
"Metadata": "Metadane",
"Next page": "Następna strona",
"Next section": "Następna sekcja",
+ "No results": "",
"Other Formats": "Inne formaty",
"page": "strona",
"Page": "Strona",
@@ -40,6 +52,7 @@
"Previous section": "Poprzednia sekcja",
"Related Resources": "Powiązane zasoby",
"Renderings": "Rendery",
+ "Report a bug": "",
"Reset": "Reset",
"Rotate": "Obróć",
"Saturation": "Saturacja",
@@ -49,6 +62,9 @@
"Title": "Tytuł",
"Toggle double-page": "Przejdź do widoku dwóch stron",
"Toggle image filters": "Włącz filtry obrazów",
+ "Toggle page select": "",
+ "User guide": "",
+ "Version": "",
"View": "Widok",
"Zoom in": "Przybliżenie",
"Zoom out": "Oddalenie"