From d22413b9317b40110fa2fd41134d1831dd760d0b Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 7 Oct 2024 22:55:10 +0100 Subject: [PATCH] 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. --- dev/docs/javascript-code.md | 4 +- resources/js/app.js | 12 +- resources/js/components/page-comment.js | 2 +- resources/js/components/page-comments.js | 13 +- resources/js/global.d.ts | 2 + .../services/__tests__/translations.test.ts | 67 +++++++++ resources/js/services/translations.js | 131 ------------------ resources/js/services/translations.ts | 67 +++++++++ 8 files changed, 152 insertions(+), 146 deletions(-) create mode 100644 resources/js/services/__tests__/translations.test.ts delete mode 100644 resources/js/services/translations.js create mode 100644 resources/js/services/translations.ts diff --git a/dev/docs/javascript-code.md b/dev/docs/javascript-code.md index ba7d7997248..e5f491839f0 100644 --- a/dev/docs/javascript-code.md +++ b/dev/docs/javascript-code.md @@ -137,8 +137,8 @@ window.$events.showValidationErrors(error); // Translator // Take the given plural text and count to decide on what plural option // to use, Similar to laravel's trans_choice function but instead -// takes the direction directly instead of a translation key. -window.trans_plural(translationString, count, replacements); +// takes the translation text directly instead of a translation key. +window.$trans.choice(translationString, count, replacements); // Component System // Parse and initialise any components from the given root el down. diff --git a/resources/js/app.js b/resources/js/app.js index 7f4bbe54d63..5f4902f866f 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -1,6 +1,6 @@ import {EventManager} from './services/events.ts'; import {HttpManager} from './services/http.ts'; -import Translations from './services/translations'; +import {Translator} from './services/translations.ts'; import * as componentMap from './components'; import {ComponentStore} from './services/components.ts'; @@ -22,16 +22,10 @@ window.importVersioned = function importVersioned(moduleName) { return import(importPath); }; -// Set events and http services on window +// Set events, http & translation services on window window.$http = new HttpManager(); window.$events = new EventManager(); - -// 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); +window.$trans = new Translator(); // Load & initialise components window.$components = new ComponentStore(); diff --git a/resources/js/components/page-comment.js b/resources/js/components/page-comment.js index cac20c9fb2c..fd8ad1f2e47 100644 --- a/resources/js/components/page-comment.js +++ b/resources/js/components/page-comment.js @@ -104,9 +104,9 @@ export class PageComment extends Component { this.showLoading(); await window.$http.delete(`/comment/${this.commentId}`); + this.$emit('delete'); this.container.closest('.comment-branch').remove(); window.$events.success(this.deletedText); - this.$emit('delete'); } showLoading() { diff --git a/resources/js/components/page-comments.js b/resources/js/components/page-comments.js index bd6dd3c82ff..1d6abfe2044 100644 --- a/resources/js/components/page-comments.js +++ b/resources/js/components/page-comments.js @@ -40,7 +40,7 @@ export class PageComments extends Component { setupListeners() { this.elem.addEventListener('page-comment-delete', () => { - this.updateCount(); + setTimeout(() => this.updateCount(), 1); this.hideForm(); }); @@ -72,7 +72,13 @@ export class PageComments extends Component { window.$http.post(`/comment/${this.pageId}`, reqData).then(resp => { 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); this.hideForm(); this.updateCount(); @@ -87,7 +93,8 @@ export class PageComments extends Component { updateCount() { 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() { diff --git a/resources/js/global.d.ts b/resources/js/global.d.ts index 0d7efc4d43c..e505c96e0d4 100644 --- a/resources/js/global.d.ts +++ b/resources/js/global.d.ts @@ -1,6 +1,7 @@ import {ComponentStore} from "./services/components"; import {EventManager} from "./services/events"; import {HttpManager} from "./services/http"; +import {Translator} from "./services/translations"; declare global { const __DEV__: boolean; @@ -8,6 +9,7 @@ declare global { interface Window { $components: ComponentStore; $events: EventManager; + $trans: Translator; $http: HttpManager; baseUrl: (path: string) => string; } diff --git a/resources/js/services/__tests__/translations.test.ts b/resources/js/services/__tests__/translations.test.ts new file mode 100644 index 00000000000..043f1745ff6 --- /dev/null +++ b/resources/js/services/__tests__/translations.test.ts @@ -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'); + }); + + }); +}); \ No newline at end of file diff --git a/resources/js/services/translations.js b/resources/js/services/translations.js deleted file mode 100644 index e562a9152e9..00000000000 --- a/resources/js/services/translations.js +++ /dev/null @@ -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; diff --git a/resources/js/services/translations.ts b/resources/js/services/translations.ts new file mode 100644 index 00000000000..b37dbdfb074 --- /dev/null +++ b/resources/js/services/translations.ts @@ -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 { + 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 { + 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; + } + +}