From 0d2d75242f1202ec82f572bca43da3dd25a4bf91 Mon Sep 17 00:00:00 2001 From: Sebastien Guibert Date: Sun, 26 Jun 2011 00:10:42 +0200 Subject: [PATCH] New improved version with some new features, doc coming soon. --- classes/multilang/core.php | 97 +++++++++++++-------- classes/multilang/request.php | 160 +++++++++++++++++++++++++--------- classes/multilang/route.php | 117 +++++-------------------- classes/multilang/routes.php | 35 +++++++- classes/multilang/url.php | 10 +-- config/multilang.php | 17 ++-- init.php | 31 +++++++ views/multilang/selector.php | 15 ++-- 8 files changed, 282 insertions(+), 200 deletions(-) create mode 100644 init.php diff --git a/classes/multilang/core.php b/classes/multilang/core.php index c00a700..49ce895 100644 --- a/classes/multilang/core.php +++ b/classes/multilang/core.php @@ -9,40 +9,44 @@ class Multilang_Core { static public $lang = ''; /** - * Looks for the best default language available and returns it. + * Looks for the user language. * A language cookie and HTTP Accept-Language headers are taken into account. + * + * If the auto detection is disabled, we return the default one * - * @return string language key, e.g. "en", "fr", "nl", etc. + * @return string language key, e.g. "en", "fr", "nl", "en_US", "en-us", etc. */ - static public function find_default() + static public function find_user_language() { - // Get the list of supported languages - $langs = (array) Kohana::config('multilang.languages'); - $cookie = Kohana::config('multilang.cookie'); - - // Look for language cookie first - if($lang = Cookie::get($cookie)) + if(Kohana::config('multilang.auto_detect')) { - // Valid language found in cookie - if(isset($langs[$lang])) + // Get the list of supported languages + $languages = (array) Kohana::config('multilang.languages'); + $cookie = Kohana::config('multilang.cookie'); + + // Look for language cookie first + if($lang = Cookie::get($cookie)) { - return $lang; + // Valid language found in cookie + if(isset($languages[$lang])) + { + return $lang; + } + + // Delete cookie with unset language + Cookie::delete($cookie); } - // Delete cookie with unset language - Cookie::delete($cookie); - } - - // Parse HTTP Accept-Language headers - foreach(Request::accept_lang() as $lang => $quality) - { - // Return the first language found (the language with the highest quality) - if(isset($langs[$lang])) + // Parse HTTP Accept-Language headers + foreach(Request::accept_lang() as $lang => $quality) { - return $lang; + // Return the first language found (the language with the highest quality) + if(isset($languages[$lang])) + { + return $lang; + } } } - // Return the hard-coded default language as final fallback return Kohana::config('multilang.default'); } @@ -78,37 +82,56 @@ class Multilang_Core { { $languages = (array) Kohana::config('multilang.languages'); - // get the current route name - $current_route = Route::name(Request::initial()->route()); - $default_language = Kohana::config('multilang.default'); + // Get the current route name + $current_route = Route::name(Request::initial()->route()); + $params = Request::initial()->param(); - if(strpos($current_route, '.') !== FALSE) + if($current_route !== 'default' && strpos($current_route, '.') !== FALSE) { // Split the route path list($lang, $name) = explode('.', $current_route, 2); - } else { + } + else + { $name = $current_route; } // Create uris for each language foreach($languages as $code => &$language) - { - if($code == Request::$lang) + { + // If it's the current language + if($code === Request::$lang) { + // We only display it when required if($current) { - $language['uri'] = FALSE; - } else { - unset($languages[$code]); + $selectors[$code] = ''.$languages[$code]['label'].''; + } + } + else + { + // If it's the default route, it's unique and special (like you <3) + if($current_route === 'default') + { + // We juste need to change the language parameter + $route = Request::initial()->route(); + $params = array( + 'lang' => $code, + ); } - } else { - - $language['uri'] = Route::get($name, $code)->uri($params, $code); + else + { + $route = Route::get($name, $code); + } + + $selectors[$code] = HTML::anchor($route->uri($params), $languages[$code]['label'], array('class' => 'multilang-selectable multilang-'.$code, 'title' => $languages[$code]['label'])); } } + + return View::factory('multilang/selector') - ->bind('languages', $languages); + ->bind('selectors', $selectors); } } \ No newline at end of file diff --git a/classes/multilang/request.php b/classes/multilang/request.php index 52378eb..9bf600b 100644 --- a/classes/multilang/request.php +++ b/classes/multilang/request.php @@ -9,72 +9,148 @@ class Multilang_Request extends Kohana_Request { /** * @var string request language code */ - static public $lang = ''; + static public $lang = NULL; /** * * Extension of the request factory method. If none given, the URI will - * be automatically detected. If the URI contains no language segment, the user - * will be redirected to the same URI with the default language prepended. - * If the URI does contain a language segment, I18n and locale will be set. - * Also, a cookie with the current language will be set. Finally, the language - * segment is chopped off the URI and normal request processing continues. + * be automatically detected. If the URI contains no language segment and + * we don't hide the default language, the user will be redirected to the + * same URI with the default language prepended. + * If the URI does contain a language segment, I18n and locale will be set and + * a cookie with the current language aswell. * * @param string URI of the request * @param Kohana_Cache cache object + * @param array $injected_routes an array of routes to use, for testing * @return Request */ public static function factory($uri = TRUE, Cache $cache = NULL, $injected_routes = array()) { + if(!Kohana::$is_cli) { - // Get the list of supported languages - $langs = (array) Kohana::config('multilang.languages'); - - if($uri === TRUE) - { - // We need the current URI - $uri = Request::detect_uri(); - } - - // Normalize URI - $uri = ltrim($uri, '/'); - - // Look for a supported language in the first URI segment - if(!preg_match('~^(?:'.implode('|', array_keys($langs)).')(?=/|$)~i', $uri, $matches)) - { - // If we don't have any, we look whether it's normal (is it in the no lang routes ?) or we need to append it - if(Request::process_uri($uri, Route::nolang_routes())) - { - return parent::factory($uri, $cache); - } - - // We can't find a language, we're gonna need to look deeper - $lang = Multilang::find_default(); - + // If we don't hide the default language, we must look for a language code for the root uri + if(!Kohana::config('multilang.hide_default') && $uri === TRUE && Request::detect_uri() === '') + { + $lang = Multilang::find_user_language(); + // Use the default server protocol $protocol = (isset($_SERVER['SERVER_PROTOCOL'])) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.1'; - // Redirect to the same URI, but with language prepended + // Redirect to the root URI, but with language prepended header($protocol.' 302 Found'); - header('Location: '.URL::base(TRUE, TRUE).$lang.'/'.$uri); - - // Stop execution + header('Location: '.URL::base(TRUE, TRUE).$lang.'/'); + exit; + } + } + + $request = parent::factory($uri, $cache, $injected_routes); + Request::$lang = $request->param('lang'); + Multilang::init(); + return $request; + } + + + + + /** + * ONLY REMOVE THE FRONT SLASHES FROM THE URI + */ + public function __construct($uri, Cache $cache = NULL, $injected_routes = array()) + { + // Remove the front slashes + $uri = ltrim($uri, '/'); + + // Initialise the header + $this->_header = new HTTP_Header(array()); + + // Assign injected routes + $this->_injected_routes = $injected_routes; + + // Cleanse query parameters from URI (faster that parse_url()) + $split_uri = explode('?', $uri); + $uri = array_shift($split_uri); + + // Initial request has global $_GET already applied + if (Request::$initial !== NULL) + { + if ($split_uri) + { + parse_str($split_uri[0], $this->_get); + } + } + + // Detect protocol (if present) + // Always default to an internal request if we don't have an initial. + // This prevents the default index.php from being able to proxy + // external pages. + if (Request::$initial === NULL OR strpos($uri, '://') === FALSE) + { + $processed_uri = Request::process_uri($uri, $this->_injected_routes); + + if ($processed_uri === NULL) + { + throw new HTTP_Exception_404('Unable to find a route to match the URI: :uri', array( + ':uri' => $uri, + )); } - // Language found in the URI - Request::$lang = strtolower($matches[0]); + // Store the URI + $this->_uri = $uri; - Multilang::init(); + // Store the matching route + $this->_route = $processed_uri['route']; + $params = $processed_uri['params']; - // Remove language from URI - $uri = (string) substr($uri, strlen(Request::$lang)); + // Is this route external? + $this->_external = $this->_route->is_external(); + + if (isset($params['directory'])) + { + // Controllers are in a sub-directory + $this->_directory = $params['directory']; + } + + // Store the controller + $this->_controller = $params['controller']; + + if (isset($params['action'])) + { + // Store the action + $this->_action = $params['action']; + } + else + { + // Use the default action + $this->_action = Route::$default_action; + } + + // These are accessible as public vars and can be overloaded + unset($params['controller'], $params['action'], $params['directory']); + + // Params cannot be changed once matched + $this->_params = $params; + + // Apply the client + $this->_client = new Request_Client_Internal(array('cache' => $cache)); + } + else + { + // Create a route + $this->_route = new Route($uri); + + // Store the URI + $this->_uri = $uri; + + // Set external state + $this->_external = TRUE; + + // Setup the client + $this->_client = new Request_Client_External(array('cache' => $cache)); } - // Continue normal request processing with the URI without language*/ - return parent::factory($uri, $cache, $injected_routes); } - } \ No newline at end of file diff --git a/classes/multilang/route.php b/classes/multilang/route.php index 9f4087a..1957573 100644 --- a/classes/multilang/route.php +++ b/classes/multilang/route.php @@ -5,30 +5,11 @@ class Multilang_Route extends Kohana_Route { - protected $_lang = ''; - - static protected $_nolang_routes = array(); + public $lang = NULL; /** - * Altered method to allow multiple routes for i18n. - * You can pass an array with the language code as the key and the uri as the value. - * - * Route::set('homepage', array( - * 'en' => 'home', - * 'fr' => 'accueil', - * ))->defaults(array( - * 'controller' => 'homepage', - * 'action' => 'index', - * )); - * - * Stores a named route and returns it. The "action" will always be set to - * "index" if it is not defined. - * - * Route::set('default', '((/(/)))') - * ->defaults(array( - * 'controller' => 'welcome', - * )); - * + * Altered method to allow a language + * * * @param string route name * @param mixed URI pattern or Array of URI patterns or a lambda/callback function * @param array regex patterns for route keys @@ -36,17 +17,8 @@ class Multilang_Route extends Kohana_Route { * @return Route */ static public function set($name, $uri_callback = NULL, $regex = NULL, $lang = NULL) - { - if($lang) - { - $name = $lang.'.'.$name; - } - Route::$_routes[$name] = new Route($uri_callback, $regex, $lang); - if($lang === FALSE) - { - Route::$_nolang_routes[$name] = Route::$_routes[$name]; - } - return Route::$_routes[$name]; + { + return Route::$_routes[$name] = new Route($uri_callback, $regex, $lang); } @@ -71,16 +43,13 @@ class Multilang_Route extends Kohana_Route { if(isset(Route::$_routes[$lang.'.'.$name])) { $name = $lang.'.'.$name; - // then the default language - } elseif(isset(Route::$_routes[Kohana::config('multilang.default').'.'.$name])) { + + } // then the default language + elseif(isset(Route::$_routes[Kohana::config('multilang.default').'.'.$name])) { $name = Kohana::config('multilang.default').'.'.$name; } - $route = parent::get($name); - if($route !== NULL) - { - $route->_lang = $lang; - } - return $route; + // And if we don't have any for this language, it means that route is neither defined nor multilingual + return parent::get($name); } /** @@ -119,7 +88,7 @@ class Multilang_Route extends Kohana_Route { */ public function __construct($uri = NULL, array $regex = NULL, $lang = NULL) { - $this->_lang = $lang; + $this->lang = $lang; return parent::__construct($uri, $regex); } @@ -144,14 +113,17 @@ class Multilang_Route extends Kohana_Route { */ public function uri(array $params = NULL, $lang = NULL) { - $uri = parent::uri($params); - - // We add the language code if required - if($this->_lang) + // We define the language if required + if($this->lang !== NULL) { - // We dont use the route language to avoid some issues with routes of different languages having the same pattern - $lang = ($lang === NULL ? Request::$lang : $lang); - return $lang.'/'.$uri; + $params['lang'] = ($lang === NULL ? $this->lang : $lang); + } + + $uri = parent::uri($params); + // If it's the default route, we add a trailing slash + if(Route::name($this) === 'default') + { + $uri .= '/'; } return $uri; } @@ -176,50 +148,5 @@ class Multilang_Route extends Kohana_Route { return URL::site(Route::get($name, $lang)->uri($params), $protocol); } - - /** - * Get all the routes without any language code - * @return array - */ - static public function nolang_routes() - { - return Route::$_nolang_routes; - } - - - /** - * Saves or loads the route cache. If your routes will remain the same for - * a long period of time, use this to reload the routes from the cache - * rather than redefining them on every page load. - * - * It remakes the nolang_routes array too for language less routes - * - * if ( ! Route::cache()) - * { - * // Set routes here (or include a file for example) - * Route::cache(TRUE); - * } - * - * @param boolean cache the current routes - * @return void when saving routes - * @return boolean when loading routes - * @uses Kohana::cache - */ - static public function cache($save = FALSE) - { - $return = parent::cache($save); - if($save !== TRUE && Route::$_routes) - { - Route::$_nolang_routes = array(); - foreach(Route::$_routes as $name => $route) - { - if($route->_lang === FALSE) - { - Route::$_nolang_routes[$name] = Route::$_routes[$name]; - } - } - } - return $return; - } - + } \ No newline at end of file diff --git a/classes/multilang/routes.php b/classes/multilang/routes.php index 95f1305..6d75a18 100644 --- a/classes/multilang/routes.php +++ b/classes/multilang/routes.php @@ -27,10 +27,39 @@ class Multilang_Routes { static public function set($name, $uris = array(), $regex = NULL) { $routes = new Routes(); - // we add all the routes setting the name to code.name (en.homepage for example). - foreach($uris as $code => $uri) + + // We add the routes for each language and set their names to lang.name (en.homepage for example). + // The segment is also added on the uri if it's not hidden + + $default_lang = Kohana::config('multilang.default'); + $languages = Kohana::config('multilang.languages'); + // We first look for the default language uri which is obviously compulsory + $default_uri = Arr::get($uris, $default_lang); + if($default_uri === NULL) { - $routes->_routes[$code.'.'.$name] = Route::set($name, $uri, $regex, $code); + throw new Kohana_Exception('The default route uri is required for the language :lang', array(':lang' => $default_lang)); + } + else + { + // If we dont hide the default language in the uri + if(!Kohana::config('multilang.hide_default')) + { + $default_uri = '/'.$default_uri; + $regex['lang'] = $default_lang; + } + $routes->_routes[$default_lang.'.'.$name] = Route::set($default_lang.'.'.$name, $default_uri, $regex, $default_lang); + + } + unset($languages[$default_lang]); + + // Then we add the routes for all the other languages + foreach($languages as $lang => $settings) + { + $uri = '/'.(Arr::get($uris, $lang) ? $uris[$lang] : $uris[$default_lang]); + $regex['lang'] = $lang; + + // For the uri, we use the one given or the default one + $routes->_routes[$lang.'.'.$name] = Route::set($lang.'.'.$name, $uri, $regex, $lang); } return $routes; } diff --git a/classes/multilang/url.php b/classes/multilang/url.php index 5329add..570f754 100644 --- a/classes/multilang/url.php +++ b/classes/multilang/url.php @@ -3,16 +3,12 @@ class Multilang_URL extends Kohana_URL { /* - * We don't trim the trailing slash if - */ + * We don't trim the right slashes + */ public static function site($uri = '', $protocol = NULL, $index = TRUE) { - if(strlen($uri) > 3) - { - $uri = trim($uri, '/'); - } // Chop off possible scheme, host, port, user and pass parts - $path = preg_replace('~^[-a-z0-9+.]++://[^/]++/?~', '', $uri); + $path = preg_replace('~^[-a-z0-9+.]++://[^/]++/?~', '', ltrim($uri, '/')); if ( ! UTF8::is_ascii($path)) { diff --git a/config/multilang.php b/config/multilang.php index 1a7d020..ae53415 100644 --- a/config/multilang.php +++ b/config/multilang.php @@ -8,10 +8,16 @@ * List of available languages */ return array( - 'default' => 'en', - 'cookie' => 'lang', - 'languages' => array( - + 'default' => 'en', // The default language code + 'cookie' => 'lang', // The cookie name + 'hide_default' => FALSE, // You can hide the language code for the default language + 'auto_detect' => TRUE, // Auto detect the user language on the homepage + /** + * The allowed languages + * For each language, you need to give a code (2-5 chars) for the key, + * the 5 letters i18n language code, the locale and the label for the auto generated language selector menu. + */ + 'languages' => array( 'en' => array( 'i18n' => 'en_US', 'locale' => array('en_US.utf-8'), @@ -26,7 +32,6 @@ return array( 'i18n' => 'de_DE', 'locale' => array('de_DE.utf-8'), 'label' => 'deutsch', - ), - + ), ), ); diff --git a/init.php b/init.php new file mode 100644 index 0000000..da52819 --- /dev/null +++ b/init.php @@ -0,0 +1,31 @@ +/'; + +// Need a regex for all the available languages +foreach(Kohana::config('multilang.languages') as $lang => $settings) +{ + // If we hdie the default language, we make lang parameter optional + if(Kohana::config('multilang.hide_default') && Kohana::config('multilang.default') === $lang) + { + $lang_param = '(/)'; + } + else + { + $languages[] = $lang; + } +} + +Route::set('default', $lang_param, array( + 'lang' => '('. implode('|', $languages).')', +))->defaults(array( + 'controller' => 'home', + 'action' => 'index', + 'lang' => NULL, +)); \ No newline at end of file diff --git a/views/multilang/selector.php b/views/multilang/selector.php index 25bbb27..535ca17 100644 --- a/views/multilang/selector.php +++ b/views/multilang/selector.php @@ -1,13 +1,8 @@
    - $language): ?> - -
  • - $language['label'])); ?> -
  • - -
  • - -
  • - + +
  • + +
  • +
\ No newline at end of file