diff options
-rw-r--r-- | core/controllers/l10n_client.php | 80 | ||||
-rw-r--r-- | core/css/l10n_client.css | 184 | ||||
-rw-r--r-- | core/helpers/core_theme.php | 13 | ||||
-rw-r--r-- | core/js/l10n_client.js | 193 | ||||
-rw-r--r-- | core/libraries/I18n.php | 48 | ||||
-rw-r--r-- | core/tests/I18n_Test.php | 7 | ||||
-rw-r--r-- | core/views/l10n_client.html.php | 30 | ||||
-rw-r--r-- | core/views/scaffold.html.php | 7 |
8 files changed, 553 insertions, 9 deletions
diff --git a/core/controllers/l10n_client.php b/core/controllers/l10n_client.php new file mode 100644 index 00000000..fb008ce0 --- /dev/null +++ b/core/controllers/l10n_client.php @@ -0,0 +1,80 @@ +<?php defined("SYSPATH") or die("No direct script access."); +/** + * Gallery - a web based photo album viewer and editor + * Copyright (C) 2000-2008 Bharat Mediratta + * + * This program 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. + * + * This program 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 this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. + */ +class L10n_Client_Controller extends Controller { + public function save($string) { + access::verify_csrf(); + + print json_encode(new stdClass()); + } + + private static function _l10n_client_form() { + $form = new Forge("/l10n_client/save", "", "post", array("id" => "gL10nClientSaveForm")); + $group = $form->group("l10n_message"); + $group->textarea("l10n-edit-target"); + $group->submit("l10n-edit-save")->value(t("Save translation")); + // TODO(andy_st): Avoiding multiple submit buttons for now (hassle with jQuery form plugin). + // $group->submit("l10n-edit-copy")->value(t("Copy source")); + // $group->submit("l10n-edit-clear")->value(t("Clear")); + + return $form; + } + + private static function _l10n_client_search_form() { + $form = new Forge("/l10n_client/search", "", "post", array("id" => "gL10nSearchForm")); + $group = $form->group("l10n_search"); + $group->input("l10n-search")->id("gL10nSearch"); + $group->submit("l10n-search-filter-clear")->value(t("X")); + + return $form; + } + + public static function l10n_form() { + $calls = I18n::instance()->getCallLog(); + + if ($calls) { + $string_list = array(); + foreach ($calls as $call) { + list ($message, $options) = $call; + if (is_array($message)) { + // TODO: Translate each message. If it has a plural form, get + // the current locale's plural rules and all plural translations. + $options['count'] = 1; + $source = $message['one']; + } else { + $source = $message; + } + $translation = ''; + if (I18n::instance()->hasTranslation($message, $options)) { + $translation = I18n::instance()->hasTtranslation($message, $options); + } + $string_list[] = array('source' => $source, + 'translation' => $translation); + } + + return View::factory('l10n_client.html', + array('string_list' => $string_list, + 'l10n_form' => self::_l10n_client_form(), + 'l10n_search_form' => self::_l10n_client_search_form())) + ->render(); + } + + return ''; + } +} diff --git a/core/css/l10n_client.css b/core/css/l10n_client.css new file mode 100644 index 00000000..9e0b03d5 --- /dev/null +++ b/core/css/l10n_client.css @@ -0,0 +1,184 @@ +// TODO(andy_st): Add original copyright notice from Drupal l10_client. +// TODO(andy_st): Add G3 copyright notice. + +/* $Id: l10n_client.css,v 1.6 2008/09/09 10:48:20 goba Exp $ */ + +/* width percentages add to 99% rather than 100% to prevent float +overflows from occurring in an unnamed browser that can't decide +how it wants to round. */ + +/* l10n_client container */ +#l10n-client { + text-align:left; + z-index:99; + line-height:1em; + color:#000; background:#fff; + position:fixed; + width:100%; height: 2em; + bottom:0px; left:0px; + overflow:hidden;} + + * html #l10n-client { + position:static;} + +#l10n-client-string-select .string-list, +#l10n-client-string-editor .source, +#l10n-client-string-editor .editor { + height:20em;} + +#l10n-client .labels { + overflow:hidden; + position:relative; + height:2em; + color:#fff; + background:#37a;} + + #l10n-client .labels .label { + display:none;} + + /* Panel toggle button (span) */ + #l10n-client .labels .toggle { + cursor:pointer; + display:block; + position:absolute; right:0em; + padding: 0em .75em; height:2em; line-height:2em; + text-transform:uppercase; + text-align:center; background:#000;} + + /* Panel labels */ + #l10n-client h2 { + border-left:1px solid #fff; + height:1em; line-height:1em; + padding: .5em; margin:0px; + font-size:1em; + text-transform:uppercase;} + + #l10n-client .strings h2 { + border:0px;} + + /* 25 + 37 + 37 = 99 */ + #l10n-client .strings { + width:25%; float:left;} + + #l10n-client .source { + width:37%; float:left;} + + #l10n-client .translation { + width:37%; float:left;} + +/* Translatable string list */ +#l10n-client-string-select { + display:none; + float:left; + width:25%;} + + #l10n-client .string-list { + height:17em; + overflow:auto; + list-style:none; list-style-image:none; + margin:0em; padding:0em;} + + #l10n-client .string-list li { + font-size:.9em; + line-height:1.5em; + cursor:default; + background:transparent; + list-style:none; list-style-image:none; + border-bottom:1px solid #ddd; + padding:.25em .5em; + margin:0em;} + + /* Green for translated */ + #l10n-client .string-list li.translated { + border-bottom-color:#9c3; + background:#cf6; color:#360;} + + #l10n-client .string-list li.translated:hover { + background: #df8;} + + #l10n-client .string-list li.translated:active { + background: #9c3;} + + #l10n-client .string-list li.hidden { + display:none;} + + /* Gray + Blue hover for untranslated */ + #l10n-client .string-list li.untranslated {} + + #l10n-client .string-list li.untranslated:hover { + background: #ace;} + + #l10n-client .string-list li.untranslated:active { + background: #8ac;} + + /* Selected string is indicated by bold text */ + #l10n-client .string-list li.active { + font-weight:bold;} + + #l10n-client #gL10nSearchForm { + background:#eee; + text-align:center; + height:2em; line-height:2em; + margin:0em; padding:.5em .5em; + } + + #l10n-client #gL10nSearchForm .form-item, + #l10n-client #gL10nSearchForm input.form-text, + #l10n-client #gL10nSearchForm #search-filter-go, + #l10n-client #gL10nSearchForm #search-filter-clear { + display:inline; + vertical-align:middle; + } + + #l10n-client #gL10nSearchForm .form-item { + margin:0em; + padding:0em; + } + + #l10n-client #gL10nSearchForm input.form-text { + width:80%; + } + + #l10n-client #gL10nSearchForm #search-filter-clear { + width:10%; + margin:0em; + } + + +#l10n-client-string-editor { + display:none; + float:left; + width:74%;} + + #l10n-client-string-editor .source { + overflow:hidden; + width:50%; float:left;} + + #l10n-client-string-editor .source .source-text { + line-height:1.5em; + background:#eee; + height:16em; margin:1em; padding:1em; + overflow:auto;} + + #l10n-client-string-editor .translation { + overflow:hidden; + width:49%; float:right;} + +#gL10nClientSaveForm { + padding:0em;} + + #gL10nClientSaveForm .form-textarea { + height:13em; + font-size:1em; line-height:1.25em; + width:95%;} + + #gL10nClientSaveForm .form-submit { + margin-top: 0em;} + + +#l10n-client form ul, +#l10n-client form li, +#l10n-client form input[type=submit], +#l10n-client form input[type=text] { + display: inline ! important ; +}
\ No newline at end of file diff --git a/core/helpers/core_theme.php b/core/helpers/core_theme.php index 72e7829e..1e1f08d5 100644 --- a/core/helpers/core_theme.php +++ b/core/helpers/core_theme.php @@ -39,6 +39,14 @@ class core_theme_Core { "</script>"; $buf .= html::script("core/js/fullsize.js"); } + + if (Session::instance()->get("l10n_mode", false)) { + $buf .= "<link rel=\"stylesheet\" type=\"text/css\" href=\"" . + url::file("core/css/l10n_client.css") . "\" />"; + $buf .= html::script("lib/jquery.cookie.js"); + $buf .= html::script("core/js/l10n_client.js"); + } + return $buf; } @@ -63,10 +71,15 @@ class core_theme_Core { } static function page_bottom($theme) { + $output = ''; + if (Session::instance()->get("l10n_mode", false)) { + $output .= L10n_Client_Controller::l10n_form(); + } if (Session::instance()->get("profiler", false)) { $profiler = new Profiler(); $profiler->render(); } + return $output; } static function admin_page_bottom($theme) { diff --git a/core/js/l10n_client.js b/core/js/l10n_client.js new file mode 100644 index 00000000..7d2ca5e3 --- /dev/null +++ b/core/js/l10n_client.js @@ -0,0 +1,193 @@ +// Fork from Drupal's l10n_client module, originally written by: +// Gbor Hojtsy http://drupal.org/user/4166 (original author) +// Young Hahn / Development Seed - http://developmentseed.org/ (friendly user interface) + +var Gallery = Gallery || { 'behaviors': {} }; + +Gallery.attachBehaviors = function(context) { + context = context || document; + // Execute all of them. + jQuery.each(Gallery.behaviors, + function() { + this(context); + }); +}; + +$(document).ready(function() { + Gallery.attachBehaviors(this); +}); + + +// Store all l10n_client related data + methods in its own object +jQuery.extend(Gallery, { + l10nClient: new (function() { + // Set "selected" string to unselected, i.e. -1 + this.selected = -1; + // Keybindings + this.keys = {'toggle':'ctrl+shift+s', 'clear': 'esc'}; // Keybindings + // Keybinding functions + this.key = function(pressed) { + switch(pressed) { + case 'toggle': + // Grab user-hilighted text & send it into the search filter + userSelection = window.getSelection ? window.getSelection() : document.getSelection ? document.getSelection() : document.selection.createRange().text; + userSelection = String(userSelection); + if(userSelection.length > 0) { + Gallery.l10nClient.filter(userSelection); + Gallery.l10nClient.toggle(1); + $('#l10n-client #gL10nSearch').focus(); + } else { + if($('#l10n-client').is('.hidden')) { + Gallery.l10nClient.toggle(1); + if(!$.browser.safari) { + $('#l10n-client #gL10nSearch').focus(); + } + } else { + Gallery.l10nClient.toggle(0); + } + } + break; + case 'clear': + this.filter(false); + break; + } + } + // Toggle the l10nclient + this.toggle = function(state) { + switch(state) { + case 1: + $('#l10n-client-string-select, #l10n-client-string-editor, #l10n-client .labels .label').show(); + $('#l10n-client').height('22em').removeClass('hidden'); + $('#l10n-client .labels .toggle').text('X'); + /* + * This CSS clashes with Gallery's CSS, probably due to + * YUI's grid / floats. + if(!$.browser.msie) { + $('body').css('border-bottom', '22em solid #fff'); + } + */ + $.cookie('Gallery_l10n_client', '1', {expires: 7, path: '/'}); + break; + case 0: + $('#l10n-client-string-select, #l10n-client-string-editor, #l10n-client .labels .label').hide(); + $('#l10n-client').height('2em').addClass('hidden'); + // TODO: Localize this message + $('#l10n-client .labels .toggle').text('Translate Text'); + /* + if(!$.browser.msie) { + $('body').css('border-bottom', '0px'); + } + */ + $.cookie('Gallery_l10n_client', '0', {expires: 7, path: '/'}); + break; + } + } + // Get a string from the DOM tree + this.getString = function(index, type) { + return l10n_client_data[index][type]; + } + // Set a string in the DOM tree + this.setString = function(index, data) { + l10n_client_data[index]['translation'] = data; + } + // Filter the the string list by a search string + this.filter = function(search) { + if(search == false || search == '') { + $('#l10n-client #l10n-search-filter-clear').focus(); + $('#l10n-client-string-select li').show(); + $('#l10n-client #gL10nSearch').val(''); + $('#l10n-client #gL10nSearch').focus(); + } else { + if(search.length > 0) { + $('#l10n-client-string-select li').hide(); + $('#l10n-client-string-select li:contains('+search+')').show(); + $('#l10n-client #gL10nSearch').val(search); + } + } + } + }) +}); + +// Attaches the localization editor behavior to all required fields. +Gallery.behaviors.l10nClient = function(context) { + + switch($.cookie('Gallery_l10n_client')) { + case '1': + Gallery.l10nClient.toggle(1); + break; + default: + Gallery.l10nClient.toggle(0); + break; + } + + // If the selection changes, copy string values to the source and target fields. + // Add class to indicate selected string in list widget. + $('#l10n-client-string-select li').click(function() { + $('#l10n-client-string-select li').removeClass('active'); + $(this).addClass('active'); + var index = $('#l10n-client-string-select li').index(this); + + $('#l10n-client-string-editor .source-text').text(Gallery.l10nClient.getString(index, 'source')); + $('#gL10nClientSaveForm #l10n-edit-target').val(Gallery.l10nClient.getString(index, 'translation')); + + Gallery.l10nClient.selected = index; + }); + + // When l10n_client window is clicked, toggle based on current state. + $('#l10n-client .labels .toggle').click(function() { + if($('#l10n-client').is('.hidden')) { + Gallery.l10nClient.toggle(1); + } else { + Gallery.l10nClient.toggle(0); + } + }); + + // Register keybindings using jQuery hotkeys + // TODO: Either remove hotkeys code or add query.hotkeys.js. + if($.hotkeys) { + $.hotkeys.add(Gallery.l10nClient.keys['toggle'], function(){Gallery.l10nClient.key('toggle')}); + $.hotkeys.add(Gallery.l10nClient.keys['clear'], {target:'#l10n-client #gL10nSearch', type:'keyup'}, function(){Gallery.l10nClient.key('clear')}); + } + + // Custom listener for l10n_client livesearch + $('#l10n-client #gL10nSearch').keyup(function(key) { + Gallery.l10nClient.filter($('#l10n-client #gL10nSearch').val()); + }); + + // Clear search + $('#l10n-client #l10n-search-filter-clear').click(function() { + Gallery.l10nClient.filter(false); + return false; + }); + + // Send AJAX POST data on form submit. + $('#gL10nClientSaveForm').ajaxForm({ + dataType: "json", + success: function(data) { + // Store string in local js + Gallery.l10nClient.setString(Gallery.l10nClient.selected, $('#gL10nClientSaveForm #l10n-edit-target').val()); + + // Mark string as translated. + $('#l10n-client-string-select li').eq(Gallery.l10nClient.selected).removeClass('untranslated').removeClass('active').addClass('translated').text($('#gL10nClientSaveForm #l10n-edit-target').val()); + + // Empty input fields. + $('#l10n-client-string-editor .source-text').html(''); + $('#gL10nClientSaveForm #l10n-edit-target').val(''); + }, + error: function(xmlhttp) { + // TODO: Localize this message + alert('An HTTP error @status occured (or empty response).'.replace('@status', xmlhttp.status)); + } + }); + + + // Copy source text to translation field on button click. + $('#gL10nClientSaveForm #l10n-edit-copy').click(function() { + $('#gL10nClientSaveForm #l10n-edit-target').val($('#l10n-client-string-editor .source-text').text()); + }); + + // Clear translation field on button click. + $('#gL10nClientSaveForm #l10n-edit-clear').click(function() { + $('#gL10nClientSaveForm #l10n-edit-target').val(''); + }); +}; diff --git a/core/libraries/I18n.php b/core/libraries/I18n.php index 1419357e..51bc2a51 100644 --- a/core/libraries/I18n.php +++ b/core/libraries/I18n.php @@ -48,11 +48,14 @@ function t2($singular, $plural, $count, $options=array()) { } class I18n_Core { + private static $_instance; + private $_config = array(); + private $_call_log = array(); + private $_cache = array(); - private static $_instance; private function __construct($config) { $this->_config = $config; @@ -81,10 +84,11 @@ class I18n_Core { $count = empty($options['count']) ? null : $options['count']; $values = $options; unset($values['locale']); + $this->log($message, $options); $entry = $this->lookup($locale, $message); - if (empty($entry)) { + if (null === $entry) { // Default to the root locale. $entry = $message; $locale = $this->_config['root_locale']; @@ -111,9 +115,7 @@ class I18n_Core { } } - // If message is an array (plural forms), use the first form as message id. - $key = is_array($message) ? array_shift($message) : $message; - $key = md5($key, true); + $key = self::getMessageKey($message); if (isset($this->_cache[$locale][$key])) { return $this->_cache[$locale][$key]; @@ -122,6 +124,33 @@ class I18n_Core { } } + public function hasTranslation($message, $options=null) { + $locale = empty($options['locale']) ? $this->_config['default_locale'] : $options['locale']; + $count = empty($options['count']) ? null : $options['count']; + $values = $options; + unset($values['locale']); + $this->log($message, $options); + + $entry = $this->lookup($locale, $message); + + if (null === $entry) { + return false; + } else if (!is_array($entry)) { + return $entry !== ''; + } else { + $plural_key = self::get_plural_key($locale, $count); + return isset($entry[$plural_key]) + && $entry[$plural_key] !== null + && $entry[$plural_key] !== ''; + } + } + + public static function getMessageKey($message) { + // If message is an array (plural forms), use the first form as message id. + $key = is_array($message) ? array_shift($message) : $message; + return md5($key, true); + } + private function interpolate($locale, $string, $values) { // TODO: Handle locale specific number formatting. @@ -157,6 +186,15 @@ class I18n_Core { } } + private function log($message, $options) { + $key = self::getMessageKey($message); + isset($this->_call_log[$key]) or $this->_call_log[$key] = array($message, $options); + } + + public function getCallLog() { + return $this->_call_log; + } + private static function get_plural_key($locale, $count) { $parts = explode('_', $locale); $language = $parts[0]; diff --git a/core/tests/I18n_Test.php b/core/tests/I18n_Test.php index 20f65528..df139f04 100644 --- a/core/tests/I18n_Test.php +++ b/core/tests/I18n_Test.php @@ -32,7 +32,7 @@ class I18n_Test extends Unit_Test_Case { ->where("locale", "te_ST") ->delete_all(); - $messages_de_DE = array( + $messages_te_ST = array( array('Hello world', 'Hallo Welt'), array(array('one' => 'One item has been added', 'other' => '%count elements have been added'), @@ -40,12 +40,11 @@ class I18n_Test extends Unit_Test_Case { 'other' => '%count Elemente wurden hinzugefuegt.')), array('Hello %name, how are you today?', 'Hallo %name, wie geht es Dir heute?')); - foreach ($messages_de_DE as $data) { + foreach ($messages_te_ST as $data) { list ($message, $translation) = $data; $key = $message; - $key = is_array($key) ? array_shift($key) : $key; $entry = ORM::factory("incoming_translation"); - $entry->key = md5($key, true); + $entry->key = I18n::getMessageKey($message); $entry->message = serialize($message); $entry->translation = serialize($translation); $entry->locale = 'te_ST'; diff --git a/core/views/l10n_client.html.php b/core/views/l10n_client.html.php new file mode 100644 index 00000000..7c09df7b --- /dev/null +++ b/core/views/l10n_client.html.php @@ -0,0 +1,30 @@ +<?php defined("SYSPATH") or die("No direct script access.") ?> + +<div id='l10n-client' class='hidden'> + <div class='labels'> + <span class='toggle'><?= t('Translate Text') ?></span> + <div class='label strings'><h2><?= t('Page Text') ?></h2></div> + <div class='label source'><h2><?= t('Source') ?></div> + <div class='label translation'><h2><?= t('Translation to %language', + array('%language' => 'TODO')) ?></h2></div> + </div> + <div id='l10n-client-string-select'> + <ul class='string-list'> + <? foreach ($string_list as $string): ?> + <li class='<?= $string["translation"] === '' ? "untranslated" : "translated" ?>'><?= $string["source"] ?></li> + <? endforeach; ?> + </ul> + <?= $l10n_search_form ?> + </div> + <div id='l10n-client-string-editor'> + <div class='source'> + <div class='source-text'></div> + </div> + <div class='translation'> + <?= $l10n_form ?> + </div> + </div> + <script type="text/javascript"> + var l10n_client_data = <?= json_encode($string_list) ?>; + </script> +</div> diff --git a/core/views/scaffold.html.php b/core/views/scaffold.html.php index 2dd256c2..b54e6cd2 100644 --- a/core/views/scaffold.html.php +++ b/core/views/scaffold.html.php @@ -440,6 +440,13 @@ <?= html::anchor("scaffold/session/debug?value=1", "on") ?> <b>off</b> <? endif ?> </li> + <li> Translation Mode: + <? if (Session::instance()->get("l10n_mode", false)): ?> + <b>on</b> <?= html::anchor("scaffold/session/l10n_mode?value=0", "off") ?> + <? else: ?> + <?= html::anchor("scaffold/session/l10n_mode?value=1", "on") ?> <b>off</b> + <? endif ?> + </li> </ul> </div> |