mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-06-19 11:41:23 +08: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:
@ -137,8 +137,8 @@ window.$events.showValidationErrors(error);
|
|||||||
// Translator
|
// Translator
|
||||||
// Take the given plural text and count to decide on what plural option
|
// Take the given plural text and count to decide on what plural option
|
||||||
// to use, Similar to laravel's trans_choice function but instead
|
// to use, Similar to laravel's trans_choice function but instead
|
||||||
// takes the direction directly instead of a translation key.
|
// takes the translation text directly instead of a translation key.
|
||||||
window.trans_plural(translationString, count, replacements);
|
window.$trans.choice(translationString, count, replacements);
|
||||||
|
|
||||||
// Component System
|
// Component System
|
||||||
// Parse and initialise any components from the given root el down.
|
// Parse and initialise any components from the given root el down.
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import {EventManager} from './services/events.ts';
|
import {EventManager} from './services/events.ts';
|
||||||
import {HttpManager} from './services/http.ts';
|
import {HttpManager} from './services/http.ts';
|
||||||
import Translations from './services/translations';
|
import {Translator} from './services/translations.ts';
|
||||||
import * as componentMap from './components';
|
import * as componentMap from './components';
|
||||||
import {ComponentStore} from './services/components.ts';
|
import {ComponentStore} from './services/components.ts';
|
||||||
|
|
||||||
@ -22,16 +22,10 @@ window.importVersioned = function importVersioned(moduleName) {
|
|||||||
return import(importPath);
|
return import(importPath);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set events and http services on window
|
// Set events, http & translation services on window
|
||||||
window.$http = new HttpManager();
|
window.$http = new HttpManager();
|
||||||
window.$events = new EventManager();
|
window.$events = new EventManager();
|
||||||
|
window.$trans = new Translator();
|
||||||
// Translation setup
|
|
||||||
// Creates a global function with name 'trans' to be used in the same way as the Laravel translation system
|
|
||||||
const translator = new Translations();
|
|
||||||
window.trans = translator.get.bind(translator);
|
|
||||||
window.trans_choice = translator.getPlural.bind(translator);
|
|
||||||
window.trans_plural = translator.parsePlural.bind(translator);
|
|
||||||
|
|
||||||
// Load & initialise components
|
// Load & initialise components
|
||||||
window.$components = new ComponentStore();
|
window.$components = new ComponentStore();
|
||||||
|
@ -104,9 +104,9 @@ export class PageComment extends Component {
|
|||||||
this.showLoading();
|
this.showLoading();
|
||||||
|
|
||||||
await window.$http.delete(`/comment/${this.commentId}`);
|
await window.$http.delete(`/comment/${this.commentId}`);
|
||||||
|
this.$emit('delete');
|
||||||
this.container.closest('.comment-branch').remove();
|
this.container.closest('.comment-branch').remove();
|
||||||
window.$events.success(this.deletedText);
|
window.$events.success(this.deletedText);
|
||||||
this.$emit('delete');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
showLoading() {
|
showLoading() {
|
||||||
|
@ -40,7 +40,7 @@ export class PageComments extends Component {
|
|||||||
|
|
||||||
setupListeners() {
|
setupListeners() {
|
||||||
this.elem.addEventListener('page-comment-delete', () => {
|
this.elem.addEventListener('page-comment-delete', () => {
|
||||||
this.updateCount();
|
setTimeout(() => this.updateCount(), 1);
|
||||||
this.hideForm();
|
this.hideForm();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -72,7 +72,13 @@ export class PageComments extends Component {
|
|||||||
|
|
||||||
window.$http.post(`/comment/${this.pageId}`, reqData).then(resp => {
|
window.$http.post(`/comment/${this.pageId}`, reqData).then(resp => {
|
||||||
const newElem = htmlToDom(resp.data);
|
const newElem = htmlToDom(resp.data);
|
||||||
this.formContainer.after(newElem);
|
|
||||||
|
if (reqData.parent_id) {
|
||||||
|
this.formContainer.after(newElem);
|
||||||
|
} else {
|
||||||
|
this.container.append(newElem);
|
||||||
|
}
|
||||||
|
|
||||||
window.$events.success(this.createdText);
|
window.$events.success(this.createdText);
|
||||||
this.hideForm();
|
this.hideForm();
|
||||||
this.updateCount();
|
this.updateCount();
|
||||||
@ -87,7 +93,8 @@ export class PageComments extends Component {
|
|||||||
|
|
||||||
updateCount() {
|
updateCount() {
|
||||||
const count = this.getCommentCount();
|
const count = this.getCommentCount();
|
||||||
this.commentsTitle.textContent = window.trans_plural(this.countText, count, {count});
|
console.log('update count', count, this.container);
|
||||||
|
this.commentsTitle.textContent = window.$trans.choice(this.countText, count, {count});
|
||||||
}
|
}
|
||||||
|
|
||||||
resetForm() {
|
resetForm() {
|
||||||
|
2
resources/js/global.d.ts
vendored
2
resources/js/global.d.ts
vendored
@ -1,6 +1,7 @@
|
|||||||
import {ComponentStore} from "./services/components";
|
import {ComponentStore} from "./services/components";
|
||||||
import {EventManager} from "./services/events";
|
import {EventManager} from "./services/events";
|
||||||
import {HttpManager} from "./services/http";
|
import {HttpManager} from "./services/http";
|
||||||
|
import {Translator} from "./services/translations";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
const __DEV__: boolean;
|
const __DEV__: boolean;
|
||||||
@ -8,6 +9,7 @@ declare global {
|
|||||||
interface Window {
|
interface Window {
|
||||||
$components: ComponentStore;
|
$components: ComponentStore;
|
||||||
$events: EventManager;
|
$events: EventManager;
|
||||||
|
$trans: Translator;
|
||||||
$http: HttpManager;
|
$http: HttpManager;
|
||||||
baseUrl: (path: string) => string;
|
baseUrl: (path: string) => string;
|
||||||
}
|
}
|
||||||
|
67
resources/js/services/__tests__/translations.test.ts
Normal file
67
resources/js/services/__tests__/translations.test.ts
Normal 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');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
@ -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;
|
|
67
resources/js/services/translations.ts
Normal file
67
resources/js/services/translations.ts
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Reference in New Issue
Block a user