2023-07-09 00:13:17 +00:00
|
|
|
import Gio from 'gi://Gio';
|
|
|
|
import GLib from 'gi://GLib';
|
2023-07-10 08:33:31 +00:00
|
|
|
import GObject from 'gi://GObject';
|
2023-06-30 10:57:41 +00:00
|
|
|
|
2023-07-01 00:11:37 +00:00
|
|
|
import {bindtextdomain} from 'gettext';
|
2023-06-30 10:57:41 +00:00
|
|
|
|
|
|
|
const Config = imports.misc.config;
|
|
|
|
|
2023-07-01 00:11:37 +00:00
|
|
|
export class ExtensionBase {
|
|
|
|
#gettextDomain;
|
|
|
|
|
2023-07-10 04:19:53 +00:00
|
|
|
/**
|
|
|
|
* Look up an extension by URL (usually 'import.meta.url')
|
|
|
|
*
|
|
|
|
* @param {string} url - a file:// URL
|
|
|
|
*/
|
|
|
|
static lookupByURL(url) {
|
|
|
|
if (!url.startsWith('file://'))
|
|
|
|
return null;
|
|
|
|
|
|
|
|
// Keep the last '/' from 'file://' to force an absolute path
|
|
|
|
let path = url.slice(6);
|
|
|
|
|
|
|
|
// Walk up the directory tree, looking for an extension with
|
|
|
|
// the same UUID as a directory name.
|
|
|
|
do {
|
|
|
|
path = GLib.path_get_dirname(path);
|
|
|
|
|
|
|
|
const dirName = GLib.path_get_basename(path);
|
2023-07-10 08:33:31 +00:00
|
|
|
const extension = this.lookupByUUID(dirName);
|
2023-07-10 04:19:53 +00:00
|
|
|
if (extension !== undefined)
|
2023-07-10 08:33:31 +00:00
|
|
|
return extension;
|
2023-07-10 04:19:53 +00:00
|
|
|
} while (path !== '/');
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2023-07-10 08:33:31 +00:00
|
|
|
/**
|
|
|
|
* Look up an extension by UUID
|
|
|
|
*
|
|
|
|
* @param {string} _uuid
|
|
|
|
*/
|
|
|
|
static lookupByUUID(_uuid) {
|
|
|
|
throw new GObject.NotImplementedError();
|
|
|
|
}
|
|
|
|
|
2023-07-01 00:11:37 +00:00
|
|
|
/**
|
|
|
|
* @param {object} metadata - metadata passed in when loading the extension
|
|
|
|
*/
|
|
|
|
constructor(metadata) {
|
|
|
|
if (this.constructor === ExtensionBase)
|
|
|
|
throw new Error('ExtensionBase cannot be used directly.');
|
|
|
|
|
|
|
|
if (!metadata)
|
|
|
|
throw new Error(`${this.constructor.name} did not pass metadata to parent`);
|
|
|
|
|
|
|
|
this.metadata = metadata;
|
2023-07-10 03:56:34 +00:00
|
|
|
|
|
|
|
const domain = this.metadata['gettext-domain'];
|
|
|
|
if (domain)
|
|
|
|
this.initTranslations(domain);
|
2023-07-01 00:11:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @type {string}
|
|
|
|
*/
|
|
|
|
get uuid() {
|
|
|
|
return this.metadata['uuid'];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @type {Gio.File}
|
|
|
|
*/
|
|
|
|
get dir() {
|
|
|
|
return this.metadata['dir'];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @type {string}
|
|
|
|
*/
|
|
|
|
get path() {
|
|
|
|
return this.metadata['path'];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get a GSettings object for schema, using schema files in
|
|
|
|
* extensionsdir/schemas. If schema is omitted, it is taken
|
|
|
|
* from metadata['settings-schema'].
|
|
|
|
*
|
|
|
|
* @param {string=} schema - the GSettings schema id
|
|
|
|
*
|
|
|
|
* @returns {Gio.Settings}
|
|
|
|
*/
|
|
|
|
getSettings(schema) {
|
|
|
|
schema ||= this.metadata['settings-schema'];
|
|
|
|
|
|
|
|
// Expect USER extensions to have a schemas/ subfolder, otherwise assume a
|
|
|
|
// SYSTEM extension that has been installed in the same prefix as the shell
|
|
|
|
const schemaDir = this.dir.get_child('schemas');
|
|
|
|
const defaultSource = Gio.SettingsSchemaSource.get_default();
|
|
|
|
let schemaSource;
|
|
|
|
if (schemaDir.query_exists(null)) {
|
|
|
|
schemaSource = Gio.SettingsSchemaSource.new_from_directory(
|
|
|
|
schemaDir.get_path(), defaultSource, false);
|
|
|
|
} else {
|
|
|
|
schemaSource = defaultSource;
|
|
|
|
}
|
|
|
|
|
|
|
|
const schemaObj = schemaSource.lookup(schema, true);
|
|
|
|
if (!schemaObj)
|
|
|
|
throw new Error(`Schema ${schema} could not be found for extension ${this.uuid}. Please check your installation`);
|
|
|
|
|
|
|
|
return new Gio.Settings({settings_schema: schemaObj});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Initialize Gettext to load translations from extensionsdir/locale. If
|
|
|
|
* domain is not provided, it will be taken from metadata['gettext-domain']
|
|
|
|
*
|
|
|
|
* @param {string=} domain - the gettext domain to use
|
|
|
|
*/
|
|
|
|
initTranslations(domain) {
|
|
|
|
domain ||= this.metadata['gettext-domain'];
|
|
|
|
|
|
|
|
if (!domain)
|
|
|
|
throw new Error('initTranslations() was called without providing a valid domain');
|
|
|
|
|
|
|
|
// Expect USER extensions to have a locale/ subfolder, otherwise assume a
|
|
|
|
// SYSTEM extension that has been installed in the same prefix as the shell
|
|
|
|
const localeDir = this.dir.get_child('locale');
|
|
|
|
if (localeDir.query_exists(null))
|
|
|
|
bindtextdomain(domain, localeDir.get_path());
|
|
|
|
else
|
|
|
|
bindtextdomain(domain, Config.LOCALEDIR);
|
|
|
|
|
|
|
|
this.#gettextDomain = domain;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Translate `str` using the extension's gettext domain
|
|
|
|
*
|
|
|
|
* @param {string} str - the string to translate
|
|
|
|
*
|
|
|
|
* @returns {string} the translated string
|
|
|
|
*/
|
|
|
|
gettext(str) {
|
|
|
|
this.#checkGettextDomain('gettext');
|
|
|
|
return GLib.dgettext(this.#gettextDomain, str);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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(str, strPlural, n) {
|
|
|
|
this.#checkGettextDomain('ngettext');
|
|
|
|
return GLib.dngettext(this.#gettextDomain, str, strPlural, n);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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(context, str) {
|
|
|
|
this.#checkGettextDomain('pgettext');
|
|
|
|
return GLib.dpgettext2(this.#gettextDomain, context, str);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {string} func
|
|
|
|
*/
|
|
|
|
#checkGettextDomain(func) {
|
|
|
|
if (!this.#gettextDomain)
|
|
|
|
throw new Error(`${func}() is used without calling initTranslations() first`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-10 05:06:17 +00:00
|
|
|
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),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|