diff options
author | Philip Chimento <philip@endlessm.com> | 2015-05-19 13:49:57 -0700 |
---|---|---|
committer | Philip Chimento <philip@endlessm.com> | 2015-05-21 13:26:48 -0700 |
commit | b8d6310bfd7af2cece81c0b7e7fc71711a991871 (patch) | |
tree | 9059cef91139c3ac5a1cfdb68e242af5692c5f4e | |
parent | a58f5454c8b046ba4f17534a88db1c19ac22b822 (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.js | 21 | ||||
-rw-r--r-- | test/webhelper/testTranslate2.js | 53 | ||||
-rw-r--r-- | webhelper/webextensions/wh2extension.c | 113 | ||||
-rw-r--r-- | webhelper/webhelper2.js | 61 |
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 * |