diff --git a/.gitattributes b/.gitattributes index feb4c02..c19cce8 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,4 +3,5 @@ /.gitignore export-ignore /.scrutinizer.yml export-ignore /.travis.yml export-ignore +/CONTRIBUTING.md export-ignore /phpunit.xml export-ignore diff --git a/composer.json b/composer.json index c690e9f..3e279ca 100644 --- a/composer.json +++ b/composer.json @@ -31,6 +31,9 @@ "Arcanedev\\Localization\\Tests\\": "tests/" } }, + "suggest": { + "ext-intl": "Use Intl extension for 'Locale' class (an identifier used to get language)." + }, "extra": { "branch-alias": { "dev-master": "1.0.x-dev" diff --git a/src/Contracts/LocalizationInterface.php b/src/Contracts/LocalizationInterface.php index 45b27bd..1f31c2c 100644 --- a/src/Contracts/LocalizationInterface.php +++ b/src/Contracts/LocalizationInterface.php @@ -178,7 +178,7 @@ public function createUrlFromUri($uri); * * @return string|false */ - public function getURLFromRouteNameTranslated($locale, $transKey, $attributes = []); + public function getUrlFromRouteName($locale, $transKey, $attributes = []); /** * Returns the translation key for a given path. @@ -187,7 +187,7 @@ public function getURLFromRouteNameTranslated($locale, $transKey, $attributes = * * @return string|false */ - public function getRouteNameFromAPath($path); + public function getRouteNameFromPath($path); /* ------------------------------------------------------------------------------------------------ | Check Functions diff --git a/src/Contracts/NegotiatorInterface.php b/src/Contracts/NegotiatorInterface.php new file mode 100644 index 0000000..72f7f97 --- /dev/null +++ b/src/Contracts/NegotiatorInterface.php @@ -0,0 +1,25 @@ + + */ +interface NegotiatorInterface +{ + /* ------------------------------------------------------------------------------------------------ + | Main Functions + | ------------------------------------------------------------------------------------------------ + */ + /** + * Negotiate the request. + * + * @param Request $request + * + * @return string + */ + public function negotiate(Request $request); +} diff --git a/src/Contracts/RouteTranslatorInterface.php b/src/Contracts/RouteTranslatorInterface.php new file mode 100644 index 0000000..0db8e5e --- /dev/null +++ b/src/Contracts/RouteTranslatorInterface.php @@ -0,0 +1,81 @@ + + */ +interface RouteTranslatorInterface +{ + /* ------------------------------------------------------------------------------------------------ + | Getters & Setters + | ------------------------------------------------------------------------------------------------ + */ + /** + * Get current route. + * + * @return string + */ + public function getCurrentRoute(); + + /** + * Set the current route. + * + * @param string $currentRoute + * + * @return self + */ + public function setCurrentRoute($currentRoute); + + /** + * Get translated routes. + * + * @return array + */ + public function getTranslatedRoutes(); + + /* ------------------------------------------------------------------------------------------------ + | Main Functions + | ------------------------------------------------------------------------------------------------ + */ + /** + * Translate routes and save them to the translated routes array (used in the localize route filter). + * + * @param string $route + * + * @return string + */ + public function trans($route); + + /** + * Returns the translation key for a given path. + * + * @param string $uri + * @param string $locale + * + * @return false|string + */ + public function getRouteNameFromPath($uri, $locale); + + /** + * Returns the translated route for the path and the url given. + * + * @param string $path - Path to check if it is a translated route + * @param string $locale - Language to check if the path exists + * + * @return string|false + */ + public function findTranslatedRouteByPath($path, $locale); + + /* ------------------------------------------------------------------------------------------------ + | Check Functions + | ------------------------------------------------------------------------------------------------ + */ + /** + * Check if has current route. + * + * @return bool + */ + public function hasCurrentRoute(); +} diff --git a/src/Exceptions/InvalidTranslationException.php b/src/Exceptions/InvalidTranslationException.php new file mode 100644 index 0000000..1987a43 --- /dev/null +++ b/src/Exceptions/InvalidTranslationException.php @@ -0,0 +1,9 @@ + + */ +class InvalidTranslationException extends LocalizationException {} diff --git a/src/Localization.php b/src/Localization.php index 62da02b..f971f28 100644 --- a/src/Localization.php +++ b/src/Localization.php @@ -4,7 +4,8 @@ use Arcanedev\Localization\Entities\LocaleCollection; use Arcanedev\Localization\Exceptions\UndefinedSupportedLocalesException; use Arcanedev\Localization\Exceptions\UnsupportedLocaleException; -use Arcanedev\Localization\Utilities\LocaleNegotiator; +use Arcanedev\Localization\Utilities\Negotiator; +use Arcanedev\Localization\Utilities\RouteTranslator; use Arcanedev\Localization\Utilities\Url; use Illuminate\Foundation\Application; @@ -63,18 +64,11 @@ class Localization implements LocalizationInterface protected $baseUrl; /** - * Translated routes collection. + * The RouteTranslator instance. * - * @var array + * @var RouteTranslator */ - protected $translatedRoutes = []; - - /** - * Current route name. - * - * @var string - */ - protected $routeName; + protected $routeTranslator; /* ------------------------------------------------------------------------------------------------ | Constructor @@ -88,11 +82,12 @@ class Localization implements LocalizationInterface * @throws UndefinedSupportedLocalesException * @throws UnsupportedLocaleException */ - public function __construct(Application $app) + public function __construct(Application $app, $routeTranslator) { $this->app = $app; $this->supportedLocales = new LocaleCollection; $this->locales = new LocaleCollection; + $this->routeTranslator = $routeTranslator; $this->defaultLocale = $this->config()->get('app.locale'); $this->getSupportedLocales(); @@ -123,16 +118,6 @@ private function request() return $this->app['request']; } - /** - * Get the translator instance. - * - * @return \Illuminate\Translation\Translator - */ - private function translator() - { - return $this->app['translator']; - } - /** * Returns default locale. * @@ -146,9 +131,9 @@ public function getDefaultLocale() /** * Return an array of all supported Locales. * - * @throws UndefinedSupportedLocalesException - * * @return LocaleCollection + * + * @throws UndefinedSupportedLocalesException */ public function getSupportedLocales() { @@ -292,13 +277,12 @@ public function getCurrentLocale() return $this->config()->get('app.locale'); } - $negotiator = new LocaleNegotiator( + $negotiator = new Negotiator( $this->defaultLocale, - $this->getSupportedLocales(), - $this->request() + $this->getSupportedLocales() ); - return $negotiator->negotiate(); + return $negotiator->negotiate($this->request()); } /** @@ -322,9 +306,7 @@ public function setBaseUrl($url) */ public function setRouteName($routeName) { - if (is_string($routeName)) { - $this->routeName = $routeName; - } + $this->routeTranslator->setCurrentRoute($routeName); } /* ------------------------------------------------------------------------------------------------ @@ -340,11 +322,7 @@ public function setRouteName($routeName) */ public function transRoute($routeName) { - if ( ! in_array($routeName, $this->translatedRoutes)) { - $this->translatedRoutes[] = $routeName; - } - - return $this->translator()->trans($routeName); + return $this->routeTranslator->trans($routeName); } /** @@ -377,14 +355,16 @@ public function getNonLocalizedURL($url = null) /** * Returns an URL adapted to $locale. * + * @todo: Refactor this beast + * * @param string|bool $locale * @param string|false $url * @param array $attributes * + * @return string|false + * * @throws UndefinedSupportedLocalesException * @throws UnsupportedLocaleException - * - * @return string|false */ public function getLocalizedURL($locale = null, $url = null, $attributes = []) { @@ -403,8 +383,12 @@ public function getLocalizedURL($locale = null, $url = null, $attributes = []) } if (empty($url)) { - if ( ! empty($this->routeName)) { - return $this->getURLFromRouteNameTranslated($locale, $this->routeName, $attributes); + if ($this->routeTranslator->hasCurrentRoute()) { + return $this->getUrlFromRouteName( + $locale, + $this->routeTranslator->getCurrentRoute(), + $attributes + ); } $url = $this->request()->fullUrl(); @@ -414,7 +398,7 @@ public function getLocalizedURL($locale = null, $url = null, $attributes = []) $locale && $translatedRoute = $this->findTranslatedRouteByUrl($url, $attributes, $this->currentLocale) ) { - return $this->getURLFromRouteNameTranslated($locale, $translatedRoute, $attributes); + return $this->getUrlFromRouteName($locale, $translatedRoute, $attributes); } $baseUrl = $this->request()->getBaseUrl(); @@ -446,8 +430,10 @@ public function getLocalizedURL($locale = null, $url = null, $attributes = []) $parsedUrl['path'] = ltrim($parsedUrl['path'], '/'); - if ($translatedRoute = $this->findTranslatedRouteByPath($parsedUrl['path'], $defaultLocale)) { - return $this->getURLFromRouteNameTranslated($locale, $translatedRoute, $attributes); + $translatedRoute = $this->routeTranslator->findTranslatedRouteByPath($parsedUrl['path'], $defaultLocale); + + if ($translatedRoute) { + return $this->getUrlFromRouteName($locale, $translatedRoute, $attributes); } if ( @@ -479,6 +465,7 @@ public function getLocalizedURL($locale = null, $url = null, $attributes = []) public function createUrlFromUri($uri) { $uri = ltrim($uri, '/'); + if (empty($this->baseUrl)) { return app('url')->to($uri); } @@ -497,10 +484,10 @@ public function createUrlFromUri($uri) * @param array $attributes * @param string $locale * + * @return string|false + * * @throws UndefinedSupportedLocalesException * @throws UnsupportedLocaleException - * - * @return string|false */ protected function findTranslatedRouteByUrl($url, $attributes, $locale) { @@ -509,8 +496,8 @@ protected function findTranslatedRouteByUrl($url, $attributes, $locale) } // check if this url is a translated url - foreach ($this->translatedRoutes as $translatedRoute) { - $routeName = $this->getURLFromRouteNameTranslated($locale, $translatedRoute, $attributes); + foreach ($this->routeTranslator->getTranslatedRoutes() as $translatedRoute) { + $routeName = $this->getUrlFromRouteName($locale, $translatedRoute, $attributes); if ($this->getNonLocalizedURL($routeName) == $this->getNonLocalizedURL($url)) { return $translatedRoute; @@ -520,26 +507,6 @@ protected function findTranslatedRouteByUrl($url, $attributes, $locale) return false; } - /** - * Returns the translated route for the path and the url given. - * - * @param string $path - Path to check if it is a translated route - * @param string $urlLocale - Language to check if the path exists - * - * @return string|false - */ - protected function findTranslatedRouteByPath($path, $urlLocale) - { - // check if this url is a translated url - foreach ($this->translatedRoutes as $translatedRoute) { - if ($this->translator()->trans($translatedRoute, [], '', $urlLocale) == rawurldecode($path)) { - return $translatedRoute; - } - } - - return false; - } - /** * Returns an URL adapted to the route name and the locale given. * @@ -547,12 +514,12 @@ protected function findTranslatedRouteByPath($path, $urlLocale) * @param string $transKey * @param array $attributes * + * @return string|false + * * @throws UndefinedSupportedLocalesException * @throws UnsupportedLocaleException - * - * @return string|false */ - public function getURLFromRouteNameTranslated($locale, $transKey, $attributes = []) + public function getUrlFromRouteName($locale, $transKey, $attributes = []) { if ( ! $this->isLocaleSupported($locale)) { throw new UnsupportedLocaleException( @@ -566,15 +533,17 @@ public function getURLFromRouteNameTranslated($locale, $transKey, $attributes = $route = ''; - if ( ! ($locale === $this->defaultLocale && $this->hideDefaultLocaleInURL())) { + if ( + ! ($locale === $this->defaultLocale && $this->hideDefaultLocaleInURL()) + ) { $route = '/' . $locale; } if ( is_string($locale) && - $this->translator()->has($transKey, $locale) + $this->routeTranslator->hasTranslation($transKey, $locale) ) { - $translation = $this->translator()->trans($transKey, [ ], '', $locale); + $translation = $this->routeTranslator->trans($transKey, $locale); $route = Url::substituteAttributes($attributes, $route . '/' . $translation); } @@ -591,25 +560,9 @@ public function getURLFromRouteNameTranslated($locale, $transKey, $attributes = * * @return string|false */ - public function getRouteNameFromAPath($path) + public function getRouteNameFromPath($path) { - $attributes = Url::extractAttributes($path); - $path = str_replace(url(), '', $path); - - if ($path[0] !== '/') { - $path = '/' . $path; - } - - $path = str_replace('/' . $this->currentLocale . '/', '', $path); - $path = trim($path, '/'); - - foreach ($this->translatedRoutes as $route) { - if (Url::substituteAttributes($attributes, $this->translator()->trans($route)) === $path) { - return $route; - } - } - - return false; + return $this->routeTranslator->getRouteNameFromPath($path, $this->currentLocale); } /* ------------------------------------------------------------------------------------------------ diff --git a/src/LocalizationServiceProvider.php b/src/LocalizationServiceProvider.php index e711154..bd080e1 100644 --- a/src/LocalizationServiceProvider.php +++ b/src/LocalizationServiceProvider.php @@ -64,6 +64,8 @@ public function boot() public function register() { $this->registerConfig(); + + $this->app->register(Providers\UtilitiesServiceProvider::class); $this->registerLocalization(); $this->app->register(Providers\RouterServiceProvider::class); } @@ -88,7 +90,9 @@ public function provides() private function registerLocalization() { $this->app->singleton('arcanedev.localization', function($app) { - return new Localization($app); + $routeTranslator = $app['arcanedev.localization.translator']; + + return new Localization($app, $routeTranslator); }); $this->addFacade( diff --git a/src/Middleware/LocalizationRoutes.php b/src/Middleware/LocalizationRoutes.php index 518c9ff..0d44469 100644 --- a/src/Middleware/LocalizationRoutes.php +++ b/src/Middleware/LocalizationRoutes.php @@ -26,7 +26,7 @@ class LocalizationRoutes extends Middleware */ public function handle(Request $request, Closure $next) { - $routeName = localization()->getRouteNameFromAPath($request->getUri()); + $routeName = localization()->getRouteNameFromPath($request->getUri()); localization()->setRouteName($routeName); diff --git a/src/Providers/UtilitiesServiceProvider.php b/src/Providers/UtilitiesServiceProvider.php new file mode 100644 index 0000000..00d6211 --- /dev/null +++ b/src/Providers/UtilitiesServiceProvider.php @@ -0,0 +1,29 @@ + + */ +class UtilitiesServiceProvider extends ServiceProvider +{ + /* ------------------------------------------------------------------------------------------------ + | Main Functions + | ------------------------------------------------------------------------------------------------ + */ + /** + * Register the service provider. + * + * @return void + */ + public function register() + { + $this->app->singleton('arcanedev.localization.translator', function ($app) { + return new RouteTranslator($app['translator']); + }); + } +} diff --git a/src/Utilities/LocaleNegotiator.php b/src/Utilities/LocaleNegotiator.php deleted file mode 100644 index da87d1a..0000000 --- a/src/Utilities/LocaleNegotiator.php +++ /dev/null @@ -1,173 +0,0 @@ - - */ -class LocaleNegotiator -{ - /* ------------------------------------------------------------------------------------------------ - | Properties - | ------------------------------------------------------------------------------------------------ - */ - /** - * Default Locale. - * - * @var string - */ - private $defaultLocale; - - /** - * @var LocaleCollection - */ - private $supportedLocales; - - /** @var Request */ - private $request; - - /* ------------------------------------------------------------------------------------------------ - | Main Functions - | ------------------------------------------------------------------------------------------------ - */ - /** - * Make LocaleNegotiator instance. - * - * @param string $defaultLocale - * @param LocaleCollection $supportedLanguages - * @param Request $request - */ - public function __construct($defaultLocale, $supportedLanguages, Request $request) - { - $this->defaultLocale = $defaultLocale; - $this->supportedLocales = $supportedLanguages; - $this->request = $request; - } - - /* ------------------------------------------------------------------------------------------------ - | Main Functions - | ------------------------------------------------------------------------------------------------ - */ - /** - * Negotiates language with the user's browser through the Accept-Language - * HTTP header or the user's host address. Language codes are generally in - * the form "ll" for a language spoken in only one country, or "ll-CC" for a - * language spoken in a particular country. For example, U.S. English is - * "en-US", while British English is "en-UK". Portuguese as spoken in - * Portugal is "pt-PT", while Brazilian Portuguese is "pt-BR". - * - * This function is based on negotiateLanguage from Pear HTTP2 - * http://pear.php.net/package/HTTP2/ - * - * Quality factors in the Accept-Language: header are supported, e.g.: - * Accept-Language: en-UK;q=0.7, en-US;q=0.6, no, dk;q=0.8 - * - * @return string The negotiated language result or app.locale. - */ - public function negotiate() - { - $matches = $this->getMatchesFromAcceptedLanguages(); - - foreach ($matches as $key => $q) { - if ($this->supportedLocales->has($key)) { - return $key; - } - } - - // If any (i.e. "*") is acceptable, return the first supported format - if (isset($matches[ '*' ])) { - /** @var \Arcanedev\Localization\Entities\Locale $locale */ - $locale = $this->supportedLocales->first(); - - return $locale->key(); - } - - if (class_exists('Locale') && ! empty($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { - $httpAcceptLanguage = Locale::acceptFromHttp($_SERVER['HTTP_ACCEPT_LANGUAGE']); - - if ($this->supportedLocales->has($httpAcceptLanguage)) { - return $httpAcceptLanguage; - } - } - - if ($this->request->server('REMOTE_HOST')) { - $remote_host = explode('.', $this->request->server('REMOTE_HOST')); - $lang = strtolower(end($remote_host)); - - if ($this->supportedLocales->has($lang)) { - return $lang; - } - } - - return $this->defaultLocale; - } - - /* ------------------------------------------------------------------------------------------------ - | Other Functions - | ------------------------------------------------------------------------------------------------ - */ - /** - * Return all the accepted languages from the browser - * @return array Matches from the header field Accept-Languages - */ - private function getMatchesFromAcceptedLanguages() - { - $matches = []; - - if ($acceptLanguages = $this->request->header('Accept-Language')) { - $acceptLanguages = explode(',', $acceptLanguages); - $generic_matches = []; - - foreach ($acceptLanguages as $option) { - $option = array_map('trim', explode(';', $option)); - $l = $option[0]; - - if (isset($option[1])) { - $q = (float) str_replace('q=', '', $option[1]); - } - else { - $q = null; - - // Assign default low weight for generic values - if ($l == '*/*') { - $q = 0.01; - } - elseif (substr($l, -1) == '*') { - $q = 0.02; - } - } - - // Unweighted values, get high weight by their position in the list - $matches[$l] = $q = isset($q) ? $q : 1000 - count($matches); - - //If for some reason the Accept-Language header only sends language with country - //we should make the language without country an accepted option, with a value - //less than it's parent. - $l_ops = explode('-', $l); - array_pop($l_ops); - - while ( ! empty($l_ops)) { - //The new generic option needs to be slightly less important than it's base - $q -= 0.001; - $op = implode('-', $l_ops); - - if (empty($generic_matches[ $op ]) || $generic_matches[$op] > $q) { - $generic_matches[ $op ] = $q; - } - - array_pop($l_ops); - } - } - - $matches = array_merge($generic_matches, $matches); - arsort($matches, SORT_NUMERIC); - } - - return $matches; - } -} diff --git a/src/Utilities/Negotiator.php b/src/Utilities/Negotiator.php new file mode 100644 index 0000000..1efab73 --- /dev/null +++ b/src/Utilities/Negotiator.php @@ -0,0 +1,215 @@ + + * + * Negotiates language with the user's browser through the Accept-Language HTTP header or the user's host address. + * Language codes are generally in the form "ll" for a language spoken in only one country, or "ll-CC" for a + * language spoken in a particular country. For example, U.S. English is "en-US", while British English is "en-UK". + * Portuguese as spoken in Portugal is "pt-PT", while Brazilian Portuguese is "pt-BR". + * + * This function is based on negotiateLanguage from Pear HTTP2 + * http://pear.php.net/package/HTTP2/ + * + * Quality factors in the Accept-Language: header are supported, e.g.: + * Accept-Language: en-UK;q=0.7, en-US;q=0.6, no, dk;q=0.8 + */ +class Negotiator implements NegotiatorInterface +{ + /* ------------------------------------------------------------------------------------------------ + | Properties + | ------------------------------------------------------------------------------------------------ + */ + /** + * Default Locale. + * + * @var string + */ + private $defaultLocale; + + /** + * @var LocaleCollection + */ + private $supportedLocales; + + /* ------------------------------------------------------------------------------------------------ + | Main Functions + | ------------------------------------------------------------------------------------------------ + */ + /** + * Make Negotiator instance. + * + * @param string $defaultLocale + * @param LocaleCollection $supportedLanguages + */ + public function __construct($defaultLocale, $supportedLanguages) + { + $this->defaultLocale = $defaultLocale; + $this->supportedLocales = $supportedLanguages; + } + + /* ------------------------------------------------------------------------------------------------ + | Main Functions + | ------------------------------------------------------------------------------------------------ + */ + /** + * Negotiate the request. + * + * @param Request $request + * + * @return string + */ + public function negotiate(Request $request) + { + $matches = $this->getMatchesFromAcceptedLanguages($request); + + foreach (array_keys($matches) as $locale) { + if ($this->isSupported($locale)) + return $locale; + } + + // If any (i.e. "*") is acceptable, return the first supported format + if (isset($matches[ '*' ])) { + /** @var \Arcanedev\Localization\Entities\Locale $locale */ + $locale = $this->supportedLocales->first(); + + return $locale->key(); + } + + if (class_exists('Locale') && ! empty($request->server('HTTP_ACCEPT_LANGUAGE'))) { + $httpAcceptLanguage = Locale::acceptFromHttp($request->server('HTTP_ACCEPT_LANGUAGE')); + + if ($this->isSupported($httpAcceptLanguage)) + return $httpAcceptLanguage; + } + + if ($request->server('REMOTE_HOST')) { + $remote_host = explode('.', $request->server('REMOTE_HOST')); + $locale = strtolower(end($remote_host)); + + if ($this->isSupported($locale)) + return $locale; + } + + return $this->defaultLocale; + } + + /* ------------------------------------------------------------------------------------------------ + | Check Functions + | ------------------------------------------------------------------------------------------------ + */ + /** + * Check if the locale is supported. + * + * @param string $locale + * + * @return bool + */ + private function isSupported($locale) + { + return $this->supportedLocales->has($locale); + } + + /* ------------------------------------------------------------------------------------------------ + | Other Functions + | ------------------------------------------------------------------------------------------------ + */ + /** + * Return all the accepted languages from the browser + * + * @param Request $request + * + * @return array - Matches from the header field Accept-Languages + */ + private function getMatchesFromAcceptedLanguages(Request $request) + { + $matches = []; + + if ($acceptLanguages = $request->header('Accept-Language')) { + $acceptLanguages = explode(',', $acceptLanguages); + + $genericMatches = $this->retrieveGenericMatches($acceptLanguages, $matches); + + $matches = array_merge($genericMatches, $matches); + arsort($matches, SORT_NUMERIC); + } + + return $matches; + } + + /** + * Get the generic matches. + * + * @param array $acceptLanguages + * @param array $matches + * + * @return array + */ + private function retrieveGenericMatches($acceptLanguages, &$matches) + { + $genericMatches = []; + + foreach ($acceptLanguages as $option) { + $option = array_map('trim', explode(';', $option)); + $locale = $option[0]; + $quality = $this->getQualityFactor($locale, $option); + + // Unweighted values, get high weight by their position in the list + $quality = isset($quality) ? $quality : 1000 - count($matches); + $matches[$locale] = $quality; + + // If for some reason the Accept-Language header only sends language with country we should make + // the language without country an accepted option, with a value less than it's parent. + $localeOptions = explode('-', $locale); + array_pop($localeOptions); + + while ( ! empty($localeOptions)) { + //The new generic option needs to be slightly less important than it's base + $quality -= 0.001; + $opt = implode('-', $localeOptions); + + if (empty($genericMatches[$opt]) || $genericMatches[$opt] > $quality) { + $genericMatches[$opt] = $quality; + } + + array_pop($localeOptions); + } + } + + return $genericMatches; + } + + /** + * Get the quality factor. + * + * @param string $locale + * @param array $option + * + * @return float|null + */ + private function getQualityFactor($locale, $option) + { + if (isset($option[1])) { + return (float) str_replace('q=', '', $option[1]); + } + + // Assign default low weight for generic values + if ($locale === '*/*') { + return 0.01; + } + + if (substr($locale, -1) === '*') { + return 0.02; + } + + return null; + } +} diff --git a/src/Utilities/RouteTranslator.php b/src/Utilities/RouteTranslator.php new file mode 100644 index 0000000..da555b1 --- /dev/null +++ b/src/Utilities/RouteTranslator.php @@ -0,0 +1,217 @@ + + */ +class RouteTranslator implements RouteTranslatorInterface +{ + /* ------------------------------------------------------------------------------------------------ + | Properties + | ------------------------------------------------------------------------------------------------ + */ + /** + * The translator instance. + * + * @var \Illuminate\Translation\Translator + */ + private $translator; + + /** + * Current route. + * + * @var string + */ + protected $currentRoute = ''; + + /** + * Translated routes collection. + * + * @var array + */ + protected $translatedRoutes = []; + + /* ------------------------------------------------------------------------------------------------ + | Constructor + | ------------------------------------------------------------------------------------------------ + */ + /** + * Create RouteTranslator instance. + * + * @param Translator $translator + */ + public function __construct(Translator $translator) + { + $this->translator = $translator; + } + + /* ------------------------------------------------------------------------------------------------ + | Getters & Setters + | ------------------------------------------------------------------------------------------------ + */ + /** + * Get current route. + * + * @return string + */ + public function getCurrentRoute() + { + return $this->currentRoute; + } + + /** + * Set the current route. + * + * @param string $currentRoute + * + * @return self + */ + public function setCurrentRoute($currentRoute) + { + if (is_string($currentRoute)) { + $this->currentRoute = $currentRoute; + } + + return $this; + } + + /** + * Get translated routes. + * + * @return array + */ + public function getTranslatedRoutes() + { + return $this->translatedRoutes; + } + + /* ------------------------------------------------------------------------------------------------ + | Main Functions + | ------------------------------------------------------------------------------------------------ + */ + /** + * Translate routes and save them to the translated routes array (used in the localize route filter). + * + * @param string $route + * @param string|null $locale + * + * @return string + * + * @throws InvalidTranslationException + */ + public function trans($route, $locale = null) + { + if ( ! in_array($route, $this->translatedRoutes)) { + $this->translatedRoutes[] = $route; + } + + return $this->translate($route, $locale); + } + + /** + * Returns the translation key for a given path. + * + * @param string $uri + * @param string $locale + * + * @return false|string + */ + public function getRouteNameFromPath($uri, $locale) + { + $attributes = Url::extractAttributes($uri); + $uri = str_replace([url(), "/$locale/"], ['', ''], $uri); + $uri = trim($uri, '/'); + + foreach ($this->translatedRoutes as $route) { + $url = Url::substituteAttributes($attributes, $this->translate($route)); + + if ($url === $uri) return $route; + } + + return false; + } + + /** + * Returns the translated route for the path and the url given. + * + * @param string $path - Path to check if it is a translated route + * @param string $locale - Language to check if the path exists + * + * @return string|false + */ + public function findTranslatedRouteByPath($path, $locale) + { + // check if this url is a translated url + foreach ($this->translatedRoutes as $route) { + if ($this->translate($route, $locale) == rawurldecode($path)) { + return $route; + } + } + + return false; + } + + /* ------------------------------------------------------------------------------------------------ + | Other Functions + | ------------------------------------------------------------------------------------------------ + */ + /** + * Get the translation for a given key. + * + * @param string $key + * @param string $locale + * + * @return string + * + * @throws InvalidTranslationException + */ + public function translate($key, $locale = null) + { + if (is_null($locale)) { + $locale = $this->translator->getLocale(); + } + + $translated = $this->translator->trans($key, [], '', $locale); + + if ( ! is_string($translated)) { + throw new InvalidTranslationException( + "The translation key [$key] for locale [$locale] has returned an array instead of string." + ); + } + + return $translated; + } + + /* ------------------------------------------------------------------------------------------------ + | Check Functions + | ------------------------------------------------------------------------------------------------ + */ + /** + * Check if has current route. + * + * @return bool + */ + public function hasCurrentRoute() + { + return ! empty($this->currentRoute); + } + + /** + * Determine if a translation exists. + * + * @param string $key + * @param string $locale + * + * @return bool + */ + public function hasTranslation($key, $locale = null) + { + return $this->translator->has($key, $locale); + } +} diff --git a/tests/LocalizationTest.php b/tests/LocalizationTest.php index 250f03a..062ee0e 100644 --- a/tests/LocalizationTest.php +++ b/tests/LocalizationTest.php @@ -30,7 +30,11 @@ public function it_can_be_instantiated() public function it_must_throw_unsupported_locale_exception_on_default_locale() { app('config')->set('app.locale', 'jp'); - new Localization($this->app); + + new Localization( + $this->app, + $this->app['arcanedev.localization.translator'] + ); } /** @@ -41,7 +45,10 @@ public function it_must_throw_unsupported_locale_exception_on_default_locale() public function it_must_throw_undefined_supported_locales_exception() { app('config')->set('localization.supported-locales', []); - new Localization($this->app); + new Localization( + $this->app, + $this->app['arcanedev.localization.translator'] + ); } /** @test */ @@ -280,52 +287,63 @@ public function it_can_get_url_from_route_name_translated() { $this->assertEquals( $this->testUrlOne . 'es/acerca', - localization()->getURLFromRouteNameTranslated('es', 'localization::routes.about') + localization()->getUrlFromRouteName('es', 'localization::routes.about') ); $this->assertEquals( $this->testUrlOne . 'en/about', - localization()->getURLFromRouteNameTranslated('en', 'localization::routes.about') + localization()->getUrlFromRouteName('en', 'localization::routes.about') ); $this->assertEquals( $this->testUrlOne . 'en/view/1', - localization()->getURLFromRouteNameTranslated('en', 'localization::routes.view', [ 'id' => 1 ]) + localization()->getUrlFromRouteName('en', 'localization::routes.view', [ 'id' => 1 ]) ); app('config')->set('localization.hide-default-in-url', true); $this->assertEquals( $this->testUrlOne . 'about', - localization()->getURLFromRouteNameTranslated('en', 'localization::routes.about') + localization()->getUrlFromRouteName('en', 'localization::routes.about') ); $this->assertEquals( $this->testUrlOne . 'es/acerca', - localization()->getURLFromRouteNameTranslated('es', 'localization::routes.about') + localization()->getUrlFromRouteName('es', 'localization::routes.about') ); $this->assertEquals( $this->testUrlOne . 'es/ver/1', - localization()->getURLFromRouteNameTranslated('es', 'localization::routes.view', ['id' => 1]) + localization()->getUrlFromRouteName('es', 'localization::routes.view', ['id' => 1]) ); $this->assertEquals( $this->testUrlOne . 'view/1', - localization()->getURLFromRouteNameTranslated('en', 'localization::routes.view', ['id' => 1]) + localization()->getUrlFromRouteName('en', 'localization::routes.view', ['id' => 1]) ); $this->assertNotEquals( $this->testUrlOne . 'en/view/1', - localization()->getURLFromRouteNameTranslated('en', 'localization::routes.view', ['id' => 1]) + localization()->getUrlFromRouteName('en', 'localization::routes.view', ['id' => 1]) ); app('config')->set('localization.hide-default-in-url', false); $this->assertNotEquals( $this->testUrlOne . 'view/1', - localization()->getURLFromRouteNameTranslated('en', 'localization::routes.view', ['id' => 1]) + localization()->getUrlFromRouteName('en', 'localization::routes.view', ['id' => 1]) ); $this->assertEquals( $this->testUrlOne . 'en/view/1', - localization()->getURLFromRouteNameTranslated('en', 'localization::routes.view', ['id' => 1]) + localization()->getUrlFromRouteName('en', 'localization::routes.view', ['id' => 1]) ); } + /** + * @test + * + * @expectedException \Arcanedev\Localization\Exceptions\UnsupportedLocaleException + * @expectedExceptionMessage Locale 'jp' is not in the list of supported locales. + */ + public function it_must_throw_an_exception() + { + localization()->getUrlFromRouteName('jp', 'localization::routes.about'); + } + /** @test */ public function it_can_get_non_localized_url() { diff --git a/tests/Utilities/LocaleNegotiatorTest.php b/tests/Utilities/NegotiatorTest.php similarity index 92% rename from tests/Utilities/LocaleNegotiatorTest.php rename to tests/Utilities/NegotiatorTest.php index 26eda31..2366ef5 100644 --- a/tests/Utilities/LocaleNegotiatorTest.php +++ b/tests/Utilities/NegotiatorTest.php @@ -3,12 +3,12 @@ use Arcanedev\Localization\Tests\TestCase; /** - * Class LocaleNegotiatorTest + * Class NegotiatorTest * * @package Arcanedev\Localization\Tests\Utilities * @author ARCANEDEV */ -class LocaleNegotiatorTest extends TestCase +class NegotiatorTest extends TestCase { /* ------------------------------------------------------------------------------------------------ | Main Functions