summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPhilip Chimento <philip@endlessm.com>2015-05-19 13:49:57 -0700
committerPhilip Chimento <philip@endlessm.com>2015-05-21 13:26:48 -0700
commitb8d6310bfd7af2cece81c0b7e7fc71711a991871 (patch)
tree9059cef91139c3ac5a1cfdb68e242af5692c5f4e
parenta58f5454c8b046ba4f17534a88db1c19ac22b822 (diff)
Expose ngettext() to client-side JS
This exposes the function set by webhelper.set_ngettext() to the client- side Javascript as a ngettext() function, defined on the global window object. This allows apps to translate messages that need to be separated into singular and plural, just like the C ngettext() function. [endlessm/eos-sdk#291]
-rw-r--r--test/smoke-tests/webhelper/webview2.js21
-rw-r--r--test/webhelper/testTranslate2.js53
-rw-r--r--webhelper/webextensions/wh2extension.c113
-rw-r--r--webhelper/webhelper2.js61
4 files changed, 240 insertions, 8 deletions
diff --git a/test/smoke-tests/webhelper/webview2.js b/test/smoke-tests/webhelper/webview2.js
index 03ed560..f50c42d 100644
--- a/test/smoke-tests/webhelper/webview2.js
+++ b/test/smoke-tests/webhelper/webview2.js
@@ -25,6 +25,16 @@ body { \
</head> \
\
<body> \
+<script type="text/javascript"> \
+ window.ngettextButton = (function () { \
+ var times = 0; \
+ return function () { \
+ times++; \
+ var message = ngettext("You have clicked me __ time", "You have clicked me __ times", times); \
+ alert(message.replace("__", times.toString())); \
+ }; \
+ })(); \
+</script> \
<h1>First page</h1> \
\
<p><a href="webhelper://moveToPage?name=page2">Move to page 2</a></p> \
@@ -43,9 +53,12 @@ message from parameter in this URL</a></p> \
<p>This is text that will be italicized: <span name="translatable">Hello, \
world!</span></p> \
\
-<p><button onclick="alert(gettext(\'I came from gettext\'));"> \
- Click me to use gettext \
-</button></p> \
+<p> \
+ <button onclick="alert(gettext(\'I came from gettext\'));"> \
+ Click me to use gettext \
+ </button> \
+ <button onclick="ngettextButton();">Click me to use ngettext</button> \
+</p> \
\
</body> \
</html>';
@@ -66,6 +79,8 @@ const TestApplication = new Lang.Class({
this.parent();
this._webhelper.set_gettext((s) => s.italics());
+ this._webhelper.set_ngettext((s, p, n) =>
+ '** ' + (n == 1 ? s : p) + ' **');
this._webhelper.define_web_actions({
moveToPage: this.moveToPage.bind(this),
showMessageFromParameter: this.showMessageFromParameter.bind(this),
diff --git a/test/webhelper/testTranslate2.js b/test/webhelper/testTranslate2.js
index 852c3a1..b34c4cf 100644
--- a/test/webhelper/testTranslate2.js
+++ b/test/webhelper/testTranslate2.js
@@ -63,6 +63,29 @@ describe('WebHelper2 translator', function () {
expect(webhelper.get_gettext()).toBeNull();
});
+ it('complains about a bad ngettext function', function () {
+ expect(function () {
+ webhelper.set_ngettext('I am not a function');
+ }).toThrow();
+ });
+
+ it('gets and sets the ngettext function', function () {
+ let translation_function = (s, p, n) => n == 1 ? s : p;
+ webhelper.set_ngettext(translation_function);
+ expect(webhelper.get_ngettext()).toBe(translation_function);
+ });
+
+ it('has a null ngettext function by default', function () {
+ expect(webhelper.get_ngettext()).toBeNull();
+ });
+
+ it('can remove the ngettext function by setting null', function () {
+ webhelper.set_ngettext((s, p, n) => n == 1 ? s : p);
+ expect(webhelper.get_ngettext()).not.toBeNull();
+ webhelper.set_ngettext(null);
+ expect(webhelper.get_ngettext()).toBeNull();
+ });
+
describe('translating a page', function () {
let webview;
@@ -118,18 +141,38 @@ describe('WebHelper2 translator', function () {
});
describe('used from client-side Javascript', function () {
- it('translates a string', function (done) {
- let webview = new WebKit2.WebView();
+ let webview;
+
+ beforeEach(function () {
+ webview = new WebKit2.WebView();
+ });
+
+ function load_script(view, script) {
+ view.load_html('<html><body><script type="text/javascript">' +
+ script + '</script></body></html>', null);
+ Mainloop.run('webhelper2');
+ }
+
+ it('translates a string with gettext()', function (done) {
let gettext_spy = jasmine.createSpy('gettext_spy').and.callFake((s) => {
Mainloop.quit('webhelper2');
return s;
});
webhelper.set_gettext(gettext_spy);
- webview.load_html('<html><body><script type="text/javascript">gettext("Translate Me");</script></body></html>',
- null);
- Mainloop.run('webhelper2');
+ load_script(webview, 'gettext("Translate Me");');
expect(gettext_spy).toHaveBeenCalledWith('Translate Me');
done();
});
+
+ it('translates a string with ngettext()', function (done) {
+ let ngettext_spy = jasmine.createSpy('ngettext_spy').and.callFake((s, p, n) => {
+ Mainloop.quit('webhelper2');
+ return n == 1 ? s : p;
+ });
+ webhelper.set_ngettext(ngettext_spy);
+ load_script(webview, 'ngettext("File", "Files", 3);');
+ expect(ngettext_spy).toHaveBeenCalledWith('File', 'Files', 3);
+ done();
+ });
});
});
diff --git a/webhelper/webextensions/wh2extension.c b/webhelper/webextensions/wh2extension.c
index 48ea9aa..4da7ae4 100644
--- a/webhelper/webextensions/wh2extension.c
+++ b/webhelper/webextensions/wh2extension.c
@@ -2,6 +2,7 @@
/* Copyright 2015 Endless Mobile, Inc. */
+#include <math.h>
#include <string.h>
#include <glib.h>
@@ -78,6 +79,36 @@ translation_function (const gchar *message,
return retval;
}
+static gchar *
+ngettext_translation_function (const gchar *singular,
+ const gchar *plural,
+ gulong number,
+ Context *ctxt)
+{
+ GError *error = NULL;
+ GVariant *result =
+ g_dbus_connection_call_sync (ctxt->connection, ctxt->main_program_name,
+ MAIN_PROGRAM_OBJECT_PATH,
+ MAIN_PROGRAM_INTERFACE_NAME, "NGettext",
+ g_variant_new ("(sst)", singular, plural,
+ number),
+ (GVariantType *) "(s)",
+ G_DBUS_CALL_FLAGS_NO_AUTO_START,
+ -1 /* timeout */, NULL /* cancellable */,
+ &error);
+ if (result == NULL)
+ {
+ g_warning ("No return value from ngettext: %s", error->message);
+ g_clear_error (&error);
+ return g_strdup (number == 1 ? singular : plural);
+ }
+
+ gchar *retval;
+ g_variant_get (result, "(s)", &retval);
+ g_variant_unref (result);
+ return retval;
+}
+
static JSValueRef
gettext_shim (JSContextRef js,
JSObjectRef function,
@@ -123,6 +154,82 @@ gettext_shim (JSContextRef js,
return retval;
}
+static JSValueRef
+ngettext_shim (JSContextRef js,
+ JSObjectRef function,
+ JSObjectRef this_object,
+ size_t n_args,
+ const JSValueRef args[],
+ JSValueRef *exception)
+{
+ if (n_args != 3)
+ {
+ gchar *errmsg = g_strdup_printf ("Expected three arguments to ngettext(),"
+ "but got %d.", n_args);
+ *exception = throw_exception (js, errmsg);
+ g_free (errmsg);
+ return NULL;
+ }
+ if (!JSValueIsString (js, args[0]))
+ {
+ *exception = throw_exception (js, "The first argument to ngettext() "
+ "must be a string.");
+ return NULL;
+ }
+ if (!JSValueIsString (js, args[1]))
+ {
+ *exception = throw_exception (js, "The second argument to ngettext() "
+ "must be a string.");
+ return NULL;
+ }
+ if (!JSValueIsNumber (js, args[2]))
+ {
+ *exception = throw_exception (js, "The third argument to ngettext() "
+ "must be a number.");
+ return NULL;
+ }
+
+ JSObjectRef window = JSContextGetGlobalObject (js);
+ JSStringRef private_name = JSStringCreateWithUTF8CString (PRIVATE_NAME);
+ JSValueRef private_data = JSObjectGetProperty (js, window, private_name,
+ exception);
+ if (JSValueIsUndefined (js, private_data))
+ return NULL; /* propagate exception */
+ Context *ctxt = (Context *) JSObjectGetPrivate ((JSObjectRef) private_data);
+
+ JSStringRef singular_ref = JSValueToStringCopy (js, args[0], exception);
+ if (singular_ref == NULL)
+ return NULL; /* propagate exception */
+ gchar *singular_msg = string_ref_to_string (singular_ref);
+ JSStringRelease (singular_ref);
+
+ JSStringRef plural_ref = JSValueToStringCopy (js, args[1], exception);
+ if (plural_ref == NULL)
+ {
+ g_free (singular_msg);
+ return NULL; /* propagate exception */
+ }
+ gchar *plural_msg = string_ref_to_string (plural_ref);
+ JSStringRelease (plural_ref);
+
+ double number = JSValueToNumber (js, args[2], exception);
+ if (isnan (number))
+ {
+ g_free (singular_msg);
+ g_free (plural_msg);
+ return NULL; /* propagate exception */
+ }
+
+ gchar *translation = ngettext_translation_function (singular_msg, plural_msg,
+ (gulong) number, ctxt);
+ g_free (singular_msg);
+ g_free (plural_msg);
+
+ JSValueRef retval = string_to_value_ref (js, translation);
+ g_free (translation);
+ return retval;
+}
+
static void
translate_html (WebKitDOMDocument *dom,
Context *ctxt)
@@ -298,6 +405,12 @@ on_window_object_cleared (WebKitScriptWorld *script_world,
if (!set_object_property (js, window, "gettext", (JSValueRef) gettext_func,
kJSPropertyAttributeNone))
return;
+
+ JSObjectRef ngettext_func =
+ JSObjectMakeFunctionWithCallback (js, NULL, ngettext_shim);
+ if (!set_object_property (js, window, "ngettext", (JSValueRef) ngettext_func,
+ kJSPropertyAttributeNone))
+ return;
}
static void
diff --git a/webhelper/webhelper2.js b/webhelper/webhelper2.js
index bba4c63..b5f87f8 100644
--- a/webhelper/webhelper2.js
+++ b/webhelper/webhelper2.js
@@ -29,6 +29,12 @@ const WH2_DBUS_MAIN_PROGRAM_INTERFACE = '\
<arg name="message" type="s" direction="in"/> \
<arg name="translation" type="s" direction="out"/> \
</method> \
+ <method name="NGettext"> \
+ <arg name="message_singular" type="s" direction="in"/> \
+ <arg name="message_plural" type="s" direction="in"/> \
+ <arg name="number" type="t" direction="in"/> \
+ <arg name="translation" type="s" direction="out"/> \
+ </method> \
</interface> \
</node>';
@@ -164,6 +170,7 @@ const WebHelper = new Lang.Class({
_init: function (props={}) {
this._web_actions = {};
this._gettext = null;
+ this._ngettext = null;
this._ProxyConstructor =
Gio.DBusProxy.makeProxyWrapper(WH2_DBUS_EXTENSION_INTERFACE);
this.parent(props);
@@ -239,6 +246,10 @@ const WebHelper = new Lang.Class({
return this._gettext(string);
},
+ NGettext: function (singular, plural, number) {
+ return this._ngettext(singular, plural, number);
+ },
+
// Public API
/**
@@ -290,6 +301,56 @@ const WebHelper = new Lang.Class({
},
/**
+ * Method: set_ngettext
+ * Define function which translates singular/plural text
+ *
+ * Parameters:
+ * ngettext_func - a function, or null
+ *
+ * When you plan to translate text in your web application, set this
+ * property to the translation function.
+ * The function must take three parameters: a string singular message, a
+ * string plural message, and a number for which to generate the message.
+ * The function must return a string.
+ * The canonical example is ngettext().
+ *
+ * This function is made available directly to the browser-side Javascript
+ * as *ngettext()*, a property of the global object.
+ *
+ * Pass null for _ngettext_func_ to unset the translation function.
+ *
+ * If this function has not been called, or has most recently been called
+ * with null, then it is illegal to call *ngettext()* in the client-side
+ * Javascript.
+ *
+ * Example:
+ * > const WebHelper2 = imports.webhelper2;
+ * > const Gettext = imports.gettext;
+ * > const GETTEXT_DOMAIN = 'smoke-grinder';
+ * >
+ * > let webhelper = new WebHelper2.WebHelper('com.example.SmokeGrinder');
+ * > webhelper.set_gettext(Gettext.dngettext.bind(null, GETTEXT_DOMAIN));
+ */
+ set_ngettext: function (ngettext_func) {
+ if (ngettext_func !== null && typeof ngettext_func !== 'function')
+ throw new Error('The translation function must be a function, or ' +
+ 'null.');
+ this._ngettext = ngettext_func;
+ },
+
+ /**
+ * Method: get_ngettext
+ * Retrieve the currently set singular/plural translation function
+ *
+ * Returns:
+ * the translation function previously set with <set_ngettext()>, or null
+ * if none is currently set.
+ */
+ get_ngettext: function () {
+ return this._ngettext;
+ },
+
+ /**
* Method: translate_html
* Translate the HTML page in a webview asynchronously
*