diff --git a/application/Config/App.php b/application/Config/App.php
index fc34b8e2ac44..a31d323b382f 100644
--- a/application/Config/App.php
+++ b/application/Config/App.php
@@ -65,6 +65,31 @@ class App extends BaseConfig
*/
public $defaultLocale = 'en';
+ /*
+ |--------------------------------------------------------------------------
+ | Negotiate Locale
+ |--------------------------------------------------------------------------
+ |
+ | If true, the current Request object will automatically determine the
+ | language to use based on the value of the Accept-Language header.
+ |
+ | If false, no automatic detection will be performed.
+ |
+ */
+ public $negotiateLocale = false;
+
+ /*
+ |--------------------------------------------------------------------------
+ | Supported Locales
+ |--------------------------------------------------------------------------
+ |
+ | If $negotiateLocale is true, this array lists the locales supported
+ | by the application in descending order of priority. If no match is
+ | found, the first locale will be used.
+ |
+ */
+ public $supportedLocales = ['en'];
+
/*
|--------------------------------------------------------------------------
| URI PROTOCOL
diff --git a/application/Config/Services.php b/application/Config/Services.php
index 1231c25c48b7..775e0e87dfe4 100644
--- a/application/Config/Services.php
+++ b/application/Config/Services.php
@@ -206,6 +206,23 @@ public static function iterator($getShared = true)
//--------------------------------------------------------------------
+ /**
+ * Responsible for loading the language string translations.
+ */
+ public static function language(string $locale = null, $getShared = true)
+ {
+ if ($getShared)
+ {
+ return self::getSharedInstance('language', $locale);
+ }
+
+ $locale = ! empty($locale) ? $locale : self::request()->getLocale();
+
+ return new \CodeIgniter\Language\Language($locale);
+ }
+
+ //--------------------------------------------------------------------
+
/**
* The file locator provides utility methods for looking for non-classes
* within namespaced folders, as well as convenience methods for
diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php
index 37f81f87f259..c0bc6c20c79f 100644
--- a/system/CodeIgniter.php
+++ b/system/CodeIgniter.php
@@ -482,6 +482,13 @@ protected function tryToRouteIt(RouteCollectionInterface $routes = null)
$this->controller = $this->router->handle($path);
$this->method = $this->router->methodName();
+ // If a {locale} segment was matched in the final route,
+ // then we need to set the correct locale on our Request.
+ if ($this->router->hasLocale())
+ {
+ $this->request->setLocale($this->router->getLocale());
+ }
+
$this->benchmark->stop('routing');
}
diff --git a/system/Common.php b/system/Common.php
index a54a7f820f15..7c34f282ade7 100644
--- a/system/Common.php
+++ b/system/Common.php
@@ -310,7 +310,24 @@ function shared_service(string $name, ...$params)
//--------------------------------------------------------------------
+if (! function_exists('lang'))
+{
+ /**
+ * A convenience method to translate a string and format it
+ * with the intl extension's MessageFormatter object.
+ *
+ * @param string $line
+ * @param array $args
+ *
+ * @return string
+ */
+ function lang(string $line, array $args=[])
+ {
+ return Services::language()->getLine($line, $args);
+ }
+}
+//--------------------------------------------------------------------
diff --git a/system/HTTP/IncomingRequest.php b/system/HTTP/IncomingRequest.php
index b627a1b2bffc..0a267df5432e 100644
--- a/system/HTTP/IncomingRequest.php
+++ b/system/HTTP/IncomingRequest.php
@@ -97,6 +97,29 @@ class IncomingRequest extends Request
*/
protected $negotiate;
+ /**
+ * The default Locale this request
+ * should operate under.
+ *
+ * @var string
+ */
+ protected $defaultLocale;
+
+ /**
+ * The current locale of the application.
+ * Default value is set in Config\App.php
+ *
+ * @var string
+ */
+ protected $locale;
+
+ /**
+ * Stores the valid locale codes.
+ *
+ * @var array
+ */
+ protected $validLocales = [];
+
//--------------------------------------------------------------------
/**
@@ -123,6 +146,90 @@ public function __construct($config, $uri = null, $body = 'php://input')
$this->uri = $uri;
$this->detectURI($config->uriProtocol, $config->baseURL);
+
+ $this->validLocales = $config->supportedLocales;
+
+ $this->detectLocale($config);
+ }
+
+ //--------------------------------------------------------------------
+
+ /**
+ * Handles setting up the locale, perhaps auto-detecting through
+ * content negotiation.
+ *
+ * @param $config
+ */
+ public function detectLocale($config)
+ {
+ $this->locale = $this->defaultLocale = $config->defaultLocale;
+
+ if (! $config->negotiateLocale)
+ {
+ return;
+ }
+
+ $this->setLocale($this->negotiate('language', $config->supportedLocales));
+ }
+
+ //--------------------------------------------------------------------
+
+ /**
+ * Returns the default locale as set in Config\App.php
+ *
+ * @return string
+ */
+ public function getDefaultLocale(): string
+ {
+ return $this->defaultLocale;
+ }
+
+ //--------------------------------------------------------------------
+
+ /**
+ * Gets the current locale, with a fallback to the default
+ * locale if none is set.
+ *
+ * @return string
+ */
+ public function getLocale(): string
+ {
+ return $this->locale ?? $this->defaultLocale;
+ }
+
+ //--------------------------------------------------------------------
+
+ /**
+ * Sets the locale string for this request.
+ *
+ * @param string $locale
+ *
+ * @return $this
+ */
+ public function setLocale(string $locale)
+ {
+ // If it's not a valid locale, set it
+ // to the default locale for the site.
+ if (! in_array($locale, $this->validLocales))
+ {
+ $locale = $this->defaultLocale;
+ }
+
+ $this->locale = $locale;
+
+ // If the intl extension is loaded, make sure
+ // that we set the locale for it... if not, though,
+ // don't worry about it.
+ try {
+ if (class_exists('\Locale', false))
+ {
+ \Locale::setDefault($locale);
+ }
+ }
+ catch (\Exception $e)
+ {}
+
+ return $this;
}
//--------------------------------------------------------------------
diff --git a/system/HTTP/Request.php b/system/HTTP/Request.php
index 147b123b76ad..7e93b494edac 100644
--- a/system/HTTP/Request.php
+++ b/system/HTTP/Request.php
@@ -57,22 +57,6 @@ class Request extends Message implements RequestInterface
*/
protected $proxyIPs;
- /**
- * The default Locale this request
- * should operate under.
- *
- * @var string
- */
- protected $defaultLocale;
-
- /**
- * The current locale of the application.
- * Default value is set in Config\App.php
- *
- * @var string
- */
- protected $locale;
-
//--------------------------------------------------------------------
/**
@@ -84,49 +68,6 @@ class Request extends Message implements RequestInterface
public function __construct($config, $uri=null)
{
$this->proxyIPs = $config->proxyIPs;
-
- $this->locale = $this->defaultLocale = $config->defaultLocale;
- }
-
- //--------------------------------------------------------------------
-
- /**
- * Returns the default locale as set in Config\App.php
- *
- * @return string
- */
- public function getDefaultLocale(): string
- {
- return $this->defaultLocale;
- }
-
- //--------------------------------------------------------------------
-
- /**
- * Gets the current locale, with a fallback to the default
- * locale if none is set.
- *
- * @return string
- */
- public function getLocale(): string
- {
- return $this->locale ?? $this->defaultLocale;
- }
-
- //--------------------------------------------------------------------
-
- /**
- * Sets the locale string for this request.
- *
- * @param string $locale
- *
- * @return $this
- */
- public function setLocale(string $locale)
- {
- $this->locale = $locale;
-
- return $this;
}
//--------------------------------------------------------------------
diff --git a/system/HTTP/RequestInterface.php b/system/HTTP/RequestInterface.php
index 9b8ddfa14441..7c060411b3aa 100644
--- a/system/HTTP/RequestInterface.php
+++ b/system/HTTP/RequestInterface.php
@@ -84,34 +84,4 @@ public function getMethod($upper = false): string;
public function getServer($index = null, $filter = null);
//--------------------------------------------------------------------
-
- /**
- * Returns the default locale as set in Config\App.php
- *
- * @return string
- */
- public function getDefaultLocale(): string;
-
- //--------------------------------------------------------------------
-
- /**
- * Gets the current locale, with a fallback to the default
- * locale if none is set.
- *
- * @return string
- */
- public function getLocale(): string;
-
- //--------------------------------------------------------------------
-
- /**
- * Sets the locale string for this request.
- *
- * @param string $locale
- *
- * @return $this
- */
- public function setLocale(string $locale);
-
- //--------------------------------------------------------------------
}
diff --git a/system/Language/Language.php b/system/Language/Language.php
new file mode 100644
index 000000000000..63a604c0348c
--- /dev/null
+++ b/system/Language/Language.php
@@ -0,0 +1,175 @@
+locale = $locale;
+
+ if (class_exists('\MessageFormatter'))
+ {
+ $this->intlSupport = true;
+ };
+ }
+
+ //--------------------------------------------------------------------
+
+ /**
+ * Parses the language string for a file, loads the file, if necessary,
+ * getting
+ *
+ * @param string $line
+ * @param array $args
+ *
+ * @return string
+ */
+ public function getLine(string $line, array $args = []): string
+ {
+ // Parse out the file name and the actual alias.
+ // Will load the language file and strings.
+ $line = $this->parseLine($line);
+
+ $output = ! empty($this->language[$line]) ? $this->language[$line] : $line;
+
+ // Do advanced message formatting here
+ // if the 'intl' extension is available.
+ if ($this->intlSupport && count($args))
+ {
+ $output = \MessageFormatter::formatMessage($this->locale, $line, $args);
+ }
+
+ return $output;
+ }
+
+ //--------------------------------------------------------------------
+
+ /**
+ * Parses the language string which should include the
+ * filename as the first segment (separated by period).
+ *
+ * @param string $line
+ *
+ * @return string
+ */
+ protected function parseLine(string $line): string
+ {
+ if (strpos($line, '.') === false)
+ {
+ throw new \InvalidArgumentException('No language file specified in line: '.$line);
+ }
+
+ $file = substr($line, 0, strpos($line, '.'));
+ $line = substr($line, strlen($file)+1);
+
+ if (! array_key_exists($line, $this->language))
+ {
+ $this->load($file, $this->locale);
+ }
+
+ return $this->language[$line];
+ }
+
+ //--------------------------------------------------------------------
+
+ /**
+ * Loads a language file in the current locale. If $return is true,
+ * will return the file's contents, otherwise will merge with
+ * the existing language lines.
+ *
+ * @param string $file
+ * @param string $locale
+ * @param bool $return
+ *
+ * @return array|null
+ */
+ protected function load(string $file, string $locale, bool $return = false)
+ {
+ if (in_array($file, $this->loadedFiles))
+ {
+ return [];
+ }
+
+ $lang = [];
+
+ $path = "Language/{$locale}/{$file}.php";
+
+ $lang = $this->requireFile($path);
+
+ // Don't load it more than once.
+ $this->loadedFiles[] = $file;
+
+ if ($return)
+ {
+ return $lang;
+ }
+
+ // Merge our string
+ $this->language = array_merge($this->language, $lang);
+ }
+
+ //--------------------------------------------------------------------
+
+ /**
+ * A simple method for including files that can be
+ * overridden during testing.
+ *
+ * @todo - should look into loading from other locations, also probably...
+ *
+ * @param string $path
+ *
+ * @return array
+ */
+ protected function requireFile(string $path): array
+ {
+ foreach ([APPPATH, BASEPATH] as $folder)
+ {
+ if (! is_file($folder.$path))
+ {
+ continue;
+ }
+
+ return require_once $folder.$path;
+ }
+
+ return [];
+ }
+
+ //--------------------------------------------------------------------
+
+}
diff --git a/system/Router/Router.php b/system/Router/Router.php
index 5472fd088eb5..10c23d24a107 100644
--- a/system/Router/Router.php
+++ b/system/Router/Router.php
@@ -106,6 +106,12 @@ class Router implements RouterInterface
*/
protected $matchedRoute = null;
+ /**
+ * The locale that was detected in a route.
+ * @var string
+ */
+ protected $detectedLocale = null;
+
//--------------------------------------------------------------------
/**
@@ -298,6 +304,31 @@ public function setTranslateURIDashes($val = false): self
//--------------------------------------------------------------------
+ /**
+ * Returns true/false based on whether the current route contained
+ * a {locale} placeholder.
+ *
+ * @return bool
+ */
+ public function hasLocale()
+ {
+ return (bool)$this->detectedLocale;
+ }
+
+ //--------------------------------------------------------------------
+
+ /**
+ * Returns the detected locale, if any, or null.
+ *
+ * @return string
+ */
+ public function getLocale()
+ {
+ return $this->detectedLocale;
+ }
+
+ //--------------------------------------------------------------------
+
/**
* Compares the uri string against the routes that the
* RouteCollection class defined for us, attempting to find a match.
@@ -306,6 +337,7 @@ public function setTranslateURIDashes($val = false): self
* @param string $uri The URI path to compare against the routes
*
* @return bool Whether the route was matched or not.
+ * @throws \CodeIgniter\Router\RedirectException
*/
protected function checkRoutes(string $uri): bool
{
@@ -320,9 +352,27 @@ protected function checkRoutes(string $uri): bool
// Loop through the route array looking for wildcards
foreach ($routes as $key => $val)
{
+ // Are we dealing with a locale?
+ if (strpos($key, '{locale}') !== false)
+ {
+ $localeSegment = array_search('{locale}', explode('/', $key));
+
+ // Replace it with a regex so it
+ // will actually match.
+ $key = str_replace('{locale}', '[^/]+', $key);
+ }
+
// Does the RegEx match?
if (preg_match('#^'.$key.'$#', $uri, $matches))
{
+ // Store our locale so CodeIgniter object can
+ // assign it to the Request.
+ if (isset($localeSegment))
+ {
+ $this->detectedLocale = (explode('/', $uri))[$localeSegment];
+ unset($localeSegment);
+ }
+
// Are we using Closures? If so, then we need
// to collect the params into an array
// so it can be passed to the controller method later.
@@ -405,7 +455,7 @@ public function autoRoute(string $uri)
$file = APPPATH.'Controllers/'.$this->directory.$this->controller.'.php';
if (file_exists($file))
{
- include $file;
+ include_once $file;
}
// Ensure the controller stores the fully-qualified class name
diff --git a/tests/_support/Config/MockAppConfig.php b/tests/_support/Config/MockAppConfig.php
index f9bf06b3bbe8..96d4d81f6d0e 100644
--- a/tests/_support/Config/MockAppConfig.php
+++ b/tests/_support/Config/MockAppConfig.php
@@ -6,10 +6,10 @@ class MockAppConfig
public $uriProtocol = 'REQUEST_URI';
- public $cookiePrefix = '';
- public $cookieDomain = '';
- public $cookiePath = '/';
- public $cookieSecure = false;
+ public $cookiePrefix = '';
+ public $cookieDomain = '';
+ public $cookiePath = '/';
+ public $cookieSecure = false;
public $cookieHTTPOnly = false;
public $proxyIPs = '';
@@ -23,5 +23,7 @@ class MockAppConfig
public $CSPEnabled = false;
- public $defaultLocale = 'en';
+ public $defaultLocale = 'en';
+ public $negotiateLocale = false;
+ public $supportedLocales = ['en', 'es'];
}
diff --git a/tests/_support/Language/MockLanguage.php b/tests/_support/Language/MockLanguage.php
new file mode 100644
index 000000000000..dae3c4ebd655
--- /dev/null
+++ b/tests/_support/Language/MockLanguage.php
@@ -0,0 +1,48 @@
+data = $data;
+
+ return $this;
+ }
+
+ //--------------------------------------------------------------------
+
+ /**
+ * Provides an override that allows us to set custom
+ * data to be returned easily during testing.
+ *
+ * @param string $path
+ *
+ * @return array|mixed
+ */
+ protected function requireFile(string $path): array
+ {
+ return $this->data ?? [];
+ }
+
+ //--------------------------------------------------------------------
+
+}
diff --git a/tests/system/HTTP/IncomingRequestTest.php b/tests/system/HTTP/IncomingRequestTest.php
index 138ba07139ca..b618f4f7d729 100644
--- a/tests/system/HTTP/IncomingRequestTest.php
+++ b/tests/system/HTTP/IncomingRequestTest.php
@@ -187,4 +187,39 @@ public function testFetchGlobalFiltersSelectedValues()
}
//--------------------------------------------------------------------
+
+ public function testStoresDefaultLocale()
+ {
+ $config = new App();
+
+ $this->assertEquals($config->defaultLocale, $this->request->getDefaultLocale());
+ $this->assertEquals($config->defaultLocale, $this->request->getLocale());
+ }
+
+ //--------------------------------------------------------------------
+
+ public function testSetLocaleSaves()
+ {
+ $this->request->setLocale('en');
+
+ $this->assertEquals('en', $this->request->getLocale());
+ }
+
+ //--------------------------------------------------------------------
+
+ public function testNegotiatesLocale()
+ {
+ $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'es; q=1.0, en; q=0.5';
+
+ $config = new App();
+ $config->negotiateLocale = true;
+ $config->supportedLocales = ['en', 'es'];
+
+ $request = new IncomingRequest($config, new URI());
+
+ $this->assertEquals($config->defaultLocale, $request->getDefaultLocale());
+ $this->assertEquals('es', $request->getLocale());
+ }
+
+ //--------------------------------------------------------------------
}
diff --git a/tests/system/HTTP/RequestTest.php b/tests/system/HTTP/RequestTest.php
index bc5ee88187e1..05a9a0e6e18a 100644
--- a/tests/system/HTTP/RequestTest.php
+++ b/tests/system/HTTP/RequestTest.php
@@ -55,24 +55,4 @@ public function testMethodReturnsRightStuff()
//--------------------------------------------------------------------
- public function testStoresDefaultLocale()
- {
- $config = new App();
-
- $this->assertEquals($config->defaultLocale, $this->request->getDefaultLocale());
- $this->assertEquals($config->defaultLocale, $this->request->getLocale());
- }
-
- //--------------------------------------------------------------------
-
- public function testSetLocaleSaves()
- {
- $this->request->setLocale('en');
-
- $this->assertEquals('en', $this->request->getLocale());
- }
-
- //--------------------------------------------------------------------
-
-
}
diff --git a/tests/system/Language/LanguageTest.php b/tests/system/Language/LanguageTest.php
new file mode 100644
index 000000000000..0c06abb4c46d
--- /dev/null
+++ b/tests/system/Language/LanguageTest.php
@@ -0,0 +1,46 @@
+setExpectedException('\InvalidArgumentException');
+
+ $lang->getLine('something');
+ }
+
+ //--------------------------------------------------------------------
+
+ public function testGetLineReturnsLine()
+ {
+ $lang = new MockLanguage('en');
+
+ $lang->setData([
+ 'bookSaved' => 'We kept the book free from the boogeyman',
+ 'booksSaved' => 'We saved some more'
+ ]);
+
+ $this->assertEquals('We saved some more', $lang->getLine('books.booksSaved'));
+ }
+
+ //--------------------------------------------------------------------
+
+ public function testGetLineFormatsMessage()
+ {
+ // No intl extension? then we can't test this - go away....
+ if (! class_exists('\MessageFormatter')) return;
+
+ $lang = new MockLanguage('en');
+
+ $lang->setData([
+ 'books' => '{0, number, integer} books have been saved.'
+ ]);
+
+ $this->assertEquals('45 books have been saved.', $lang->getLine('books.books', [91/2]));
+ }
+
+ //--------------------------------------------------------------------
+
+}
diff --git a/tests/system/Router/RouterTest.php b/tests/system/Router/RouterTest.php
index 48ec17e4e3c2..a1a8078b2ea7 100644
--- a/tests/system/Router/RouterTest.php
+++ b/tests/system/Router/RouterTest.php
@@ -26,6 +26,7 @@ public function setUp()
'posts/(:num)/edit' => 'Blog::edit/$1',
'books/(:num)/(:alpha)/(:num)' => 'Blog::show/$3/$1',
'closure/(:num)/(:alpha)' => function ($num, $str) { return $num.'-'.$str; },
+ '{locale}/pages' => 'App\Pages::list_all',
];
$this->collection->map($routes);
@@ -178,4 +179,20 @@ public function testAutoRouteFindsControllerWithSubfolder()
}
//--------------------------------------------------------------------
+
+ /**
+ * @group single
+ */
+ public function testDetectsLocales()
+ {
+ $router = new Router($this->collection);
+
+ $router->handle('fr/pages');
+
+ $this->assertTrue($router->hasLocale());
+ $this->assertEquals('fr', $router->getLocale());
+ }
+
+ //--------------------------------------------------------------------
+
}
diff --git a/user_guide_src/source/general/common_functions.rst b/user_guide_src/source/general/common_functions.rst
index 39b1606db1e2..1f28504fc2a8 100644
--- a/user_guide_src/source/general/common_functions.rst
+++ b/user_guide_src/source/general/common_functions.rst
@@ -54,6 +54,15 @@ Service Accessors
For full details, see the :doc:`helpers` page.
+.. php:function:: lang(string $line[, array $args]): string
+
+ :param string $line: The line of text to retrieve
+ :param array $args: An array of data to substitute for placeholders.
+
+ Retrieves a locale-specific file based on an alias string.
+
+ For more information, see the :doc:`Localization ` page.
+
.. php:function:: session( [$key] )
:param string $key: The name of the session item to check for.
@@ -72,7 +81,7 @@ Service Accessors
A convenience method that provides quick access to the Timer class. You can pass in the name
of a benchmark point as the only parameter. This will start timing from this point, or stop
timing if a timer with this name is already running.
-
+
Example::
// Get an instance
diff --git a/user_guide_src/source/general/routing.rst b/user_guide_src/source/general/routing.rst
index f1ff7be6aef9..89f5d3a94dcc 100644
--- a/user_guide_src/source/general/routing.rst
+++ b/user_guide_src/source/general/routing.rst
@@ -18,7 +18,7 @@ For example, let’s say you want your URLs to have this prototype::
example.com/product/2/
example.com/product/3/
example.com/product/4/
-
+
Normally the second segment of the URL is reserved for the method name, but in the example
above it instead has a product ID. To overcome this, CodeIgniter allows you to remap the URI handler.
@@ -46,7 +46,7 @@ Placeholders
A typical route might look something like this::
$routes->add('product/:num', 'App\Catalog::productLookup');
-
+
In a route, the first parameter contains the URI to be matched, while the second parameter
contains the destination it should be re-routed to. In the above example, if the literal word
"product" is found in the first segment of the URL, and a number is found in the second segment,
@@ -56,15 +56,18 @@ Placeholders are simply strings that represent a Regular Expression pattern. Dur
process, these placeholders are replaced with the value of the Regular Expression. They are primarily
used for readability.
-The following placeholders are available for you to use in your routes:
+The following placeholders are available for you to use in your routes:
-* **(:any)** will match all characters from that point to the end of the URI. This may include multiple URI segments.
+* **(:any)** will match all characters from that point to the end of the URI. This may include multiple URI segments.
* **(:segment)** will match any character except for a forward slash (/) restricting the result to a single segment.
* **(:num)** will match any integer.
* **(:alpha)** will match any string of alphabetic characters
* **(:alphanum)** will match any string of alphabetic characters or integers, or any combination of the two.
* **(:hash)** is the same as **:segment**, but can be used to easily see which routes use hashed ids (see the :doc:`Model ` docs).
+.. note:: **{locale}** cannot be used as a placeholder or other part of the route, as it is reserved for use
+ in :doc:`localization `.
+
Examples
========
@@ -81,12 +84,12 @@ A URL containing the segments "blog/joe" will be remapped to the “\Blogs” cl
The ID will be set to “34”::
$routes->add('product/(:any)', 'Catalog::productLookup');
-
+
A URL with “product” as the first segment, and anything in the second will be remapped to the “\Catalog” class
and the “productLookup” method::
$routes->add('product/(:num)', 'Catalog::productLookupByID/$1';
-
+
A URL with “product” as the first segment, and a number in the second will be remapped to the “\Catalog” class
and the “productLookupByID” method passing in the match as a variable to the method.
@@ -130,7 +133,7 @@ For example, if a user accesses a password protected area of your web applicatio
redirect them back to the same page after they log in, you may find this example useful::
$routes->add('login/(.+)', 'Auth::login/$1');
-
+
For those of you who don’t know regular expressions and want to learn more about them,
`regular-expressions.info `_ might be a good starting point.
@@ -161,7 +164,7 @@ define an array of routes and then pass it as the first parameter to the `map()`
$routes = [];
$routes['product/(:num)'] = 'Catalog::productLookupById';
$routes['product/(:alphanum)'] = 'Catalog::productLookupByName';
-
+
$collection->map($routes);
@@ -196,7 +199,7 @@ extensive set of routes that all share the opening string, like when building an
$routes->add('users', 'Admin\Users::index');
$routes->add('blog', 'Admin\Blog::index');
});
-
+
This would prefix the 'users' and 'blog" URIs with "admin", handling URLs like ``/admin/users`` and ``/admin/blog``.
It is possible to nest groups within groups for finer organization if you need it::
diff --git a/user_guide_src/source/intro/requirements.rst b/user_guide_src/source/intro/requirements.rst
index ef27423e67bc..27270babd806 100644
--- a/user_guide_src/source/intro/requirements.rst
+++ b/user_guide_src/source/intro/requirements.rst
@@ -4,13 +4,16 @@ Server Requirements
`PHP `_ version 7.0 or newer is required.
+While not required, the *intl* extension is recommended, and some portions of the
+framework will provided enhanced functionality when it's present.
+
A database is required for most web application programming.
Currently supported databases are:
- MySQL (5.1+) via the *MySQLi* driver
- PostgreSQL via the *Postgre* driver
-Not all of the drivers have been converted/rewritten for CodeIgniter4.
+Not all of the drivers have been converted/rewritten for CodeIgniter4.
The list below shows the outstanding ones.
- MySQL (5.1+) via the *pdo* driver
diff --git a/user_guide_src/source/libraries/index.rst b/user_guide_src/source/libraries/index.rst
index 78af41551b5d..3d22070b09a2 100644
--- a/user_guide_src/source/libraries/index.rst
+++ b/user_guide_src/source/libraries/index.rst
@@ -9,6 +9,7 @@ Library Reference
caching
cli
content_negotiation
+ localization
curlrequest
incomingrequest
message
diff --git a/user_guide_src/source/libraries/localization.rst b/user_guide_src/source/libraries/localization.rst
new file mode 100644
index 000000000000..d0152267270e
--- /dev/null
+++ b/user_guide_src/source/libraries/localization.rst
@@ -0,0 +1,228 @@
+############
+Localization
+############
+
+.. contents::
+ :local:
+
+********************
+Working With Locales
+********************
+
+CodeIgniter provides several tools to help you localize your application for different languages. While full
+localization of an application is a complex subject, it's simple to swap out strings in your application
+with different supported languages.
+
+Language strings are stored in the **application/Language** directory, with a sub-directory for each
+supported language::
+
+ /application
+ /Language
+ /en
+ app.php
+ /fr
+ app.php
+
+.. important:: Locale detection only works for web-based requests that use the IncomingRequest class.
+ Command-line requests will not have these features.
+
+Configuring the Locale
+======================
+
+Every site will have a default language/locale they operate in. This can be set in **Config/App.php**::
+
+ public $defaultLocale = 'en';
+
+The value can be any string that your application uses to manage text strings and other formats. It is
+recommended that a [BCP 47](http://www.rfc-editor.org/rfc/bcp/bcp47.txt) language code is used. This results in
+language codes like en-US for American English, or fr-FR, for French/France. A more readable introduction
+to this can be found on the [W3C's site](https://www.w3.org/International/articles/language-tags/).
+
+The system is smart enough to fallback to more generic language codes if an exact match
+cannot be found. If the locale code was set to **en_US** and we only have language files setup for **en**
+then those will be used since nothing exists for the more specific **en_US**. If, however, a language
+directory existed at **application/Language/en_US** then that we be used first.
+
+Locale Detection
+================
+
+There are two methods supported to detect the correct locale during the request. The first is a "set and forget"
+method that will automatically perform :doc:`content negotiation ` for you to
+determine the correct locale to use. The second method allows you to specify a segment in your routes that
+will be used to set the locale.
+
+Content Negotiation
+-------------------
+
+You can setup content negotiation to happen automatically by setting two additional settings in Config/App.
+The first value tells the Request class that we do want to negotiate a locale, so simply set it to true::
+
+ public $negotiateLocale = true;
+
+Once this is enabled, the system will automatically negotiate the correct language based upon an array
+of locales that you have defined in ``$supportLocales``. If no match is found between the languages
+that you support, and the requested language, the first item in $supportedLocales will be used. In
+the following example, the **en** locale would be used if no match is found::
+
+ public $supportedLocales = ['en', 'es', 'fr_FR'];
+
+In Routes
+---------
+
+The second method uses a custom placeholder to detect the desired locale and set it on the Request. The
+placeholder ``{locale}`` can be placed as a segment in your route. If present, the contents of the matching
+segment will be your locale::
+
+ $routes->get('{locale}/books', 'App\Books::index');
+
+In this example, if the user tried to visit ``http://example.com/fr/books``, then the locale would be
+set to ``fr``, assuming it was configured as a valid locale.
+
+.. note:: If the value doesn't match a valid locale as defined in the App configuration file, the default
+ locale will be used in it's place.
+
+Retrieving the Current Locale
+=============================
+
+The current locale can always be retrieved from the IncomingRequest object, through the ``getLocale()` method.
+If your controller is extending ``CodeIgniter\Controller``, this will be available through ``$this->request``::
+
+ namespace App\Controllers;
+
+ class UserController extends \CodeIgniter\Controller
+ {
+ public function index()
+ {
+ $locale = $this->request->getLocale();
+ }
+ }
+
+Alternatively, you can use the :doc:`Services class ` to retrieve the current request::
+
+ $locale = service('request')->getLocale();
+
+*********************
+Language Localization
+*********************
+
+Creating Language Files
+=======================
+
+Language do not have any specific naming convention that are required. The file should be named logically to
+describe the type of content it holds. For example, let's say you want to create a file containing error messages.
+You might name it simply: **Errors.php**.
+
+Within the file you would return an array, where each element in the array has a language key and the string to return::
+
+ 'language_key' => 'The actual message to be shown.'
+
+.. note:: It's good practice to use a common prefix for all messages in a given file to avoid collisions with
+ similarly named items in other files. For example, if you are creating error messages you might prefix them
+ with error\_
+
+::
+
+ return [
+ 'errorEmailMissing' => 'You must submit an email address',
+ 'errorURLMissing' => 'You must submit a URL',
+ 'errorUsernameMissing' => 'You must submit a username',
+ ];
+
+Basic Usage
+===========
+
+You can use the ``lang()`` helper function to retrieve text from any of the language files, by passing the
+filename and the language key as the first paremeter, separated by a period (.). For example, to load the
+``errorEmailMissing`` string from the ``Errors`` language file, you would do the following::
+
+ echo lang('Errors.errorEmailMissing');
+
+If the requested language key doesn't exist in the file for the current locale, the string will be passed
+back, unchanged. In this example, it would return 'Errors.errorEmailMissing' if it didn't exist.
+
+Replacing Parameters
+--------------------
+
+.. note:: The following functions all require the `intl `_ extension to
+ be loaded on your system in order to work. If the extension is not loaded, no replacement will be attempted.
+ A great overview can be found over at `Sitepoint `_.
+
+You can pass an array of values to replace placeholders in the language string as the second parameter to the
+``lang()`` function. This allows for very simple number translations and formatting::
+
+ // The language file, Tests.php:
+ return [
+ "apples" => "I have {0, number} apples.",
+ "men" => "I have {1, number} men out-performed the remaining {0, number}",
+ "namedApples" => "I have {number_apples, number, integer} apples.",
+ ];
+
+ // Displays "I have 3 apples."
+ echo lang('Tests.apples', [ 3 ]);
+
+The first item in the placeholder corresponds to the index of the item in the array, if it's numerical::
+
+ // Displays "The top 23 men out-performed the remaining 20"
+ echo lang('Tests.men', [20, 23]);
+
+You can also use named keys to make it easier to keep things straight, if you'd like::
+
+ // Displays "I have 3 apples."
+ echo lang("Tests.namedApples", ['number_apples' => 3]);
+
+Obviously, you can do more than just number replacement. According to the
+`official ICU docs `_ for the underlying
+library, the following types of data can be replaced:
+
+* numbers - integer, currency, percent
+* dates - short, medium, long, full
+* time - short, medium, long, full
+* spellout - spells out numbers (i.e. 34 becomes thirty-four)
+* ordinal
+* duration
+
+Here are a few examples::
+
+ // The language file, Tests.php
+ return [
+ 'shortTime' => 'The time is now {0, time, short}.',
+ 'mediumTime' => 'The time is now {0, time, medium}.',
+ 'longTime' => 'The time is now {0, time, long}.',
+ 'fullTime' => 'The time is now {0, time, full}.',
+ 'shortDate' => 'The date is now {0, date, short}.',
+ 'mediumDate' => 'The date is now {0, date, medium}.',
+ 'longDate' => 'The date is now {0, date, long}.',
+ 'fullDate' => 'The date is now {0, date, full}.',
+ 'spelledOut' => '34 is {0, spellout}',
+ 'ordinal' => 'The ordinal is {0, ordinal}',
+ 'duration' => 'It has been {0, duration}',
+ ];
+
+ // Displays "The time is now 11:18 PM"
+ echo lang('Tests.shortTime', [time()]);
+ // Displays "The time is now 11:18:50 PM"
+ echo lang('Tests.mediumTime', [time()]);
+ // Displays "The time is now 11:19:09 PM CDT"
+ echo lang('Tests.longTime', [time()]);
+ // Displays "The time is now 11:19:26 PM Central Daylight Time"
+ echo lang('Tests.fullTime', [time()]);
+
+ // Displays "The date is now 8/14/16"
+ echo lang('Tests.shortDate', [time()]);
+ // Displays "The date is now Aug 14, 2016"
+ echo lang('Tests.mediumDate', [time()]);
+ // Displays "The date is now August 14, 2016"
+ echo lang('Tests.longDate', [time()]);
+ // Displays "The date is now Sunday, August 14, 2016"
+ echo lang('Tests.fullDate', [time()]);
+
+ // Displays "34 is thirty-four"
+ echo lang('Tests.spelledOut', [34]);
+
+ // Displays "It has been 408,676:24:35"
+ echo lang('Tests.ordinal', [time()]);
+
+You should be sure to read up on the MessageFormatter class and the underlying ICU formatting to get a better
+idea on what capabilities it has, like permorming conditional replacement, pluralization, and more. Both of the links provided
+earlier will give you an excellent idea as to the options available.
+