diff --git a/README.md b/README.md index e134da7e..5c5633ed 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,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 original English string, the value is the translation. The first key `$language` denotes the native name of the translation’s language. 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 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..ed91d678 --- /dev/null +++ b/build/create-translation.js @@ -0,0 +1,51 @@ +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, +}); + +const langCode = (await new Promise((resolve) => { + rl.question('Two-letter language code (ISO 639-1): ', resolve); +})).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); +} + +const language = await new Promise((resolve) => { + rl.question('Native language name: ', resolve); +}); + +rl.close(); + +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..479f13fe --- /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 a270e1a1..6df262c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,8 +17,10 @@ "@vitest/eslint-plugin": "^1.1.28", "@vue/eslint-config-airbnb": "^8.0.0", "@vue/test-utils": "^2.4.6", + "chalk": "^5.4.1", "click-outside-vue3": "^4.0.1", "cypress": "^14.0.3", + "diff": "^7.0.0", "dotenv": "^16.4.7", "eslint": "^8.57.1", "eslint-import-resolver-typescript": "^3.7.0", @@ -3378,33 +3380,18 @@ } }, "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.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, + "license": "MIT", "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": "2.1.1", "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", @@ -3770,6 +3757,36 @@ "node": "^18.0.0 || ^20.0.0 || >=22.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, + "license": "MIT", + "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, + "license": "MIT", + "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", @@ -3990,6 +4007,16 @@ "node": ">=0.4.0" } }, + "node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -4810,6 +4837,23 @@ "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, + "license": "MIT", + "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/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -4823,6 +4867,19 @@ "node": ">=6.0.0" } }, + "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, + "license": "MIT", + "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", @@ -6877,6 +6934,36 @@ "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, + "license": "MIT", + "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, + "license": "MIT", + "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", @@ -12392,25 +12479,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.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "dev": true }, "check-error": { "version": "2.1.1", @@ -12685,6 +12757,29 @@ "tree-kill": "1.2.2", "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": { @@ -12828,6 +12923,12 @@ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "dev": true }, + "diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "dev": true + }, "doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -13199,6 +13300,16 @@ "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" + } + }, "doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -13207,6 +13318,15 @@ "requires": { "esutils": "^2.0.2" } + }, + "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" + } } } }, @@ -14854,6 +14974,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 1621991c..9e625950 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": { @@ -39,8 +41,10 @@ "@vitest/eslint-plugin": "^1.1.28", "@vue/eslint-config-airbnb": "^8.0.0", "@vue/test-utils": "^2.4.6", + "chalk": "^5.4.1", "click-outside-vue3": "^4.0.1", "cypress": "^14.0.3", + "diff": "^7.0.0", "dotenv": "^16.4.7", "eslint": "^8.57.1", "eslint-import-resolver-typescript": "^3.7.0", diff --git a/public/translations/de.json b/public/translations/de.json index de3ae122..fbd167e0 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-Dokumenten­betrachter, veröffentlicht unter der GNU Affero General Public License 3.0.", "About TIFY": "Über TIFY", "Brightness": "Helligkeit", @@ -11,12 +11,13 @@ "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", "Dismiss": "Ausblenden", "Document": "Dokument", + "Documentation": "Dokumentation", "Download Individual Images": "Einzelbilder herunterladen", "Exit fullscreen": "Vollbildmodus verlassen", "Expand": "Ausklappen", @@ -63,7 +64,6 @@ "Toggle double-page": "Doppelseite umschalten", "Toggle image filters": "Bildfilter umschalten", "Toggle page select": "Seitenauswahl umschalten", - "User guide": "Kurzanleitung", "Version": "Version", "View": "Ansicht", "Zoom in": "Vergrößern", diff --git a/public/translations/eo.json b/public/translations/eo.json index 65ca0682..bb3011bc 100644 --- a/public/translations/eo.json +++ b/public/translations/eo.json @@ -1,32 +1,38 @@ { "$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", "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", + "Documentation": "", "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 +42,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,7 +63,7 @@ "Title": "Titolo", "Toggle double-page": "Ŝaltu duoblan paĝon", "Toggle image filters": "Ŝaltu bildfiltrilojn", - "User guide": "Uzant-gvidilo", + "Toggle page select": "", "Version": "Versio", "View": "Vido", "Zoom in": "Zomi", diff --git a/public/translations/fr.json b/public/translations/fr.json index 6d27a02e..be3d991b 100644 --- a/public/translations/fr.json +++ b/public/translations/fr.json @@ -1,30 +1,34 @@ { "$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", "Dismiss": "Rejeter", "Document": "Document", + "Documentation": "", "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,7 +63,7 @@ "Title": "Titre", "Toggle double-page": "Basculer en mode double page", "Toggle image filters": "Appliquer les filtres visuels", - "User guide": "Guide d’utilisation", + "Toggle page select": "", "Version": "Version", "View": "Vue", "Zoom in": "Zoomer", diff --git a/public/translations/hr.json b/public/translations/hr.json index 8fb45ea8..a2d499ff 100644 --- a/public/translations/hr.json +++ b/public/translations/hr.json @@ -1,32 +1,34 @@ { "$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", "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", "Dismiss": "Odbaci", "Document": "Dokument", + "Documentation": "", "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,7 +63,7 @@ "Title": "Naslov", "Toggle double-page": "Dvije stranice na stranici", "Toggle image filters": "Filtri slika", - "User guide": "Korisnički vodič", + "Toggle page select": "", "Version": "Verzija", "View": "Pogled", "Zoom in": "Uvećaj", diff --git a/public/translations/it.json b/public/translations/it.json index 77544540..79ed53fe 100644 --- a/public/translations/it.json +++ b/public/translations/it.json @@ -1,35 +1,48 @@ { "$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", + "Documentation": "", "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 +53,7 @@ "Previous section": "Sezione precedente", "Related Resources": "Risorse correlate", "Renderings": "Rendering", + "Report a bug": "", "Reset": "Ripristina", "Rotate": "Ruota", "Saturation": "Saturazione", @@ -49,6 +63,8 @@ "Title": "Titolo", "Toggle double-page": "Attiva/disattiva doppia pagina", "Toggle image filters": "Attiva/disattiva filtri immagine", + "Toggle page select": "", + "Version": "", "View": "Vista", "Zoom in": "Zoom in", "Zoom out": "Zoom out" diff --git a/public/translations/nl.json b/public/translations/nl.json index 3a6f9b5d..ae96077a 100644 --- a/public/translations/nl.json +++ b/public/translations/nl.json @@ -17,6 +17,7 @@ "Description": "Beschrijving", "Dismiss": "Verbergen", "Document": "Document", + "Documentation": "", "Download Individual Images": "Afzonderlijke afbeeldingen downloaden", "Exit fullscreen": "Sluit de volledig scherm", "Expand": "Uitklappen", @@ -63,7 +64,6 @@ "Toggle double-page": "Dubbele pagina wisselen", "Toggle image filters": "Schakel afbeeldingsfilter in", "Toggle page select": "Schakel paginaselectie in", - "User guide": "Handleiding", "Version": "Versie", "View": "Weergave", "Zoom in": "Vergroten", diff --git a/public/translations/pl.json b/public/translations/pl.json index 5d3b117b..64dc367e 100644 --- a/public/translations/pl.json +++ b/public/translations/pl.json @@ -1,35 +1,48 @@ { "$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", + "Documentation": "", "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 +53,7 @@ "Previous section": "Poprzednia sekcja", "Related Resources": "Powiązane zasoby", "Renderings": "Rendery", + "Report a bug": "", "Reset": "Reset", "Rotate": "Obróć", "Saturation": "Saturacja", @@ -49,6 +63,8 @@ "Title": "Tytuł", "Toggle double-page": "Przejdź do widoku dwóch stron", "Toggle image filters": "Włącz filtry obrazów", + "Toggle page select": "", + "Version": "", "View": "Widok", "Zoom in": "Przybliżenie", "Zoom out": "Oddalenie" diff --git a/src/components/ViewHelp.vue b/src/components/ViewHelp.vue index db45fbca..430b4cee 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';