Permalink
Browse files

Add new Locale i18n/l10n API to replace lang_api

Commit: 4deea44

This is some initial work towards replacing lang_api with a new Locale
API that makes use of PHP's gettext support for translation.

gettext offers the following advantages:
a) Greater performance
b) Better supporting tools for translation work
c) Import/export support at MediaWiki.net that doesn't require
MantisBT-specific hacks
d) Pluralisation (1 thing vs 1 things)
e) en-US language is embedded in the source code and acts as a key --
this makes the source code easier to read and sections of code easier to
find upon searching for UI strings
f) Easy plugin support via loading of new text domains
g) Ability to perform more specific translations (en-US, en-GB,
en-AU...) rather than "one English fits all"
h) Support for context aware translations

There is lot of negativity in the wild regarding gettext and PHP.
Typical arguments include:

1. Lack of thread safety - this is no longer an issue with php-fpm (or
earlier Fast CGI methods) because each script will execute in a separate
process. Threaded execution of PHP under mod_php (Apache) or an IIS
equivalent is thoroughly deprecated and not recommended for numerous
reasons.

2. gettext is difficult to install with PHP - Linux distributions of PHP
will most likely have gettext enabled by default. If not, packages are
readily available in all the major distribution repositories. Windows
users are given php_gettext.dll in the .zip distribution of PHP and
simple instructions at [1] can be followed to get things up and running
quickly.

3. The gettext API is "inconsistent" or doesn't support xyz - it's
trivial to create a new wrapper function around these APIs that allows
us to implement enhanced functionality or make the API easier to use.

Don't be tricked by the outdated negativity from the early 2000's.
gettext is an "industry standard" and is not going away any time soon.

[1] http://php.net/manual/en/install.windows.extensions.php
  • Loading branch information...
