]> BookStack Code Mirror - bookstack/commitdiff
JS: Converted/updated translation code to TS, fixed some comment counts
authorDan Brown <redacted>
Mon, 7 Oct 2024 21:55:10 +0000 (22:55 +0100)
committerDan Brown <redacted>
Mon, 7 Oct 2024 21:55:10 +0000 (22:55 +0100)
- 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
resources/js/app.js
resources/js/components/page-comment.js
resources/js/components/page-comments.js
resources/js/global.d.ts
resources/js/services/__tests__/translations.test.ts [new file with mode: 0644]
resources/js/services/translations.js [deleted file]
resources/js/services/translations.ts [new file with mode: 0644]

index ba7d799724896700b53b70e4172697971a995c1c..e5f491839f0933bc7f7f80ae126289b488f8ebde 100644 (file)
@@ -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.
index 7f4bbe54d639e58db5811a4f3a0b3c84265517b9..5f4902f866f70b55af84ef7eb6ae6309c0bbe0d3 100644 (file)
@@ -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();
index cac20c9fb2c6351ffaccf1cc6a31d486680b4c43..fd8ad1f2e47b0ac5a5c6494f7ffcc9c5b2df5d8e 100644 (file)
@@ -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() {
index bd6dd3c82ff2673f2480904acef7d3e87d7d694f..1d6abfe2044ff3a0a9e006f29e04fb4fef94601c 100644 (file)
@@ -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() {
index 0d7efc4d43c8673e4e3a7def39d1082a5dfeaba5..e505c96e0d4a24f1a5d83fcee75a9e1b70962273 100644 (file)
@@ -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 (file)
index 0000000..043f174
--- /dev/null
@@ -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 (file)
index e562a91..0000000
+++ /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 (file)
index 0000000..b37dbdf
--- /dev/null
@@ -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;
+    }
+
+}