mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 1192433: Part 2 - [webext] Allow loading and querying all available locales. r=billm
This commit is contained in:
parent
0b70cfbcfd
commit
bc17521a3c
@ -33,8 +33,12 @@ XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
|
|||||||
"resource://gre/modules/NetUtil.jsm");
|
"resource://gre/modules/NetUtil.jsm");
|
||||||
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
|
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
|
||||||
"resource://gre/modules/FileUtils.jsm");
|
"resource://gre/modules/FileUtils.jsm");
|
||||||
|
XPCOMUtils.defineLazyModuleGetter(this, "OS",
|
||||||
|
"resource://gre/modules/osfile.jsm");
|
||||||
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
|
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
|
||||||
"resource://gre/modules/PrivateBrowsingUtils.jsm");
|
"resource://gre/modules/PrivateBrowsingUtils.jsm");
|
||||||
|
XPCOMUtils.defineLazyModuleGetter(this, "Task",
|
||||||
|
"resource://gre/modules/Task.jsm");
|
||||||
|
|
||||||
Cu.import("resource://gre/modules/ExtensionManagement.jsm");
|
Cu.import("resource://gre/modules/ExtensionManagement.jsm");
|
||||||
|
|
||||||
@ -336,22 +340,48 @@ this.ExtensionData = function(rootURI)
|
|||||||
this.rootURI = rootURI;
|
this.rootURI = rootURI;
|
||||||
|
|
||||||
this.manifest = null;
|
this.manifest = null;
|
||||||
this.localeMessages = null;
|
this.id = null;
|
||||||
|
// Map(locale-name -> message-map)
|
||||||
|
// Contains a key for each loaded locale, each of which is a
|
||||||
|
// JSON-compatible object with a property for each message
|
||||||
|
// in that locale.
|
||||||
|
this.localeMessages = new Map();
|
||||||
this.selectedLocale = null;
|
this.selectedLocale = null;
|
||||||
|
this._promiseLocales = null;
|
||||||
|
|
||||||
this.errors = [];
|
this.errors = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
ExtensionData.prototype = {
|
ExtensionData.prototype = {
|
||||||
|
get logger() {
|
||||||
|
let id = this.id || "<unknown>";
|
||||||
|
return Log.repository.getLogger(LOGGER_ID_BASE + id);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Report an error about the extension's manifest file.
|
||||||
|
manifestError(message) {
|
||||||
|
this.packagingError(`Reading manifest: ${message}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Report an error about the extension's general packaging.
|
||||||
|
packagingError(message) {
|
||||||
|
this.errors.push(message);
|
||||||
|
this.logger.error(`Loading extension '${this.id}': ${message}`);
|
||||||
|
},
|
||||||
|
|
||||||
// https://developer.chrome.com/extensions/i18n
|
// https://developer.chrome.com/extensions/i18n
|
||||||
localizeMessage(message, substitutions) {
|
localizeMessage(message, substitutions, locale = this.selectedLocale) {
|
||||||
if (message in this.localeMessages) {
|
let messages = {};
|
||||||
let str = this.localeMessages[message].message;
|
if (this.localeMessages.has(locale)) {
|
||||||
|
messages = this.localeMessages.get(locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message in messages) {
|
||||||
|
let str = messages[message].message;
|
||||||
|
|
||||||
if (!substitutions) {
|
if (!substitutions) {
|
||||||
substitutions = [];
|
substitutions = [];
|
||||||
}
|
} else if (!Array.isArray(substitutions)) {
|
||||||
if (!Array.isArray(substitutions)) {
|
|
||||||
substitutions = [substitutions];
|
substitutions = [substitutions];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -364,7 +394,7 @@ ExtensionData.prototype = {
|
|||||||
if (name.length == 1 && name[0] >= '1' && name[0] <= '9') {
|
if (name.length == 1 && name[0] >= '1' && name[0] <= '9') {
|
||||||
return substitutions[parseInt(name) - 1];
|
return substitutions[parseInt(name) - 1];
|
||||||
} else {
|
} else {
|
||||||
let content = this.localeMessages[message].placeholders[name].content;
|
let content = messages[message].placeholders[name].content;
|
||||||
if (content[0] == '$') {
|
if (content[0] == '$') {
|
||||||
return replacer(matched, content[1]);
|
return replacer(matched, content[1]);
|
||||||
} else {
|
} else {
|
||||||
@ -379,7 +409,13 @@ ExtensionData.prototype = {
|
|||||||
|
|
||||||
// Check for certain pre-defined messages.
|
// Check for certain pre-defined messages.
|
||||||
if (message == "@@extension_id") {
|
if (message == "@@extension_id") {
|
||||||
return this.id;
|
if ("uuid" in this) {
|
||||||
|
// Per Chrome, this isn't available before an ID is guaranteed
|
||||||
|
// to have been assigned, namely, in manifest files.
|
||||||
|
// This should only be present in instances of the |Extension|
|
||||||
|
// subclass.
|
||||||
|
return this.uuid;
|
||||||
|
}
|
||||||
} else if (message == "@@ui_locale") {
|
} else if (message == "@@ui_locale") {
|
||||||
return Locale.getLocale();
|
return Locale.getLocale();
|
||||||
} else if (message == "@@bidi_dir") {
|
} else if (message == "@@bidi_dir") {
|
||||||
@ -390,28 +426,115 @@ ExtensionData.prototype = {
|
|||||||
return "??";
|
return "??";
|
||||||
},
|
},
|
||||||
|
|
||||||
localize(str) {
|
// Localize a string, replacing all |__MSG_(.*)__| tokens with the
|
||||||
|
// matching string from the current locale, as determined by
|
||||||
|
// |this.selectedLocale|.
|
||||||
|
//
|
||||||
|
// This may not be called before calling either |initLocale| or
|
||||||
|
// |initAllLocales|.
|
||||||
|
localize(str, locale = this.selectedLocale) {
|
||||||
if (!str) {
|
if (!str) {
|
||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (str.startsWith("__MSG_") && str.endsWith("__")) {
|
return str.replace(/__MSG_([A-Za-z0-9@_]+?)__/g, (matched, message) => {
|
||||||
let message = str.substring("__MSG_".length, str.length - "__".length);
|
return this.localizeMessage(message, [], locale);
|
||||||
return this.localizeMessage(message);
|
});
|
||||||
}
|
|
||||||
|
|
||||||
return str;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
readJSON(uri) {
|
// If a "default_locale" is specified in that manifest, returns it
|
||||||
|
// as a Gecko-compatible locale string. Otherwise, returns null.
|
||||||
|
get defaultLocale() {
|
||||||
|
if ("default_locale" in this.manifest) {
|
||||||
|
return this.normalizeLocaleCode(this.manifest.default_locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Normalizes a Chrome-compatible locale code to the appropriate
|
||||||
|
// Gecko-compatible variant. Currently, this means simply
|
||||||
|
// replacing underscores with hyphens.
|
||||||
|
normalizeLocaleCode(locale) {
|
||||||
|
return String.replace(locale, /_/g, "-");
|
||||||
|
},
|
||||||
|
|
||||||
|
readDirectory: Task.async(function* (path) {
|
||||||
|
if (this.rootURI instanceof Ci.nsIFileURL) {
|
||||||
|
let uri = NetUtil.newURI(this.rootURI.resolve("./" + path));
|
||||||
|
let fullPath = uri.QueryInterface(Ci.nsIFileURL).file.path;
|
||||||
|
|
||||||
|
let iter = new OS.File.DirectoryIterator(fullPath);
|
||||||
|
let results = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
yield iter.forEach(entry => {
|
||||||
|
results.push(entry);
|
||||||
|
});
|
||||||
|
} catch (e) {}
|
||||||
|
iter.close();
|
||||||
|
|
||||||
|
// Always return a list, even if the directory does not exist (or is
|
||||||
|
// not a directory) for symmetry with the ZipReader behavior.
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(this.rootURI instanceof Ci.nsIJARURI &&
|
||||||
|
this.rootURI.JARFile instanceof Ci.nsIFileURL)) {
|
||||||
|
throw Error("Invalid extension root URL");
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: We need a way to do this without main thread IO.
|
||||||
|
|
||||||
|
let file = this.rootURI.JARFile.file;
|
||||||
|
let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].createInstance(Ci.nsIZipReader);
|
||||||
|
try {
|
||||||
|
zipReader.open(file);
|
||||||
|
|
||||||
|
let results = [];
|
||||||
|
|
||||||
|
// Normalize the directory path.
|
||||||
|
path = path.replace(/\/\/+/g, "/").replace(/^\/|\/$/g, "") + "/";
|
||||||
|
|
||||||
|
// Escape pattern metacharacters.
|
||||||
|
let pattern = path.replace(/[[\]()?*~|$\\]/g, "\\$&");
|
||||||
|
|
||||||
|
let enumerator = zipReader.findEntries(pattern + "*");
|
||||||
|
while (enumerator.hasMore()) {
|
||||||
|
let name = enumerator.getNext();
|
||||||
|
if (!name.startsWith(path)) {
|
||||||
|
throw new Error("Unexpected ZipReader entry");
|
||||||
|
}
|
||||||
|
|
||||||
|
// The enumerator returns the full path of all entries.
|
||||||
|
// Trim off the leading path, and filter out entries from
|
||||||
|
// subdirectories.
|
||||||
|
name = name.slice(path.length);
|
||||||
|
if (name && !/\/./.test(name)) {
|
||||||
|
results.push({
|
||||||
|
name: name.replace("/", ""),
|
||||||
|
isDir: name.endsWith("/"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
} finally {
|
||||||
|
zipReader.close();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
readJSON(path) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
let uri = this.rootURI.resolve(`./${path}`);
|
||||||
|
|
||||||
NetUtil.asyncFetch({uri, loadUsingSystemPrincipal: true}, (inputStream, status) => {
|
NetUtil.asyncFetch({uri, loadUsingSystemPrincipal: true}, (inputStream, status) => {
|
||||||
if (!Components.isSuccessCode(status)) {
|
if (!Components.isSuccessCode(status)) {
|
||||||
reject(status);
|
reject(new Error(status));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let text = NetUtil.readInputStreamToString(inputStream, inputStream.available());
|
|
||||||
try {
|
try {
|
||||||
|
let text = NetUtil.readInputStreamToString(inputStream, inputStream.available());
|
||||||
resolve(JSON.parse(text));
|
resolve(JSON.parse(text));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
reject(e);
|
reject(e);
|
||||||
@ -420,69 +543,108 @@ ExtensionData.prototype = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Reads the extension's |manifest.json| file, and stores its
|
||||||
|
// parsed contents in |this.manifest|.
|
||||||
readManifest() {
|
readManifest() {
|
||||||
let manifestURI = Services.io.newURI("manifest.json", null, this.baseURI);
|
return this.readJSON("manifest.json").then(manifest => {
|
||||||
return this.readJSON(manifestURI);
|
this.manifest = manifest;
|
||||||
},
|
|
||||||
|
|
||||||
readLocaleFile(locale) {
|
|
||||||
let dir = locale.replace("-", "_");
|
|
||||||
let url = `_locales/${dir}/messages.json`;
|
|
||||||
let uri = Services.io.newURI(url, null, this.baseURI);
|
|
||||||
return this.readJSON(uri);
|
|
||||||
},
|
|
||||||
|
|
||||||
readLocaleMessages() {
|
|
||||||
let locales = [];
|
|
||||||
|
|
||||||
// We need to base this off of this.addonData.resourceURI rather
|
|
||||||
// than baseURI since baseURI is a moz-extension URI, which always
|
|
||||||
// QIs to nsIFileURL.
|
|
||||||
let uri = Services.io.newURI("_locales", null, this.addonData.resourceURI);
|
|
||||||
if (uri instanceof Ci.nsIFileURL) {
|
|
||||||
let file = uri.file;
|
|
||||||
let enumerator;
|
|
||||||
try {
|
try {
|
||||||
enumerator = file.directoryEntries;
|
this.id = this.manifest.applications.gecko.id;
|
||||||
} catch (e) {
|
} catch (e) {}
|
||||||
return {};
|
|
||||||
}
|
if (typeof this.id != "string") {
|
||||||
while (enumerator.hasMoreElements()) {
|
this.manifestError("Missing required `applications.gecko.id` property");
|
||||||
let file = enumerator.getNext().QueryInterface(Ci.nsIFile);
|
|
||||||
locales.push({
|
|
||||||
name: file.leafName,
|
|
||||||
locales: [file.leafName.replace("_", "-")]
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return manifest;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// Reads the locale file for the given Gecko-compatible locale code, and
|
||||||
|
// stores its parsed contents in |this.localeMessages.get(locale)|.
|
||||||
|
readLocaleFile: Task.async(function* (locale) {
|
||||||
|
let locales = yield this.promiseLocales();
|
||||||
|
let dir = locales.get(locale);
|
||||||
|
let file = `_locales/${dir}/messages.json`;
|
||||||
|
|
||||||
|
let messages = {};
|
||||||
|
try {
|
||||||
|
messages = yield this.readJSON(file);
|
||||||
|
} catch (e) {
|
||||||
|
this.packagingError(`Loading locale file ${file}: ${e}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (uri instanceof Ci.nsIJARURI && uri.JARFile instanceof Ci.nsIFileURL) {
|
this.localeMessages.set(locale, messages);
|
||||||
let file = uri.JARFile.file;
|
return messages;
|
||||||
let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].createInstance(Ci.nsIZipReader);
|
}),
|
||||||
try {
|
|
||||||
zipReader.open(file);
|
// Reads the list of locales available in the extension, and returns a
|
||||||
let enumerator = zipReader.findEntries("_locales/*");
|
// Promise which resolves to a Map upon completion.
|
||||||
while (enumerator.hasMore()) {
|
// Each map key is a Gecko-compatible locale code, and each value is the
|
||||||
let name = enumerator.getNext();
|
// "_locales" subdirectory containing that locale:
|
||||||
let match = name.match(new RegExp("_locales\/([^/]*)"));
|
//
|
||||||
if (match && match[1]) {
|
// Map(gecko-locale-code -> locale-directory-name)
|
||||||
locales.push({
|
promiseLocales() {
|
||||||
name: match[1],
|
if (!this._promiseLocales) {
|
||||||
locales: [match[1].replace("_", "-")]
|
this._promiseLocales = Task.spawn(function* () {
|
||||||
});
|
let locales = new Map();
|
||||||
|
|
||||||
|
let entries = yield this.readDirectory("_locales");
|
||||||
|
for (let file of entries) {
|
||||||
|
if (file.isDir) {
|
||||||
|
let locale = this.normalizeLocaleCode(file.name);
|
||||||
|
locales.set(locale, file.name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
zipReader.close();
|
return locales;
|
||||||
}
|
}.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
let locale = Locale.findClosestLocale(locales);
|
return this._promiseLocales;
|
||||||
if (locale) {
|
},
|
||||||
return this.readLocaleFile(locale.name).catch(() => {});
|
|
||||||
|
// Reads the locale messages for all locales, and returns a promise which
|
||||||
|
// resolves to a Map of locale messages upon completion. Each key in the map
|
||||||
|
// is a Gecko-compatible locale code, and each value is a locale data object
|
||||||
|
// as returned by |readLocaleFile|.
|
||||||
|
initAllLocales: Task.async(function* () {
|
||||||
|
let locales = yield this.promiseLocales();
|
||||||
|
|
||||||
|
yield Promise.all(Array.from(locales.keys(),
|
||||||
|
locale => this.readLocaleFile(locale)));
|
||||||
|
|
||||||
|
let defaultLocale = this.defaultLocale;
|
||||||
|
if (defaultLocale) {
|
||||||
|
if (!locales.has(defaultLocale)) {
|
||||||
|
this.manifestError('Value for "default_locale" property must correspond to ' +
|
||||||
|
'a directory in "_locales/". Not found: ' +
|
||||||
|
JSON.stringify(`_locales/${default_locale}/`));
|
||||||
|
}
|
||||||
|
} else if (this.localeMessages.size) {
|
||||||
|
this.manifestError('The "default_locale" property is required when a ' +
|
||||||
|
'"_locales/" directory is present.');
|
||||||
}
|
}
|
||||||
return {};
|
|
||||||
}
|
return this.localeMessages;
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Reads the locale file for the given Gecko-compatible locale code, or the
|
||||||
|
// default locale if no locale code is given, and sets it as the currently
|
||||||
|
// selected locale on success.
|
||||||
|
//
|
||||||
|
// If no locales are unavailable, resolves to |null|.
|
||||||
|
initLocale: Task.async(function* (locale = this.defaultLocale) {
|
||||||
|
if (locale == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let localeData = yield this.readLocaleFile(locale);
|
||||||
|
|
||||||
|
this.selectedLocale = locale;
|
||||||
|
return localeData;
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
// We create one instance of this class per extension. |addonData|
|
// We create one instance of this class per extension. |addonData|
|
||||||
@ -506,7 +668,6 @@ this.Extension = function(addonData)
|
|||||||
this.id = addonData.id;
|
this.id = addonData.id;
|
||||||
this.baseURI = Services.io.newURI("moz-extension://" + uuid, null, null);
|
this.baseURI = Services.io.newURI("moz-extension://" + uuid, null, null);
|
||||||
this.baseURI.QueryInterface(Ci.nsIURL);
|
this.baseURI.QueryInterface(Ci.nsIURL);
|
||||||
this.logger = Log.repository.getLogger(LOGGER_ID_BASE + this.id.replace(/\./g, "-"));
|
|
||||||
this.principal = this.createPrincipal();
|
this.principal = this.createPrincipal();
|
||||||
|
|
||||||
this.views = new Set();
|
this.views = new Set();
|
||||||
@ -668,11 +829,6 @@ Extension.prototype = extend(Object.create(ExtensionData.prototype), {
|
|||||||
return common == this.baseURI.spec;
|
return common == this.baseURI.spec;
|
||||||
},
|
},
|
||||||
|
|
||||||
// Report an error about the extension's manifest file.
|
|
||||||
manifestError(message) {
|
|
||||||
this.logger.error(`Loading extension '${this.id}': ${message}`);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Representation of the extension to send to content
|
// Representation of the extension to send to content
|
||||||
// processes. This should include anything the content process might
|
// processes. This should include anything the content process might
|
||||||
// need.
|
// need.
|
||||||
@ -745,6 +901,24 @@ Extension.prototype = extend(Object.create(ExtensionData.prototype), {
|
|||||||
this.onShutdown.delete(obj);
|
this.onShutdown.delete(obj);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Reads the locale file for the given Gecko-compatible locale code, or if
|
||||||
|
// no locale is given, the available locale closest to the UI locale.
|
||||||
|
// Sets the currently selected locale on success.
|
||||||
|
initLocale: Task.async(function* (locale = undefined) {
|
||||||
|
if (locale === undefined) {
|
||||||
|
let locales = yield this.promiseLocales();
|
||||||
|
|
||||||
|
let localeList = Object.keys(locales).map(locale => {
|
||||||
|
return { name: locale, locales: [locale] };
|
||||||
|
});
|
||||||
|
|
||||||
|
let match = Locale.findClosestLocale(localeList);
|
||||||
|
locale = match ? match.name : this.defaultLocale;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ExtensionData.prototype.initLocale.call(this, locale);
|
||||||
|
}),
|
||||||
|
|
||||||
startup() {
|
startup() {
|
||||||
try {
|
try {
|
||||||
ExtensionManagement.startupExtension(this.uuid, this.addonData.resourceURI, this);
|
ExtensionManagement.startupExtension(this.uuid, this.addonData.resourceURI, this);
|
||||||
@ -752,21 +926,20 @@ Extension.prototype = extend(Object.create(ExtensionData.prototype), {
|
|||||||
return Promise.reject(e);
|
return Promise.reject(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.all([this.readManifest(), this.readLocaleMessages()]).then(([manifest, messages]) => {
|
return this.readManifest().then(() => {
|
||||||
|
return this.initLocale();
|
||||||
|
}).then(() => {
|
||||||
if (this.hasShutdown) {
|
if (this.hasShutdown) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
GlobalManager.init(this);
|
GlobalManager.init(this);
|
||||||
|
|
||||||
this.manifest = manifest;
|
|
||||||
this.localeMessages = messages;
|
|
||||||
|
|
||||||
Management.emit("startup", this);
|
Management.emit("startup", this);
|
||||||
|
|
||||||
return this.runManifest(manifest);
|
return this.runManifest(this.manifest);
|
||||||
}).catch(e => {
|
}).catch(e => {
|
||||||
dump(`Extension error: ${e} ${e.fileName}:${e.lineNumber}\n`);
|
dump(`Extension error: ${e} ${e.filename}:${e.lineNumber}\n`);
|
||||||
Cu.reportError(e);
|
Cu.reportError(e);
|
||||||
throw e;
|
throw e;
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user