├── LICENSE.md ├── README.md ├── composer.json ├── pint.json ├── publishable └── config │ └── translatable.php └── src ├── Facades └── Translatable.php ├── Helpers └── helpers.php ├── Macros └── TranslatableRoutesMacro.php ├── Middleware └── TranslatableRoutesHandler.php ├── Traits └── HasTranslations.php ├── Translatable.php └── TranslatableServiceProvider.php /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Sertxu Developer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |
Copyright © 2022 Sertxu Developer
177 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sertxudeveloper/laravel-translatable", 3 | "description": "Manage localized routes and use translatable models in a Laravel app.", 4 | "keywords": [ 5 | "laravel", 6 | "translatable", 7 | "localization" 8 | ], 9 | "homepage": "https://github.com/sertxudeveloper/laravel-translatable", 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Sertxu Developer", 14 | "email": "dev.sertxu@gmail.com", 15 | "role": "Developer" 16 | } 17 | ], 18 | "require": { 19 | "php": "^8.1|^8.2", 20 | "illuminate/contracts": "^10.0|^11.0", 21 | "illuminate/support": "^10.0|^11.0", 22 | "illuminate/database": "^10.0|^11.0" 23 | }, 24 | "require-dev": { 25 | "orchestra/testbench": "^8.0|^9.0", 26 | "phpunit/phpunit": "^9.5|^10.0" 27 | }, 28 | "autoload": { 29 | "psr-4": { 30 | "SertxuDeveloper\\Translatable\\": "src/" 31 | } 32 | }, 33 | "autoload-dev": { 34 | "psr-4": { 35 | "SertxuDeveloper\\Translatable\\Tests\\": "tests/", 36 | "SertxuDeveloper\\Translatable\\Tests\\Database\\Factories\\": "tests/database/factories" 37 | } 38 | }, 39 | "scripts": { 40 | "test": "vendor/bin/phpunit", 41 | "test-coverage": "vendor/bin/phpunit --coverage-html coverage" 42 | }, 43 | "config": { 44 | "sort-packages": true 45 | }, 46 | "extra": { 47 | "laravel": { 48 | "providers": [ 49 | "SertxuDeveloper\\Translatable\\TranslatableServiceProvider" 50 | ], 51 | "aliases": { 52 | "Translatable": "SertxuDeveloper\\Translatable\\Facades\\Translatable" 53 | } 54 | } 55 | }, 56 | "minimum-stability": "dev", 57 | "prefer-stable": true 58 | } 59 | -------------------------------------------------------------------------------- /pint.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "laravel", 3 | "rules": { 4 | "simplified_null_return": true, 5 | "braces": { 6 | "position_after_anonymous_constructs": "same", 7 | "position_after_functions_and_oop_constructs": "same" 8 | }, 9 | "new_with_braces": { 10 | "anonymous_class": false, 11 | "named_class": false 12 | }, 13 | "curly_braces_position": { 14 | "anonymous_classes_opening_brace": "same_line", 15 | "functions_opening_brace": "same_line", 16 | "allow_single_line_empty_anonymous_classes": true, 17 | "allow_single_line_anonymous_functions": true 18 | }, 19 | "phpdoc_types": { 20 | "groups": [ 21 | "simple" 22 | ] 23 | }, 24 | "no_useless_else": true, 25 | "not_operator_with_successor_space": false 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /publishable/config/translatable.php: -------------------------------------------------------------------------------- 1 | ['es', 'en', 'it'], 9 | 10 | /** 11 | * The default locale in your project, it will be used if the requested locale is not available. 12 | */ 13 | 'fallback_locale' => 'en', 14 | 15 | /** 16 | * Hide the default locale from the URL. 17 | */ 18 | 'hide_fallback_locale' => true, 19 | 20 | /** 21 | * The translations' table suffix. 22 | */ 23 | 'table_suffix' => '_translations', 24 | ]; 25 | -------------------------------------------------------------------------------- /src/Facades/Translatable.php: -------------------------------------------------------------------------------- 1 | routeIs($patterns)) { 23 | return true; 24 | } 25 | 26 | /** Check route with locales */ 27 | foreach (config('translatable.locales') as $locale) { 28 | foreach ($patterns as $pattern) { 29 | if (request()->routeIs("$locale.$pattern")) { 30 | return true; 31 | } 32 | } 33 | } 34 | 35 | return false; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Macros/TranslatableRoutesMacro.php: -------------------------------------------------------------------------------- 1 | getPathInfo()); 22 | 23 | // Dump the first element (empty string) as getPathInfo() always returns a leading slash 24 | array_shift($params); 25 | 26 | if (count($params) > 0) { 27 | $locale = $params[0]; 28 | 29 | if (Translatable::checkLocaleInSupportedLocales($locale)) { 30 | App::setLocale($locale); 31 | 32 | if (Translatable::isFallbackLocaleHidden() && Translatable::isFallbackLocale($locale)) { 33 | array_shift($params); 34 | $params = implode('/', $params); 35 | $url = $request->root()."/$params"; 36 | $query = http_build_query($request->all()); 37 | $url = ($query) ? "$url?$query" : $url; 38 | 39 | Session::reflash(); 40 | 41 | return new RedirectResponse($url, 302, ['Vary' => 'Accept-Language']); 42 | } 43 | } 44 | } 45 | 46 | return $next($request); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Traits/HasTranslations.php: -------------------------------------------------------------------------------- 1 | table.config('translatable.table_suffix')) 23 | ->where([ 24 | ['locale', $lang], [ 25 | $this->getKeyName(), $this->getKey(), 26 | ], 27 | ])->first(); 28 | 29 | return (!isset($translation->$attribute)) ? $this[$attribute] : $translation->$attribute; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Translatable.php: -------------------------------------------------------------------------------- 1 | getPathInfo()); 28 | 29 | // Dump the first element (empty string) as getPathInfo() always returns a leading slash 30 | array_shift($params); 31 | 32 | if (count($params) > 0) { 33 | $locale = $params[0]; 34 | 35 | if ($this->checkLocaleInSupportedLocales($locale)) { 36 | return $locale; 37 | } 38 | } 39 | 40 | return app()->getLocale() ?? config('translatable.fallback_locale'); 41 | } 42 | 43 | /** 44 | * Check if the provided locale is the fallback locale. 45 | * 46 | * @param string $locale The locale to check. 47 | */ 48 | public function isFallbackLocale(string $locale): bool { 49 | return config('translatable.fallback_locale') === $locale; 50 | } 51 | 52 | /** 53 | * Get if the fallback locale should be hidden from the URL. 54 | */ 55 | public function isFallbackLocaleHidden(): bool { 56 | return config('translatable.hide_fallback_locale'); 57 | } 58 | 59 | /** 60 | * Given a route name or path, return it localized if possible. 61 | * 62 | * @param string $name The route name or path to localize. 63 | * @param mixed $parameters The route parameters. 64 | * @param bool $absolute Whether to return an absolute URL. 65 | * @param string|null $locale The locale to use. 66 | * @return string The localized route. 67 | */ 68 | public function route(string $name, $parameters = [], bool $absolute = false, ?string $locale = null): string { 69 | $name = $this->stripLocaleFromRouteName($name); 70 | 71 | $currentLocale = $this->getLocaleFromRequest(); 72 | $fallbackLocale = config('translatable.fallback_locale'); 73 | $locale = $locale ?: $currentLocale; 74 | 75 | /** The provided $name is a route name */ 76 | if (Route::has($name)) { 77 | if (!$this->isFallbackLocaleHidden() || $locale !== $fallbackLocale) { 78 | $name = "$locale.$name"; 79 | } 80 | 81 | return URL::route($name, $parameters, $absolute); 82 | } 83 | 84 | /** The provided $name is a route path */ 85 | $params = explode('/', $name); 86 | if ($params[0] == null) { 87 | array_shift($params); 88 | } 89 | 90 | if (count($params) > 0) { 91 | if ($this->checkLocaleInSupportedLocales($params[0])) { 92 | array_shift($params); 93 | } 94 | 95 | $params = implode('/', $params); 96 | 97 | if (!$this->isFallbackLocaleHidden() || !$this->isFallbackLocale($locale)) { 98 | $params = "$locale/$params"; 99 | } 100 | 101 | return URL::to($params); 102 | } 103 | 104 | /** Fallback */ 105 | return URL::route($name, $parameters, $absolute); 106 | } 107 | 108 | /** 109 | * Get the current path localized with the provided locale. 110 | * 111 | * @param string $locale The locale to use. 112 | * @return string The localized path. 113 | */ 114 | public function switchToLocale(string $locale): string { 115 | $root = request()->root(); 116 | $params = explode('/', request()->getPathInfo()); 117 | 118 | // Dump the first element (empty string) as getPathInfo() always returns a leading slash 119 | array_shift($params); 120 | 121 | if (count($params) === 0) { 122 | return "$root"; 123 | } 124 | 125 | if (!$this->checkLocaleInSupportedLocales($locale)) { 126 | return "$root"; 127 | } 128 | 129 | if ($this->checkLocaleInSupportedLocales($params[0])) { 130 | array_shift($params); 131 | } 132 | 133 | $params = implode('/', $params); 134 | 135 | $url = ($this->isFallbackLocaleHidden() && $this->isFallbackLocale($locale)) 136 | ? $params : "$locale/$params"; 137 | 138 | return "$root/$url"; 139 | } 140 | 141 | /** 142 | * Strip the locale from the beginning of a route name. 143 | * 144 | * @param string $name The route name to strip. 145 | * @return string The stripped route name. 146 | */ 147 | protected function stripLocaleFromRouteName(string $name): string { 148 | $parts = explode('.', $name); 149 | 150 | /** If there is no dot in the route name, couldn't be a locale in the name. */ 151 | if (count($parts) === 1) { 152 | return $name; 153 | } 154 | 155 | /** Get the locales from the configuration file */ 156 | $locales = config('translatable.locales', []); 157 | 158 | /** If the first part of the route name is a valid locale, then remove it from the array. */ 159 | if (in_array($parts[0], $locales)) { 160 | array_shift($parts); 161 | } 162 | 163 | /** Rebuild the normalized route name. */ 164 | return implode('.', $parts); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/TranslatableServiceProvider.php: -------------------------------------------------------------------------------- 1 | registerPublishableFiles(); 19 | $this->registerMacros(); 20 | $this->mergeConfig(); 21 | } 22 | 23 | /** 24 | * Register any application services. 25 | * 26 | * @return void 27 | */ 28 | public function register() { 29 | $loader = AliasLoader::getInstance(); 30 | $loader->alias('Translatable', TranslatableFacade::class); 31 | 32 | $this->app->singleton('translatable', function () { 33 | return new Translatable; 34 | }); 35 | 36 | $this->loadHelpers(); 37 | } 38 | 39 | /** 40 | * Register macros. 41 | * 42 | * @return void 43 | */ 44 | protected function registerMacros() { 45 | TranslatableRoutesMacro::register(); 46 | } 47 | 48 | /** 49 | * Register the publishable files. 50 | * 51 | * @return void 52 | */ 53 | protected function registerPublishableFiles() { 54 | $packagePath = dirname(__DIR__); 55 | 56 | $publishable = [ 57 | 'trans-config' => [ 58 | "$packagePath/publishable/config/translatable.php" => config_path('translatable.php'), 59 | ], 60 | ]; 61 | 62 | foreach ($publishable as $group => $paths) { 63 | $this->publishes($paths, $group); 64 | } 65 | } 66 | 67 | /** 68 | * Merge published configuration file with 69 | * the original package configuration file. 70 | */ 71 | protected function mergeConfig() { 72 | $this->mergeConfigFrom(dirname(__DIR__).'/publishable/config/translatable.php', 'translatable'); 73 | } 74 | 75 | /** 76 | * Get dynamically the Helpers from the /src/Helpers directory and require_once each file. 77 | */ 78 | protected function loadHelpers() { 79 | foreach (glob(__DIR__.'/Helpers/*.php') as $filename) { 80 | require_once $filename; 81 | } 82 | } 83 | } 84 | --------------------------------------------------------------------------------