grangeway committed Oct 28, 2012
1 parent 6a165ba commit 159be15d300a0d44701daba25f69c8656570ee8c
@@ -0,0 +1,12 @@
+<?php
+namespace MantisBT\Exception\Locale;
+use MantisBT\Exception\ExceptionAbstract;
+
+class LocaleNotProvidedByUserAgent extends ExceptionAbstract {
+ public function __construct() {
+ $errorMessage = _('An Accept-Language header was expected from your browser, but none was received.');
+ parent::__construct($errorMessage);
+ /* TODO: check response code is the correct one to send */
+ $this->responseCode = 500;
+ }
+}
@@ -0,0 +1,13 @@
+<?php
+namespace MantisBT\Exception\Locale;
+use MantisBT\Exception\ExceptionAbstract;
+
+class LocalesNotSupported extends ExceptionAbstract {
+ public function __construct(array $locales) {
+ $localesAsString = implode(', ', $locales);
+ $errorMessage = sprintf(_('Could not set the current locale to any of the preferences provided: %1$s.'), $localesAsString);
+ parent::__construct($errorMessage);
+ /* TODO: check response code is the correct one to send */
+ $this->responseCode = 500;
+ }
+}
@@ -0,0 +1,116 @@
+<?php
+# MantisBT - A PHP based bugtracking system
+
+# MantisBT is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# MantisBT is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with MantisBT. If not, see <http://www.gnu.org/licenses/>.
+
+namespace MantisBT\Locale;
+use \Locale;
+use MantisBT\Exception\Locale\LocaleNotProvidedByUserAgent;
+use MantisBT\Exception\Locale\LocalesNotSupported;
+
+class LocaleManager {
+ private $textDomains = array();
+
+ public function __construct() {
+ /* Load some additional gettext-style functions (and aliases)
+ * that are missing from PHP's implementation. These functions
+ * are globally accessible.
+ */
+ if (!function_exists('pgettext')) {
+ require_once('locale_support_functions.php');
+ }
+ }
+
+ /* $newLocales is either a string (en_US.UTF-8) or an array of strings
+ * that are locale names to try. The order of the strings in the array
+ * is important as the first valid locale name in the array will be
+ * used.
+ */
+ public function setLocale($newLocales = null) {
+ if ($newLocales === null) {
+ if (!$_SERVER['HTTP_ACCEPT_LANGUAGE'])
+ throw new LocaleNotProvidedByUser();
+ $newLocales = Locale::acceptFromHttp($_SERVER['HTTP_ACCEPT_LANGUAGE']);
+ }
+ $result = setlocale(LC_ALL, $newLocales);
+ if ($result === false) {
+ if (!is_array($newLocales))
+ $newLocales = array($newLocales);
+ throw new LocalesNotSupported($newLocales);
+ }
+ return $result;
+ }
+
+ public function addTextDomain($textDomainName, $path) {
+ $this->textDomains[$textDomainName] = $path;
+ bindtextdomain($textDomainName, $path);
+ bind_textdomain_codeset($textDomainName, 'UTF-8');
+ }
+
+ /* Reads the Accept-Language header sent by the browser in the format
+ * specified in section 14.4 of RFC2616 and returns an array of
+ * RFC1766 formatted language tags and quality values (preferences),
+ * sorted from the most preferred language to the least preferred.
+ *
+ * If this header was not sent, contained no language preferences or
+ * only had a catch-all "*", this function will return an empty array.
+ *
+ * Based on Jesse Skinner's example at
+ * http://www.thefutureoftheweb.com/blog/use-accept-language-header
+ *
+ * @TODO: Cater for the "*" catch-all so we know with certainty whether
+ * the browser can accept any language as a fall back.
+ */
+ public function getParsedAcceptLanguageHeader() {
+ $languageTags = array();
+
+ /* Ensure that the browser has sent an Accept-Language header */
+ if (!isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
+ return $languageTags;
+ }
+
+ /* Split the Accept-Language header into components */
+ $languageTagParts = array();
+ preg_match_all('/([a-z]{1,8}(-[a-z]{1,8})?)\s*(;\s*q\s*=\s*(1|0\.[0-9]+))?/i', $_SERVER['HTTP_ACCEPT_LANGUAGE'], $languageTagParts);
+
+ /* Create an array of accepted language tags */
+ $languageTags = array_combine($languageTagParts[1], $languageTagParts[4]);
+ foreach ($languageTags as $languageRange => $qualityValue) {
+ /* If a quality value wasn't provided by the browser,
+ * use the default value of 1.0 */
+ $qualityValue = $qualityValue === '' ? 1.0 : (float)$qualityValue;
+ $languageTags[$languageRange] = $qualityValue;
+ }
+
+ /* Sort the array of language tags by quality value such that
+ * the most preferred language is the first element of the
+ * array. */
+ arsort($languageTags, SORT_NUMERIC);
+
+ return $languageTags;
+ }
+
+/* DEPRECATED CODE NO LONGER REQUIRED
+ $languageTagsAccepted = $this->getParsedAcceptLanguageHeader();
+ $newLocales = array();
+ foreach ($languageTagsAccepted as $languageRange => $qualityValue) {
+ $languageTagParts = array();
+ preg_match('/([a-z]{1,8})(-([a-z]{1,8}))?/i', $languageRange, $languageTagParts);
+ $newLocale = strtolower($languageTagParts[1]);
+ if (count($languageTagParts) === 4)
+ $newLocale .= '_' . strtoupper($languageTagParts[3]);
+ $newLocales[] = $newLocale . '.UTF-8';
+ }
+*/
+}
@@ -0,0 +1,105 @@
+<?php
+# MantisBT - A PHP based bugtracking system
+
+# MantisBT is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# MantisBT is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with MantisBT. If not, see <http://www.gnu.org/licenses/>.
+
+/* GNU gettext's pgettext function implemented in PHP. Upstream definition:
+ * http://git.savannah.gnu.org/cgit/gettext.git/tree/gnulib-local/lib/gettext.h
+ * pgettext(Msgctxt, Msgid)
+ */
+function pgettext($context, $message) {
+ $contextPrefixedMessage = "{$context}\004{$message}";
+ $translation = gettext($contextPrefixedMessage);
+ if ($translation === $contextPrefixedMessage)
+ return $message;
+ else
+ return $translation;
+}
+
+/* GNU gettext's npgettext function implemented in PHP. Upstream definition:
+ * http://git.savannah.gnu.org/cgit/gettext.git/tree/gnulib-local/lib/gettext.h
+ * npgettext(Msgctxt, Msgid, MsgidPlural, N)
+ */
+function npgettext($context, $messageSingular, $messagePlural, $number) {
+ $contextPrefixedMessageSingular = "{$context}\004{$messageSingular}";
+ $contextPrefixedMessagePlural = "{$context}\004{$messagePlural}";
+ $translation = ngettext($contextPrefixedMessageSingular, $contextPrefixedMessagePlural, $number);
+ if ($translation === $contextPrefixedMessageSingular || $translation === $contextPrefixedMessagePlural)
+ return $messageSingular;
+ else
+ return $translation;
+}
+
+/* GNU gettext's dpgettext function implemented in PHP. Upstream definition:
+ * http://git.savannah.gnu.org/cgit/gettext.git/tree/gnulib-local/lib/gettext.h
+ * dpgettext(Domainname, Msgctxt, Msgid)
+ */
+function dpgettext($domain, $context, $message) {
+ $contextPrefixedMessage = "{$context}\004{$message}";
+ $translation = dgettext($domain, $contextPrefixedMessage);
+ if ($translation === $contextPrefixedMessage)
+ return $message;
+ else
+ return $translation;
+}
+
+/* GNU gettext's dnpgettext function implemented in PHP. Upstream definition:
+ * http://git.savannah.gnu.org/cgit/gettext.git/tree/gnulib-local/lib/gettext.h
+ * dnpgettext(Domainname, Msgctxt, Msgid, MsgidPlural, N)
+ */
+function dnpgettext($domain, $context, $messageSingular, $messagePlural, $number) {
+ $contextPrefixedMessageSingular = "{$context}\004{$messageSingular}";
+ $contextPrefixedMessagePlural = "{$context}\004{$messagePlural}";
+ $translation = dngettext($domain, $contextPrefixedMessageSingular, $contextPrefixedMessagePlural, $number);
+ if ($translation === $contextPrefixedMessageSingular || $translation === $contextPrefixedMessagePlural)
+ return $messageSingular;
+ else
+ return $translation;
+}
+
+/* Shortcut wrapper for pgettext/gettext
+ */
+function ___($message, $context = null) {
+ if ($context !== null)
+ return pgettext($context, $message);
+ else
+ return gettext($message);
+}
+
+/* Shortcut wrapper for npgettext/ngettext
+ */
+function n___($messageSingular, $messagePlural, $number, $context = null) {
+ if ($context !== null)
+ return npgettext($context, $messageSingular, $messagePlural, $number);
+ else
+ return ngettext($messageSingular, $messagePlural, $number);
+}
+
+/* Shortcut wrapper for dpgettext/dgettext
+ */
+function d___($domain, $message, $context = null) {
+ if ($context !== null)
+ return dpgettext($domain, $context, $message);
+ else
+ return dgettext($domain, $message);
+}
+
+/* Shortcut wrapper for dnpgettext/dngettext
+ */
+function dn___($domain, $messageSingular, $messagePlural, $number, $context = null) {
+ if ($context !== null)
+ return dnpgettext($domain, $context, $messageSingular, $messagePlural, $number);
+ else
+ return dngettext($domain, $messageSingular, $messagePlural, $number);
+}

0 comments on commit 159be15

Please sign in to comment.