Skip to content

Commit

Permalink
Initial localization support.
Browse files Browse the repository at this point in the history
This adds support for localized script description/name.

Refs greasemonkey#1963.
  • Loading branch information
Ventero committed Jul 27, 2014
1 parent 4ea165f commit 41c0f27
Show file tree
Hide file tree
Showing 9 changed files with 126 additions and 10 deletions.
2 changes: 1 addition & 1 deletion content/browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ function GM_showPopup(aEvent) {
function appendScriptAfter(script, point) {
if (script.needsUninstall) return;
var mi = document.createElement("menuitem");
mi.setAttribute("label", script.name);
mi.setAttribute("label", script.localized.name);
mi.script = script;
mi.setAttribute("type", "checkbox");
mi.setAttribute("checked", script.enabled.toString());
Expand Down
4 changes: 2 additions & 2 deletions content/install.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,12 @@ function init() {

var desc = document.getElementById('scriptDescription');
desc.appendChild(document.createElementNS(gHtmlNs, 'strong'));
desc.firstChild.appendChild(document.createTextNode(gScript.name));
desc.firstChild.appendChild(document.createTextNode(gScript.localized.name));
if (gScript.version) {
desc.appendChild(document.createTextNode(' ' + gScript.version));
}
desc.appendChild(document.createElementNS(gHtmlNs, 'br'));
desc.appendChild(document.createTextNode(gScript.description));
desc.appendChild(document.createTextNode(gScript.localized.description));

if (gRemoteScript.done) {
// Download finished before we could open, fake a progress event.
Expand Down
2 changes: 1 addition & 1 deletion content/scriptprefs.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ var gUserTabEl;

window.addEventListener('load', function() {
// I wanted "%s" but % is reserved in a DTD and I don't know the literal.
document.title = document.title.replace('!!', gScript.name);
document.title = document.title.replace('!!', gScript.localized.name);

var gTabboxEl = document.getElementsByTagName('tabbox')[0];
gUserTabEl = gTabboxEl.tabs.getItemAtIndex(0);
Expand Down
6 changes: 3 additions & 3 deletions modules/addons4.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,9 @@ function ScriptAddon(aScript) {

this.id = aScript.id + SCRIPT_ID_SUFFIX;
this.forceUpdate = false;
this.name = this._script.name;
this.name = this._script.localized.name;
this.version = this._script.version;
this.description = this._script.description;
this.description = this._script.localized.decription;

This comment has been minimized.

Copy link
@wenketel

wenketel Aug 30, 2014

decription→description

This comment has been minimized.

Copy link
@Ventero

Ventero Aug 30, 2014

Author Owner

Well spotted, thanks! greasemonkey#2006

this.iconURL = this._script.icon && this._script.icon.fileURL;
this.updateDate = this._script.modifiedDate;
this.providesUpdatesSecurely = aScript.updateIsSecure;
Expand Down Expand Up @@ -255,7 +255,7 @@ function ScriptInstallFactoryByAddon(aAddon) {
function ScriptInstall(aAddon) {
var newScript = aAddon._script.availableUpdate;
this.iconURL = newScript.icon.fileURL;
this.name = newScript.name;
this.name = newScript.localized.name;
this.version = newScript.version;

this._script = aAddon._script;
Expand Down
13 changes: 11 additions & 2 deletions modules/parseScript.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ var gIoService = Components.classes["@mozilla.org/network/io-service;1"]
var gLineSplitRegexp = /.+/g;
var gAllMetaRegexp = new RegExp(
'^// ==UserScript==([\\s\\S]*?)^// ==/UserScript==', 'm');
var gMetaLineRegexp = new RegExp('// @(\\S+)(?:\\s+(.*))?');
var gMetaLineRegexp = new RegExp('// @([^\\s:]+)(?::([a-zA-Z-]+))?(?:\\s+(.*))?');

This comment has been minimized.

Copy link
@Martii

Martii Jul 27, 2014

I know OUJS doesn't do this currently with this exact key... but doing a "what if" scenario...

What if we (or anyone) support // @oujs:name:de-DE? How might this kill GM?


Another good question: Is GM going to need @name or can authors just omit that altogether?

Refs: greasemonkey#1963

This comment has been minimized.

Copy link
@Ventero

Ventero Jul 27, 2014

Author Owner

Not sure what you mean by "kill GM". If I'm not mistaken, a metadata line like that would be parsed as header type oujs with name as potential locale part and no value. Since GM doesn't care about header type oujs, it would simply be ignored.

Admittedly, the fact that this is slightly misparsed isn't ideal, so maybe the regexp should be changed to something like new RegExp('// @(\\S+?)(?::([a-zA-Z-]+))?(?:\\s+(.*))?$') (which would parse this correctly as header type oujs:name with locale de-DE). But either way, you can always construct a case that isn't parsed the way it should be.

This comment has been minimized.

Copy link
@Martii

Martii Jul 28, 2014

But either way, you can always construct a case that isn't parsed the way it should be

Agreed... usually those would just be ignored I think. This could work with quite a bit of extra work in the future.

I know the popular vote is for another : but I was hoping for some consideration with the following fictitious use case code and output:

var re = /\/\/ @(?:(\S+?):)?(\S+?)(?:#([a-zA-Z-]+))?(?:\s+(.*))?$/;

console.log('// @run-at document-end'.match(re));
console.log('// @run-at#fr-FR document-end'.match(re));
console.log('// @oujs:run-at document-end'.match(re));
console.log('// @oujs:run-at#fr-FR document-end'.match(re));
console.log('// @run-at#fr-FR'.match(re));
console.log('// @run-at'.match(re));
console.log('// @oujs:run-at#fr-FR'.match(re));
console.log('// @oujs:run-at'.match(re));
Array [ "// @run-at document-end",            undefined, "run-at", undefined, "document-end" ] Scratchpad/1:3
Array [ "// @run-at#fr-FR document-end",      undefined, "run-at", "fr-FR",   "document-end" ] Scratchpad/1:4
Array [ "// @oujs:run-at document-end",       "oujs",    "run-at", undefined, "document-end" ] Scratchpad/1:5
Array [ "// @oujs:run-at#fr-FR document-end", "oujs",    "run-at", "fr-FR",   "document-end" ] Scratchpad/1:6
Array [ "// @run-at#fr-FR",                   undefined, "run-at", "fr-FR",   undefined      ] Scratchpad/1:7
Array [ "// @run-at",                         undefined, "run-at", undefined, undefined      ] Scratchpad/1:8
Array [ "// @oujs:run-at#fr-FR",              "oujs",    "run-at", "fr-FR",   undefined      ] Scratchpad/1:9
Array [ "// @oujs:run-at",                    "oujs",    "run-at", undefined, undefined      ] Scratchpad/1:10

Notice how everything lines up and there is zero additional coding, that I can tell, to handle site/project prefixing and localization of all the keys. This methodology should be both forwards and backwards compatible. Am I thinking incorrectly here? Is there a better way? Thanks for your assistance and patience.

This comment has been minimized.

Copy link
@Ventero

Ventero Jul 29, 2014

Author Owner

I don't see why GM should take special care of properly parsing headers it doesn't care about. There's no backwards compatibility to consider on our end, as all metadata keys GM ever supported contain only alphabetic characters.

If your concern is that using : for localization makes it harder for external applications/libraries to parse the metadata, then you should raise that issue in the original bug report, so that it can be discussed further. But if we take colons in metadata keys as some kind of "attribute specifier" (i.e. name:is-IS is the is-IS attribute of the name), then in my opinion it makes sense to specifically use the character that's already being used in that manner. That way, if GM ever decids to parse all metadata keys, the keys could just be parsed as // @key:subkey1:subkey2:.... This would lead to all oujs:... keys being grouped together, as they should.

Either way, this discussion should probably be continued in greasemonkey#1963.

This comment has been minimized.

Copy link
@arantius

arantius Jul 29, 2014

As Ventero's first reply says, and I agree, from GM's perspective this is moot. Invalid (i.e. non-matching, today) metadata lines are simply ignored.

Aside from that, I already independently thought that this particular line will need re-visiting. I love regular expressions, but this starts to pass beyond the threshold of readable.

var gStringBundle = Components
.classes["@mozilla.org/intl/stringbundle;1"]
.getService(Components.interfaces.nsIStringBundleService)
Expand Down Expand Up @@ -55,11 +55,20 @@ function parse(aSource, aUri, aFailWhenMissing, aNoMetaOk) {
if (!match) continue;

var header = match[1];
var value = match[2] || null;
var locale = match[2];
var value = match[3] || null;

switch (header) {
case 'description':
case 'name':
if (locale) {
if (!script._locales[locale])
script._locales[locale] = {};

script._locales[locale][header] = value;
break;
}
// fall-through if no locale given
case 'namespace':
case 'version':
case 'updateMetaStatus':
Expand Down
2 changes: 1 addition & 1 deletion modules/remoteScript.js
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ RemoteScript.prototype.install = function(aOldScript, aOnlyDependencies) {
// Let the user know we're all done.
if (!this._silent) {
GM_notification(
"'" + this.script.name + "' "
"'" + this.script.localized.name + "' "
+ stringBundleBrowser.GetStringFromName(this.messageName),
this.messageName);
}
Expand Down
58 changes: 58 additions & 0 deletions modules/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ function Script(configNode) {
this._id = null;
this._installTime = null;
this._includes = [];
// All available localized properties.
this._locales = {};
// The best localized matches for the current browser locale.
this._localized = null;
this._matches = [];
this._modifiedTime = null;
this._name = 'user-script';
Expand Down Expand Up @@ -139,6 +143,35 @@ function Script_getDependencies() {
Script.prototype.__defineGetter__('description',
function Script_getDescription() { return this._description; });

Script.prototype.__defineGetter__('localized',
function Script_getLocalizedDescription() {
// We can't simply return this._locales[locale], as the best match for name
// and description might be for different locales (e.g. if an exact match is
// only provided for one of them).
if (!this._localized) {
var locales = this._locales;
var preferred = GM_util.getPreferredLocale();

function getBestLocalization(aProp) {
var available = Object.keys(locales).filter(function(locale) {
return !!locales[locale][aProp];
});

var bestMatch = GM_util.getBestLocaleMatch(preferred, available);
if (!bestMatch) return null;

return locales[bestMatch][aProp];
}

this._localized = {
description: getBestLocalization("description") || this._description,
name: getBestLocalization("name") || this._name
};
}

return this._localized
});

Script.prototype.__defineGetter__('downloadURL',
function Script_getDownloadUrl() { return this._downloadURL; });
Script.prototype.__defineSetter__('downloadURL',
Expand Down Expand Up @@ -342,6 +375,11 @@ Script.prototype._loadFromConfigNode = function(node) {
scriptResource._charset = childNode.getAttribute("charset");
this._resources.push(scriptResource);
break;
case "Name":
case "Description":
var lang = childNode.getAttribute("lang");
if (!this._locales[lang]) this._locales[lang] = {};
this._locales[lang][childNode.nodeName.toLowerCase()] = childNode.textContent;
}
}

Expand All @@ -363,6 +401,8 @@ Script.prototype.toConfigNode = function(doc) {
node.appendChild(doc.createTextNode(content));
scriptNode.appendChild(doc.createTextNode("\n\t\t"));
scriptNode.appendChild(node);

return node;
}

function addArrayNodes(aName, aArray) {
Expand All @@ -371,6 +411,11 @@ Script.prototype.toConfigNode = function(doc) {
}
}

function addLocaleNode(aName, aLang, aContent) {
var node = addNode(aName, aContent);
node.setAttribute("lang", lang);
}

addArrayNodes('Exclude', this._excludes);
addArrayNodes('Grant', this._grants);
addArrayNodes('Include', this._includes);
Expand Down Expand Up @@ -406,6 +451,15 @@ Script.prototype.toConfigNode = function(doc) {
scriptNode.appendChild(resourceNode);
}


for (var lang in this._locales) {
if (this._locales[lang].name)
addLocaleNode("Name", lang, this._locales[lang].name);

if (this._locales[lang].description)
addLocaleNode("Description", lang, this._locales[lang].description);
}

scriptNode.appendChild(doc.createTextNode("\n\t"));

scriptNode.setAttribute("basedir", this._basedir);
Expand Down Expand Up @@ -470,6 +524,8 @@ Script.prototype.info = function() {
'excludes': this.excludes,
// 'icon': ???,
'includes': this.includes,
'localizedDescription': this.localized.description,
'localizedName': this.localized.name,
'matches': matches,
'name': this.name,
'namespace': this.namespace,
Expand Down Expand Up @@ -559,6 +615,8 @@ Script.prototype.updateFromNewScript = function(newScript, safeWin) {
this._includes = newScript._includes;
this._matches = newScript._matches;
this._description = newScript._description;
this._localized = newScript._localized;
this._locales = newScript._locales;
this._runAt = newScript._runAt;
this._version = newScript._version;
this.downloadURL = newScript.downloadURL;
Expand Down
32 changes: 32 additions & 0 deletions modules/util/getBestLocaleMatch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
Components.utils.import("resource://gre/modules/Services.jsm");
Components.utils.import('resource://greasemonkey/util.js');

const EXPORTED_SYMBOLS = ['getBestLocaleMatch'];

// This function tries to find the best matching locale.
// Locales should be given in the form "lang[-COUNTRY]".
// If an exact match (i.e. both lang and country match) can be found, it is
// returned. Otherwise, a partial match based on the lang part is attempted.
// Partial matches without country are preferred over lang matches with
// non-matching country.
// If no locale matches, null is returned.
function getBestLocaleMatch(aPreferred, aAvailable) {

This comment has been minimized.

Copy link
@JasonBarnabe

This comment has been minimized.

Copy link
@Ventero

Ventero Aug 5, 2014

Author Owner

I don't think that function allows me to specify what locale is preferred. Instead it simply takes an Accept-Language-header like string and returns the locale with the highest preference/"quality" from that list. E.g., even though my browser's locale is de-DE, localeService.getLocaleFromAcceptLanguage("en,de-DE,de").getCategory("NSILOCALE_CTYPE") returns en.

This comment has been minimized.

Copy link
@Ventero

Ventero Aug 5, 2014

Author Owner

Sorry, forgot to add: I also couldn't find anything else in the various locale-related services that does something similar. If there is a function like that in the API and I just missed, I'd definitely want to use it!

var preferredLang = aPreferred.split("-")[0];

var langMatch, partialMatch = null;
for (var i = 0, current; current = aAvailable[i]; i++) {
// Both lang and country match
if (current == aPreferred)
return current;

if (current == preferredLang) {
// Only lang matches, no country
langMatch = current;
} else if (current.split("-")[0] == preferredLang) {
// Only lang matches, non-matching country
partialMatch = current;
}
}

return langMatch || partialMatch;
}
17 changes: 17 additions & 0 deletions modules/util/getPreferredLocale.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
Components.utils.import("resource://gre/modules/Services.jsm");
Components.utils.import('resource://greasemonkey/util.js');

const EXPORTED_SYMBOLS = ['getPreferredLocale'];

var preferredLocale = (function() {
var matchOS = Services.prefs.getBoolPref("intl.locale.matchOS");

if (matchOS)
return Services.locale.getLocaleComponentForUserAgent();

return Services.prefs.getCharPref("general.useragent.locale") || "en-US";
})();

function getPreferredLocale() {
return preferredLocale;
}

0 comments on commit 41c0f27

Please sign in to comment.