1
0
mirror of https://github.com/BookStackApp/BookStack.git synced 2026-01-03 23:42:28 +03:00

JS: Converted/updated translation code to TS, fixed some comment counts

- Migrated translation service to TS, stripping a lot of now unused code
  along the way.
- Added test to cover translation service.
- Fixed some comment count issues, where it was not showing correct
  value. or updating, on comment create or delete.
This commit is contained in:
Dan Brown
2024-10-07 22:55:10 +01:00
parent 8b9bcc1768
commit d22413b931
8 changed files with 152 additions and 146 deletions

View File

@@ -0,0 +1,67 @@
import {Translator} from "../translations";
describe('Translations Service', () => {
let $trans: Translator;
beforeEach(() => {
$trans = new Translator();
});
describe('choice()', () => {
test('it pluralises as expected', () => {
const cases = [
{
translation: `cat`, count: 10000,
expected: `cat`,
},
{
translation: `cat|cats`, count: 1,
expected: `cat`,
},
{
translation: `cat|cats`, count: 0,
expected: `cats`,
},
{
translation: `cat|cats`, count: 2,
expected: `cats`,
},
{
translation: `{0} cat|[1,100] dog|[100,*] turtle`, count: 0,
expected: `cat`,
},
{
translation: `{0} cat|[1,100] dog|[100,*] turtle`, count: 40,
expected: `dog`,
},
{
translation: `{0} cat|[1,100] dog|[100,*] turtle`, count: 101,
expected: `turtle`,
},
];
for (const testCase of cases) {
const output = $trans.choice(testCase.translation, testCase.count, {});
expect(output).toEqual(testCase.expected);
}
});
test('it replaces as expected', () => {
const caseA = $trans.choice(`{0} cat|[1,100] :count dog|[100,*] turtle`, 4, {count: '5'});
expect(caseA).toEqual('5 dog');
const caseB = $trans.choice(`an :a :b :c dinosaur|many`, 1, {a: 'orange', b: 'angry', c: 'big'});
expect(caseB).toEqual('an orange angry big dinosaur');
});
test('not provided replacements are left as-is', () => {
const caseA = $trans.choice(`An :a dog`, 5, {});
expect(caseA).toEqual('An :a dog');
});
});
});

View File

@@ -1,131 +0,0 @@
/**
* Translation Manager
* Handles the JavaScript side of translating strings
* in a way which fits with Laravel.
*/
class Translator {
constructor() {
this.store = new Map();
this.parseTranslations();
}
/**
* Parse translations out of the page and place into the store.
*/
parseTranslations() {
const translationMetaTags = document.querySelectorAll('meta[name="translation"]');
for (const tag of translationMetaTags) {
const key = tag.getAttribute('key');
const value = tag.getAttribute('value');
this.store.set(key, value);
}
}
/**
* Get a translation, Same format as Laravel's 'trans' helper
* @param key
* @param replacements
* @returns {*}
*/
get(key, replacements) {
const text = this.getTransText(key);
return this.performReplacements(text, replacements);
}
/**
* Get pluralised text, Dependent on the given count.
* Same format at Laravel's 'trans_choice' helper.
* @param key
* @param count
* @param replacements
* @returns {*}
*/
getPlural(key, count, replacements) {
const text = this.getTransText(key);
return this.parsePlural(text, count, replacements);
}
/**
* Parse the given translation and find the correct plural option
* to use. Similar format at Laravel's 'trans_choice' helper.
* @param {String} translation
* @param {Number} count
* @param {Object} replacements
* @returns {String}
*/
parsePlural(translation, count, replacements) {
const splitText = translation.split('|');
const exactCountRegex = /^{([0-9]+)}/;
const rangeRegex = /^\[([0-9]+),([0-9*]+)]/;
let result = null;
for (const t of splitText) {
// Parse exact matches
const exactMatches = t.match(exactCountRegex);
if (exactMatches !== null && Number(exactMatches[1]) === count) {
result = t.replace(exactCountRegex, '').trim();
break;
}
// Parse range matches
const rangeMatches = t.match(rangeRegex);
if (rangeMatches !== null) {
const rangeStart = Number(rangeMatches[1]);
if (rangeStart <= count && (rangeMatches[2] === '*' || Number(rangeMatches[2]) >= count)) {
result = t.replace(rangeRegex, '').trim();
break;
}
}
}
if (result === null && splitText.length > 1) {
result = (count === 1) ? splitText[0] : splitText[1];
}
if (result === null) {
result = splitText[0];
}
return this.performReplacements(result, replacements);
}
/**
* Fetched translation text from the store for the given key.
* @param key
* @returns {String|Object}
*/
getTransText(key) {
const value = this.store.get(key);
if (value === undefined) {
console.warn(`Translation with key "${key}" does not exist`);
}
return value;
}
/**
* Perform replacements on a string.
* @param {String} string
* @param {Object} replacements
* @returns {*}
*/
performReplacements(string, replacements) {
if (!replacements) return string;
const replaceMatches = string.match(/:(\S+)/g);
if (replaceMatches === null) return string;
let updatedString = string;
replaceMatches.forEach(match => {
const key = match.substring(1);
if (typeof replacements[key] === 'undefined') return;
updatedString = updatedString.replace(match, replacements[key]);
});
return updatedString;
}
}
export default Translator;

View File

@@ -0,0 +1,67 @@
/**
* Translation Manager
* Helps with some of the JavaScript side of translating strings
* in a way which fits with Laravel.
*/
export class Translator {
/**
* Parse the given translation and find the correct plural option
* to use. Similar format at Laravel's 'trans_choice' helper.
*/
choice(translation: string, count: number, replacements: Record<string, string> = {}): string {
const splitText = translation.split('|');
const exactCountRegex = /^{([0-9]+)}/;
const rangeRegex = /^\[([0-9]+),([0-9*]+)]/;
let result = null;
for (const t of splitText) {
// Parse exact matches
const exactMatches = t.match(exactCountRegex);
if (exactMatches !== null && Number(exactMatches[1]) === count) {
result = t.replace(exactCountRegex, '').trim();
break;
}
// Parse range matches
const rangeMatches = t.match(rangeRegex);
if (rangeMatches !== null) {
const rangeStart = Number(rangeMatches[1]);
if (rangeStart <= count && (rangeMatches[2] === '*' || Number(rangeMatches[2]) >= count)) {
result = t.replace(rangeRegex, '').trim();
break;
}
}
}
if (result === null && splitText.length > 1) {
result = (count === 1) ? splitText[0] : splitText[1];
}
if (result === null) {
result = splitText[0];
}
return this.performReplacements(result, replacements);
}
protected performReplacements(string: string, replacements: Record<string, string>): string {
const replaceMatches = string.match(/:(\S+)/g);
if (replaceMatches === null) {
return string;
}
let updatedString = string;
for (const match of replaceMatches) {
const key = match.substring(1);
if (typeof replacements[key] === 'undefined') {
continue;
}
updatedString = updatedString.replace(match, replacements[key]);
}
return updatedString;
}
}