From f59d5236948ed593fd4cfc13b58a039733e1e7c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Mon, 10 Jul 2023 07:06:17 +0200 Subject: [PATCH] extensions: Add static defineTranslationFunctions() method The method can be used to define a set of gettext functions that call the corresponding method of an extension. Those functions are very similar to the gettext functions we are exporting, except that: - looking up the extension is delegated to the Extension/Preferences class - it is possible to avoid examining the stack when called with `import.meta.url` Part-of: --- js/extensions/extension.js | 7 ++- js/extensions/prefs.js | 7 ++- js/extensions/sharedInternals.js | 93 ++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 2 deletions(-) diff --git a/js/extensions/extension.js b/js/extensions/extension.js index 56f11fa63..966a8336b 100644 --- a/js/extensions/extension.js +++ b/js/extensions/extension.js @@ -1,4 +1,4 @@ -import {ExtensionBase, setExtensionManager} from './sharedInternals.js'; +import {ExtensionBase, GettextWrapper, setExtensionManager} from './sharedInternals.js'; export {gettext, ngettext, pgettext} from './sharedInternals.js'; @@ -6,6 +6,11 @@ const {extensionManager} = imports.ui.main; setExtensionManager(extensionManager); export class Extension extends ExtensionBase { + static defineTranslationFunctions(url) { + const wrapper = new GettextWrapper(this, url); + return wrapper.defineTranslationFunctions(); + } + enable() { } diff --git a/js/extensions/prefs.js b/js/extensions/prefs.js index 49c72d84a..db285e247 100644 --- a/js/extensions/prefs.js +++ b/js/extensions/prefs.js @@ -1,6 +1,6 @@ import GObject from 'gi://GObject'; -import {ExtensionBase, setExtensionManager} from './sharedInternals.js'; +import {ExtensionBase, GettextWrapper, setExtensionManager} from './sharedInternals.js'; import {extensionManager} from '../extensionsService.js'; setExtensionManager(extensionManager); @@ -8,6 +8,11 @@ setExtensionManager(extensionManager); export {gettext, ngettext, pgettext} from './sharedInternals.js'; export class ExtensionPreferences extends ExtensionBase { + static defineTranslationFunctions(url) { + const wrapper = new GettextWrapper(this, url); + return wrapper.defineTranslationFunctions(); + } + /** * Fill the preferences window with preferences. * diff --git a/js/extensions/sharedInternals.js b/js/extensions/sharedInternals.js index 8ec152332..2d5afd0f6 100644 --- a/js/extensions/sharedInternals.js +++ b/js/extensions/sharedInternals.js @@ -184,6 +184,99 @@ export class ExtensionBase { } } +export class GettextWrapper { + #url; + #extensionClass; + + constructor(extensionClass, url) { + this.#url = url; + this.#extensionClass = extensionClass; + } + + #detectUrl() { + const basePath = '/gnome-shell/extensions/'; + + // Search for an occurrence of an extension stack frame + // Start at 1 because 0 is the stack frame of this function + const [, ...stack] = new Error().stack.split('\n'); + const extensionLine = stack.find( + line => line.includes(basePath)); + + if (!extensionLine) + return null; + + // The exact stack line differs depending on where the function + // was called (function or module scope), and whether it's called + // from a module or legacy import (file:// URI vs. plain path). + // + // We don't have to care about the exact composition, all we need + // is a string that can be traversed as path and contains the UUID + const path = extensionLine.slice(extensionLine.indexOf(basePath)); + return `file://${path}`; + } + + #lookupExtension(funcName) { + if (!this.#url) + this.#url = this.#detectUrl(); + + const extension = this.#extensionClass.lookupByURL(this.#url); + if (!extension) + throw new Error(`${funcName} can only be called from extensions`); + return extension; + } + + #gettext(str) { + const extension = this.#lookupExtension('gettext'); + return extension.gettext(str); + } + + #ngettext(str, strPlural, n) { + const extension = this.#lookupExtension('ngettext'); + return extension.gettext(str, strPlural, n); + } + + #pgettext(context, str) { + const extension = this.#lookupExtension('pgettext'); + return extension.pgettext(context, str); + } + + defineTranslationFunctions() { + return { + /** + * Translate `str` using the extension's gettext domain + * + * @param {string} str - the string to translate + * + * @returns {string} the translated string + */ + gettext: this.#gettext.bind(this), + + /** + * Translate `str` and choose plural form using the extension's + * gettext domain + * + * @param {string} str - the string to translate + * @param {string} strPlural - the plural form of the string + * @param {number} n - the quantity for which translation is needed + * + * @returns {string} the translated string + */ + ngettext: this.#ngettext.bind(this), + + /** + * Translate `str` in the context of `context` using the extension's + * gettext domain + * + * @param {string} context - context to disambiguate `str` + * @param {string} str - the string to translate + * + * @returns {string} the translated string + */ + pgettext: this.#pgettext.bind(this), + }; + } +} + /** * @private *