From a2af88a36e0439935399a086d895bcabb3f4b935 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Sat, 5 Sep 2015 02:24:56 +0200 Subject: [PATCH] initial work on translations, covering the PHP side of it --- css/bootstrap/zerobin.css | 4 + i18n/de.json | 78 ++++++++++++++ index.php | 1 + lib/RainTPL.php | 5 + lib/i18n.php | 219 ++++++++++++++++++++++++++++++++++++++ lib/zerobin.php | 45 ++++---- tpl/bootstrap.html | 38 ++++--- tpl/page.html | 34 +++--- tst/bootstrap.php | 1 + tst/i18n.php | 41 +++++++ 10 files changed, 409 insertions(+), 57 deletions(-) create mode 100644 i18n/de.json create mode 100644 lib/i18n.php create mode 100644 tst/i18n.php diff --git a/css/bootstrap/zerobin.css b/css/bootstrap/zerobin.css index c797639..3b660a3 100644 --- a/css/bootstrap/zerobin.css +++ b/css/bootstrap/zerobin.css @@ -26,3 +26,7 @@ body { padding: 5px 0 5px 5px; white-space: pre-wrap; } + +h4 { + margin-top: 0; +} diff --git a/i18n/de.json b/i18n/de.json new file mode 100644 index 0000000..65fa633 --- /dev/null +++ b/i18n/de.json @@ -0,0 +1,78 @@ +{ + "en": "de", + "Paste does not exist, has expired or has been deleted.": + "Diesen Schnipsel gibt es nicht, er ist abgelaufen oder wurde gelöscht.", + "ZeroBin requires php 5.2.6 or above to work. Sorry.": + "ZeroBin benötigt PHP 5.2.6 oder höher um zu funktionieren, sorry.", + "ZeroBin requires configuration section [%s] to be present in configuration file.": + "ZeroBin benötigt die Konfigurationssektion [%s] in der Konfigurationsdatei um zu funktionieren.", + "Please wait %d seconds between each post.": + "Bitte warte %d Sekunden zwischen dem Absenden.", + "Paste is limited to %s of encrypted data.": + "Schnipsel sind auf %s verschlüsselte Datenmenge beschränkt.", + "Invalid data.": + "Ungültige Daten.", + "You are unlucky. Try again.": + "Du hast Pech. Versuchs nochmal.", + "Error saving comment. Sorry.": + "Fehler beim Speichern des Kommentars, sorry.", + "Error saving paste. Sorry.": + "Fehler beim Speichern des Schnipsels, sorry.", + "Invalid paste ID.": + "Ungültige Schnipsel ID.", + "Paste is not of burn-after-reading type.": + "Schnipsel ist kein \"Einweg\"-Typ.", + "Wrong deletion token. Paste was not deleted.": + "Falscher Lösch-Kode. Schnipsel wurde nicht gelöscht.", + "Paste was properly deleted.": + "Schnipsel wurde erfolgreich gelöscht.", + "ZeroBin": "ZeroBin", + "ZeroBin is a minimalist, opensource online pastebin where the server has zero knowledge of pasted data. Data is encrypted/decrypted in the browser using 256 bits AES. More information on the project page.": + "ZeroBin ist ein minimalistischer, quelloffener \"pastebin\"-artiger Dienst bei dem der Server keinerlei Kenntnis der Daten-Schnipsel hat. Die Daten werden im Browser mit 256 Bit AES ver- und entschlüsselt. Weitere Informationen sind auf der Projektseite zu finden.", + "Because ignorance is bliss": + "Was ich nicht weiss, macht mich nicht heiss", + "Javascript is required for ZeroBin to work.
Sorry for the inconvenience.": + "Javascript ist eine Voraussetzung um ZeroBin zu nutzen.
Bitte entschuldige die Unannehmlichkeiten.", + "ZeroBin requires a modern browser to work.": + "ZeroBin setzt einen modernen Browser voraus um funktionieren zu können.", + "Still using Internet Explorer? Do yourself a favor, switch to a modern browser:": + "Du benutzt immer noch den Internet Explorer? Tu Dir einen Gefallen und wechsle zu einem moderneren Browser:", + "New": + "Neu", + "Send": + "Senden", + "Clone": + "Klonen", + "Raw text": + "Reiner Text", + "Expires": + "Ablaufzeit", + "Burn after reading": + "Einweg-Schnipsel", + "Open discussion": + "Diskussion eröffnen", + "Password (recommended)": + "Passwort (empfohlen)", + "Discussion": + "Diskussion", + "Toggle navigation": + "Navigation umschalten", + "5 minutes": + "5 Minuten", + "10 minutes": + "10 Minuten", + "1 hour": + "1 Stunde", + "1 day": + "1 Tag", + "1 week": + "1 Woche", + "1 month": + "1 Monat", + "1 year": + "1 Jahr", + "Never": + "Nie", + "Note: This is a test service: Data may be deleted anytime. Kittens will die if you abuse this service.": + "Hinweis: Dies ist ein Versuchsdienst. Daten können jederzeit gelöscht werden. Kätzchen werden sterben wenn Du diesen Dienst missbrauchst." +} diff --git a/index.php b/index.php index 572738c..5ad75f7 100644 --- a/index.php +++ b/index.php @@ -13,5 +13,6 @@ // change this, if your php files and data is outside of your webservers document root define('PATH', ''); +define('PUBLIC_PATH', dirname(__FILE__)); require PATH . 'lib/auto.php'; new zerobin; diff --git a/lib/RainTPL.php b/lib/RainTPL.php index 8edb708..2250b1d 100644 --- a/lib/RainTPL.php +++ b/lib/RainTPL.php @@ -1162,4 +1162,9 @@ class RainTpl_SyntaxException extends RainTpl_Exception{ } } +// shorthand translate function for use in templates +function t() { + return call_user_func_array(array('i18n', 'translate'), func_get_args()); +} + // -- end diff --git a/lib/i18n.php b/lib/i18n.php new file mode 100644 index 0000000..73934a9 --- /dev/null +++ b/lib/i18n.php @@ -0,0 +1,219 @@ +read())) + { + if (preg_match('/^([a-z]{2}).json$/', $file, $match) === 1) + { + $availableLanguages[] = $match[1]; + } + } + + $match = self::_getMatchingLanguage( + self::getBrowserLanguages(), $availableLanguages + ); + // load translations + if ($match != 'en') + { + self::$_translations = json_decode( + file_get_contents($path . DIRECTORY_SEPARATOR . $match . '.json'), + true + ); + } + } + + /** + * detect the clients supported languages and return them ordered by preference + * + * From: http://stackoverflow.com/questions/3770513/detect-browser-language-in-php#3771447 + * + * @return array + */ + public static function getBrowserLanguages() + { + $languages = array(); + if (array_key_exists('HTTP_ACCEPT_LANGUAGE', $_SERVER)) + { + $languageRanges = explode(',', trim($_SERVER['HTTP_ACCEPT_LANGUAGE'])); + foreach ($languageRanges as $languageRange) { + if (preg_match( + '/(\*|[a-zA-Z0-9]{1,8}(?:-[a-zA-Z0-9]{1,8})*)(?:\s*;\s*q\s*=\s*(0(?:\.\d{0,3})|1(?:\.0{0,3})))?/', + trim($languageRange), $match + )) + { + if (!isset($match[2])) + { + $match[2] = '1.0'; + } + else + { + $match[2] = (string) floatval($match[2]); + } + if (!isset($languages[$match[2]])) + { + $languages[$match[2]] = array(); + } + $languages[$match[2]][] = strtolower($match[1]); + } + } + krsort($languages); + } + return $languages; + } + + /** + * compares two language preference arrays and returns the preferred match + * + * From: http://stackoverflow.com/questions/3770513/detect-browser-language-in-php#3771447 + * + * @param array $acceptedLanguages + * @param array $availableLanguages + * @return string + */ + protected static function _getMatchingLanguage($acceptedLanguages, $availableLanguages) { + $matches = array(); + $any = false; + foreach ($acceptedLanguages as $acceptedQuality => $acceptedValues) { + $acceptedQuality = floatval($acceptedQuality); + if ($acceptedQuality === 0.0) continue; + foreach ($availableLanguages as $availableValue) + { + $availableQuality = 1.0; + foreach ($acceptedValues as $acceptedValue) + { + if ($acceptedValue === '*') + { + $any = true; + } + $matchingGrade = self::_matchLanguage($acceptedValue, $availableValue); + if ($matchingGrade > 0) + { + $q = (string) ($acceptedQuality * $availableQuality * $matchingGrade); + if (!isset($matches[$q])) + { + $matches[$q] = array(); + } + if (!in_array($availableValue, $matches[$q])) + { + $matches[$q][] = $availableValue; + } + } + } + } + } + if (count($matches) === 0 && $any) + { + if (count($availableLanguages) > 0) + { + $matches['1.0'] = $availableLanguages; + } + } + if (count($matches) === 0) + { + return 'en'; + } + krsort($matches); + $topmatches = current($matches); + return current($topmatches); + } + + /** + * compare two language IDs and return the degree they match + * + * From: http://stackoverflow.com/questions/3770513/detect-browser-language-in-php#3771447 + * + * @param string $a + * @param string $b + * @return float + */ + protected static function _matchLanguage($a, $b) { + $a = explode('-', $a); + $b = explode('-', $b); + for ($i=0, $n=min(count($a), count($b)); $i<$n; $i++) + { + if ($a[$i] !== $b[$i]) break; + } + return $i === 0 ? 0 : (float) $i / count($a); + } +} diff --git a/lib/zerobin.php b/lib/zerobin.php index 3485fee..6f9b12c 100644 --- a/lib/zerobin.php +++ b/lib/zerobin.php @@ -93,7 +93,7 @@ class zerobin { if (version_compare(PHP_VERSION, '5.2.6') < 0) { - throw new Exception('ZeroBin requires php 5.2.6 or above to work. Sorry.', 1); + throw new Exception(i18n::_('ZeroBin requires php 5.2.6 or above to work. Sorry.'), 1); } // in case stupid admin has left magic_quotes enabled in php.ini @@ -156,7 +156,7 @@ class zerobin $this->_conf = parse_ini_file(PATH . 'cfg' . DIRECTORY_SEPARATOR . 'conf.ini', true); foreach (array('main', 'model') as $section) { if (!array_key_exists($section, $this->_conf)) { - throw new Exception("ZeroBin requires configuration section [$section] to be present in configuration file.", 2); + throw new Exception(i18n::_('ZeroBin requires configuration section [%s] to be present in configuration file.', $section), 2); } } $this->_model = $this->_conf['model']['class']; @@ -184,12 +184,12 @@ class zerobin * Store new paste or comment * * POST contains: - * data (mandatory) = json encoded SJCL encrypted text (containing keys: iv,salt,ct) + * data (mandatory) = json encoded SJCL encrypted text (containing keys: iv,v,iter,ks,ts,mode,adata,cipher,salt,ct) * * All optional data will go to meta information: * expire (optional) = expiration delay (never,5min,10min,1hour,1day,1week,1month,1year,burn) (default:never) * opendiscusssion (optional) = is the discussion allowed on this paste ? (0/1) (default:0) - * nickname (optional) = in discussion, encoded SJCL encrypted text nickname of author of comment (containing keys: iv,salt,ct) + * nickname (optional) = in discussion, encoded SJCL encrypted text nickname of author of comment (containing keys: iv,v,iter,ks,ts,mode,adata,cipher,salt,ct) * parentid (optional) = in discussion, which comment this comment replies to. * pasteid (optional) = in discussion, which paste this comment belongs to. * @@ -208,9 +208,10 @@ class zerobin { $this->_return_message( 1, - 'Please wait ' . - $this->_conf['traffic']['limit'] . - ' seconds between each post.' + i18n::_( + 'Please wait %d seconds between each post.', + $this->_conf['traffic']['limit'] + ) ); return; } @@ -221,9 +222,10 @@ class zerobin { $this->_return_message( 1, - 'Paste is limited to ' . - filter::size_humanreadable($sizelimit) . - ' of encrypted data.' + i18n::_( + 'Paste is limited to %s of encrypted data.', + filter::size_humanreadable($sizelimit) + ) ); return; } @@ -232,15 +234,18 @@ class zerobin if (!sjcl::isValid($data)) return $this->_return_message(1, 'Invalid data.'); // Read additional meta-information. - $meta=array(); + $meta = array(); // Read expiration date if (!empty($_POST['expire'])) { $selected_expire = (string) $_POST['expire']; - if (array_key_exists($selected_expire, $this->_conf['expire_options'])) { + if (array_key_exists($selected_expire, $this->_conf['expire_options'])) + { $expire = $this->_conf['expire_options'][$selected_expire]; - } else { + } + else + { $expire = $this->_conf['expire_options'][$this->_conf['expire']['default']]; } if ($expire > 0) $meta['expire_date'] = time() + $expire; @@ -575,23 +580,25 @@ class zerobin // label all the expiration options $expire = array(); foreach ($this->_conf['expire_options'] as $key => $value) { - $expire[$key] = array_key_exists($key, $this->_conf['expire_labels']) ? + $expire[$key] = i18n::_( + array_key_exists($key, $this->_conf['expire_labels']) ? $this->_conf['expire_labels'][$key] : - $key; + $key + ); } $page = new RainTPL; $page::$path_replace = false; // we escape it here because ENT_NOQUOTES can't be used in RainTPL templates $page->assign('CIPHERDATA', htmlspecialchars($this->_data, ENT_NOQUOTES)); - $page->assign('ERROR', $this->_error); - $page->assign('STATUS', $this->_status); + $page->assign('ERROR', i18n::_($this->_error)); + $page->assign('STATUS', i18n::_($this->_status)); $page->assign('VERSION', self::VERSION); $page->assign('DISCUSSION', $this->_getMainConfig('discussion', true)); $page->assign('OPENDISCUSSION', $this->_getMainConfig('opendiscussion', true)); $page->assign('SYNTAXHIGHLIGHTING', $this->_getMainConfig('syntaxhighlighting', true)); $page->assign('SYNTAXHIGHLIGHTINGTHEME', $this->_getMainConfig('syntaxhighlightingtheme', '')); - $page->assign('NOTICE', $this->_getMainConfig('notice', '')); + $page->assign('NOTICE', i18n::_($this->_getMainConfig('notice', ''))); $page->assign('BURNAFTERREADINGSELECTED', $this->_getMainConfig('burnafterreadingselected', false)); $page->assign('PASSWORD', $this->_getMainConfig('password', true)); $page->assign('BASE64JSVERSION', $this->_getMainConfig('base64version', '2.1.9')); @@ -629,7 +636,7 @@ class zerobin $result = array('status' => $status); if ($status) { - $result['message'] = $message; + $result['message'] = i18n::_($message); } else { diff --git a/tpl/bootstrap.html b/tpl/bootstrap.html index d4ba294..28f0518 100644 --- a/tpl/bootstrap.html +++ b/tpl/bootstrap.html @@ -5,7 +5,7 @@ - ZeroBin + {function="t('ZeroBin')"} {if="$SYNTAXHIGHLIGHTING"} @@ -28,31 +28,31 @@
{/if}{if="strlen($STATUS)"} {/if} - - -