├── src ├── Routing │ ├── Exception │ │ ├── ExceptionInterface.php │ │ ├── MissingRouteLocaleException.php │ │ ├── MissingLocaleException.php │ │ └── UnknownLocaleException.php │ ├── RouteGenerator │ │ ├── NameInflector │ │ │ ├── PostfixInflector.php │ │ │ ├── RouteNameInflectorInterface.php │ │ │ └── DefaultPostfixInflector.php │ │ ├── RouteGeneratorInterface.php │ │ ├── FilteredLocaleGenerator.php │ │ ├── StrictLocaleRouteGenerator.php │ │ └── I18nRouteGenerator.php │ ├── Translator │ │ ├── TranslationTranslator.php │ │ ├── DoctrineDBAL │ │ │ └── SchemaListener.php │ │ ├── AttributeTranslatorInterface.php │ │ └── DoctrineDBALTranslator.php │ ├── Loader │ │ ├── schema │ │ │ └── routing │ │ │ │ └── routing-1.0.xsd │ │ ├── AnnotatedRouteControllerLoader.php │ │ ├── YamlFileLoader.php │ │ └── XmlFileLoader.php │ ├── Annotation │ │ └── I18nRoute.php │ └── Router.php ├── BeSimpleI18nRoutingBundle.php ├── Resources │ ├── meta │ │ └── LICENSE │ └── config │ │ ├── annotation.xml │ │ ├── dbal.xml │ │ └── routing.xml └── DependencyInjection │ ├── Compiler │ └── OverrideRoutingCompilerPass.php │ ├── Configuration.php │ └── BeSimpleI18nRoutingExtension.php ├── .php_cs ├── .editorconfig ├── CONTRIBUTORS.md ├── CHANGELOG.md ├── composer.json ├── UPGRADE-2.4.md └── README.md /src/Routing/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @api 10 | */ 11 | interface ExceptionInterface 12 | { 13 | } 14 | -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | in(__DIR__ . '/src') 7 | ; 8 | 9 | return Symfony\CS\Config\Config::create() 10 | ->level(Symfony\CS\FixerInterface::PSR2_LEVEL) 11 | ->finder($finder) 12 | ; 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ; This file is for unifying the coding style for different editors and IDEs. 2 | ; More information at http://editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | indent_size = 4 9 | indent_style = space 10 | end_of_line = lf 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /src/Routing/Exception/MissingRouteLocaleException.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @api 10 | */ 11 | class MissingRouteLocaleException extends \InvalidArgumentException implements ExceptionInterface 12 | { 13 | } 14 | -------------------------------------------------------------------------------- /src/Routing/Exception/MissingLocaleException.php: -------------------------------------------------------------------------------- 1 | addCompilerPass(new OverrideRoutingCompilerPass()); 16 | 17 | parent::build($container); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Routing/RouteGenerator/NameInflector/DefaultPostfixInflector.php: -------------------------------------------------------------------------------- 1 | defaultLocale = $defaultLocale; 17 | } 18 | 19 | /** 20 | * @inheritdoc 21 | */ 22 | public function inflect($name, $locale) 23 | { 24 | if ($this->defaultLocale === $locale) { 25 | return $name; 26 | } 27 | 28 | return $name.'.'.$locale; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Routing/RouteGenerator/RouteGeneratorInterface.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class TranslationTranslator implements AttributeTranslatorInterface 15 | { 16 | /** 17 | * @var TranslatorInterface 18 | */ 19 | private $translator; 20 | 21 | public function __construct(TranslatorInterface $translator) 22 | { 23 | $this->translator = $translator; 24 | } 25 | 26 | public function reverseTranslate($route, $locale, $attribute, $originalValue) 27 | { 28 | $domain = $route . "_" . $attribute; 29 | return $this->translator->trans($originalValue, array(), $domain, $locale); 30 | } 31 | 32 | public function translate($route, $locale, $attribute, $localizedValue) 33 | { 34 | $domain = $route . "_" . $attribute; 35 | return $this->translator->trans($localizedValue, array(), $domain, $locale); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Routing/Translator/DoctrineDBAL/SchemaListener.php: -------------------------------------------------------------------------------- 1 | getSchema(); 13 | $this->addRoutingTranslationsTable($schema); 14 | } 15 | 16 | public function addRoutingTranslationsTable(Schema $schema) 17 | { 18 | $table = $schema->createTable('routing_translations'); 19 | $table->addColumn('id', 'integer', array('autoincrement' => true)); 20 | $table->addColumn('route', 'string'); 21 | $table->addColumn('locale', 'string'); 22 | $table->addColumn('attribute', 'string'); 23 | $table->addColumn('localized_value', 'string'); 24 | $table->addColumn('original_value', 'string'); 25 | $table->setPrimaryKey(array('id')); 26 | $table->addUniqueIndex(array('route', 'locale', 'attribute')); 27 | $table->addIndex(array('localized_value')); // this is much more selective than the unique index 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Resources/config/annotation.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/DependencyInjection/Compiler/OverrideRoutingCompilerPass.php: -------------------------------------------------------------------------------- 1 | hasDefinition('be_simple_i18n_routing.router')) { 15 | return; 16 | } 17 | 18 | if ($container->hasAlias('router')) { 19 | // router is an alias. 20 | // Register a private alias for this service to inject it as the parent 21 | $container->setAlias('be_simple_i18n_routing.router.parent', new Alias((string) $container->getAlias('router'), false)); 22 | } elseif ($container->hasDefinition('router')) { 23 | // router is a definition. 24 | // Register it again as a private service to inject it as the parent 25 | $definition = $container->getDefinition('router'); 26 | $definition->setPublic(false); 27 | $container->setDefinition('be_simple_i18n_routing.router.parent', $definition); 28 | } else { 29 | throw new ServiceNotFoundException('router', 'be_simple_i18n_routing.router'); 30 | } 31 | 32 | $container->setAlias('router', 'be_simple_i18n_routing.router'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Routing/RouteGenerator/FilteredLocaleGenerator.php: -------------------------------------------------------------------------------- 1 | routeGenerator = $internalRouteGenerator; 23 | $this->locales = array_flip(array_values($allowedLocales)); 24 | } 25 | 26 | /** 27 | * @inheritdoc 28 | */ 29 | public function generateRoutes($name, array $localesWithPaths, Route $baseRoute) 30 | { 31 | return $this->routeGenerator->generateRoutes( 32 | $name, 33 | array_intersect_key($localesWithPaths, $this->locales), 34 | $baseRoute 35 | ); 36 | } 37 | 38 | /** 39 | * @inheritdoc 40 | */ 41 | public function generateCollection($localesWithPrefix, RouteCollection $baseCollection) 42 | { 43 | if (is_array($localesWithPrefix)) { 44 | $localesWithPrefix = array_intersect_key($localesWithPrefix, $this->locales); 45 | } 46 | 47 | return $this->routeGenerator->generateCollection($localesWithPrefix, $baseCollection); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Routing/Translator/AttributeTranslatorInterface.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | interface AttributeTranslatorInterface 26 | { 27 | /** 28 | * Translate a route attribute. 29 | * 30 | * Always returns a value, if no translation is found the original value. 31 | * 32 | * @param string $route 33 | * @param string $locale 34 | * @param string $attribute 35 | * @param string $localizedValue 36 | * @return string 37 | */ 38 | public function translate($route, $locale, $attribute, $localizedValue); 39 | 40 | /** 41 | * Reverse Translate a value into its current locale. 42 | * 43 | * This feature can optionally be used when generating route urls by passing 44 | * the "translate" parameter to RouterInterface::generate() 45 | * specifying which attributes should be translated. 46 | * 47 | * @param string $route 48 | * @param string $locale 49 | * @param string $attribute 50 | * @param string $originalValue 51 | * @return string 52 | */ 53 | public function reverseTranslate($route, $locale, $attribute, $originalValue); 54 | } 55 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "besimple/i18n-routing-bundle", 3 | "type": "symfony-bundle", 4 | "description": "Full routing internationalized on your Symfony2 project ", 5 | "keywords": ["routing", "internationalisation", "Symfony2", "BeSimpleI18nRoutingBundle"], 6 | "homepage": "https://github.com/BeSimple/BeSimpleI18nRoutingBundle", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Francis Besset", 11 | "email": "francis.besset@gmail.com" 12 | }, 13 | { 14 | "name": "Christophe Coevoet", 15 | "email": "stof@notk.org" 16 | }, 17 | { 18 | "name": "Benjamin Eberlei", 19 | "email": "kontakt@beberlei.de" 20 | }, 21 | { 22 | "name": "BeSimple Community", 23 | "homepage": "https://github.com/BeSimple/BeSimpleI18nRoutingBundle/contributors" 24 | } 25 | ], 26 | "require": { 27 | "php": ">=5.3.2", 28 | 29 | "symfony/routing": "~2.3 || ~3.0", 30 | "symfony/config": "~2.3 || ~3.0" 31 | }, 32 | "require-dev": { 33 | "doctrine/dbal": "~2.2", 34 | 35 | "symfony/dependency-injection": "~2.3 || ~3.0", 36 | "symfony/http-kernel": "~2.3 || ~3.0", 37 | "symfony/translation": "~2.3 || ~3.0", 38 | 39 | "phpunit/phpunit": "~4.7@stable", 40 | 41 | "matthiasnoback/symfony-dependency-injection-test": "^0.7", 42 | "matthiasnoback/symfony-config-test": "^1.4" 43 | }, 44 | "suggest": { 45 | "doctrine/dbal": "~2.2" 46 | }, 47 | "autoload": { 48 | "psr-4": { "BeSimple\\I18nRoutingBundle\\": "src/" } 49 | }, 50 | "autoload-dev": { 51 | "psr-4": { 52 | "BeSimple\\I18nRoutingBundle\\Tests\\": "tests/" 53 | } 54 | }, 55 | "extra": { 56 | "branch-alias": { 57 | "dev-master": "2.4-dev" 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Routing/Loader/schema/routing/routing-1.0.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /UPGRADE-2.4.md: -------------------------------------------------------------------------------- 1 | # UPGRADE FROM 2.3 to 2.4 2 | 3 | ## Configuration 4 | 5 | * The `locale` container parameter was used for the default locale this need to be specified from now on. 6 | 7 | After: 8 | ```YAML 9 | # app/config/config.yml 10 | be_simple_i18n_routing: 11 | locales: 12 | default_locale: "%locale%" 13 | ``` 14 | 15 | ## Routing 16 | 17 | * The `I18nRouteCollectionBuilder` class has been removed in favor of `I18nRouteGenerator`. 18 | 19 | Before: 20 | ```PHP 21 | use BeSimple\I18nRoutingBundle\Routing\I18nRouteCollectionBuilder; 22 | 23 | $builder = new I18nRouteCollectionBuilder(); 24 | $builder->buildCollection( 25 | 'homepage', 26 | array('en' => '/welcome', 'fr' => '/bienvenue', 'de' => '/willkommen'), 27 | array('_controller' => 'MyWebsiteBundle:Frontend:index') 28 | ); 29 | ``` 30 | 31 | After: 32 | ```PHP 33 | use BeSimple\I18nRoutingBundle\Routing\RouteGenerator\I18nRouteGenerator; 34 | 35 | $generator = new I18nRouteGenerator(); 36 | $generator->generateRoutes( 37 | 'homepage', 38 | array('en' => '/welcome', 'fr' => '/bienvenue', 'de' => '/willkommen'), 39 | new Route('', array( 40 | '_controller' => 'MyWebsiteBundle:Frontend:index' 41 | )) 42 | ); 43 | ``` 44 | 45 | * The `I18nRouteCollection` class has been removed in favor of using the Symfony `RouteCollection` with the `I18nRouteGenerator`. 46 | 47 | Before: 48 | ```PHP 49 | use BeSimple\I18nRoutingBundle\Routing\I18nRouteCollection; 50 | 51 | $collection = new I18nRouteCollection(); 52 | $collection->addResource(new FileResource($path)); 53 | $collection->addPrefix($prefix); 54 | ``` 55 | 56 | After: 57 | ```PHP 58 | use BeSimple\I18nRoutingBundle\Routing\RouteGenerator\I18nRouteGenerator; 59 | 60 | $collection = new \Symfony\Component\Routing\RouteCollection(); 61 | $collection->addResource(new FileResource($path)); 62 | 63 | $generator = new I18nRouteGenerator(); 64 | $collection = $generator->generateCollection($prefix, $collection); 65 | ``` 66 | 67 | ## Loaders 68 | 69 | * The `XmlFileLoader` and `YamlFileLoader` now required a `RouteGeneratorInterface` instance or null as a second constructor parameter instead of the now removed `BeSimple\I18nRoutingBundle\Routing\I18nRouteCollectionBuilder`. 70 | 71 | * The `XmlFileLoader` class no longer inherits from `Symfony\Component\Routing\Loader\XmlFileLoader`. You may need to change your code hints from `Symfony\Component\Routing\Loader\XmlFileLoader` to `Symfony\Component\Config\Loader\FileLoader`. 72 | 73 | * The `YamlFileLoader` class no longer inherits from `Symfony\Component\Routing\Loader\YamlFileLoader`. You may need to change your code hints from `Symfony\Component\Routing\Loader\YamlFileLoader` to `Symfony\Component\Config\Loader\FileLoader`. 74 | -------------------------------------------------------------------------------- /src/Routing/RouteGenerator/StrictLocaleRouteGenerator.php: -------------------------------------------------------------------------------- 1 | routeGenerator = $internalRouteGenerator; 26 | $this->locales = $supportedLocales; 27 | } 28 | 29 | public function allowFallback($enabled = true) 30 | { 31 | $this->allowFallback = $enabled; 32 | } 33 | 34 | /** 35 | * Generate localized versions of the given route. 36 | * 37 | * @param $name 38 | * @param array $localesWithPaths 39 | * @param Route $baseRoute 40 | * @return RouteCollection 41 | */ 42 | public function generateRoutes($name, array $localesWithPaths, Route $baseRoute) 43 | { 44 | $this->assertLocalesAreSupported(array_keys($localesWithPaths)); 45 | 46 | return $this->routeGenerator->generateRoutes($name, $localesWithPaths, $baseRoute); 47 | } 48 | 49 | /** 50 | * Generate a localized version of the given route collection. 51 | * 52 | * @param array|string $localesWithPrefix 53 | * @param RouteCollection $baseCollection 54 | * @return RouteCollection 55 | */ 56 | public function generateCollection($localesWithPrefix, RouteCollection $baseCollection) 57 | { 58 | if (is_array($localesWithPrefix)) { 59 | $this->assertLocalesAreSupported(array_keys($localesWithPrefix)); 60 | } 61 | 62 | return $this->routeGenerator->generateCollection($localesWithPrefix, $baseCollection); 63 | } 64 | 65 | private function assertLocalesAreSupported(array $locales) 66 | { 67 | if (!$this->allowFallback) { 68 | $missingLocales = array_diff($this->locales, $locales); 69 | if (!empty($missingLocales)) { 70 | throw MissingLocaleException::shouldSupportLocale($missingLocales); 71 | } 72 | } 73 | 74 | $unknownLocales = array_diff($locales, $this->locales); 75 | if (!empty($unknownLocales)) { 76 | throw UnknownLocaleException::unexpectedLocale($unknownLocales, $this->locales); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Routing/Annotation/I18nRoute.php: -------------------------------------------------------------------------------- 1 | $value) { 38 | $method = 'set'.str_replace('_', '', $key); 39 | if (!method_exists($this, $method)) { 40 | throw new \BadMethodCallException(sprintf('Unknown property "%s" on annotation "%s".', $key, get_class($this))); 41 | } 42 | $this->$method($value); 43 | } 44 | } 45 | 46 | public function setLocales($locales) 47 | { 48 | $this->locales = $locales; 49 | } 50 | 51 | public function getLocales() 52 | { 53 | return $this->locales; 54 | } 55 | 56 | public function setHost($pattern) 57 | { 58 | $this->host = $pattern; 59 | } 60 | 61 | public function getHost() 62 | { 63 | return $this->host; 64 | } 65 | 66 | public function setName($name) 67 | { 68 | $this->name = $name; 69 | } 70 | 71 | public function getName() 72 | { 73 | return $this->name; 74 | } 75 | 76 | public function setRequirements($requirements) 77 | { 78 | if (isset($requirements['_method'])) { 79 | if (0 === count($this->methods)) { 80 | $this->methods = explode('|', $requirements['_method']); 81 | } 82 | } 83 | 84 | if (isset($requirements['_scheme'])) { 85 | if (0 === count($this->schemes)) { 86 | $this->schemes = explode('|', $requirements['_scheme']); 87 | } 88 | } 89 | 90 | $this->requirements = $requirements; 91 | } 92 | 93 | public function getRequirements() 94 | { 95 | return $this->requirements; 96 | } 97 | 98 | public function setOptions($options) 99 | { 100 | $this->options = $options; 101 | } 102 | 103 | public function getOptions() 104 | { 105 | return $this->options; 106 | } 107 | 108 | public function setDefaults($defaults) 109 | { 110 | $this->defaults = $defaults; 111 | } 112 | 113 | public function getDefaults() 114 | { 115 | return $this->defaults; 116 | } 117 | 118 | public function setSchemes($schemes) 119 | { 120 | $this->schemes = is_array($schemes) ? $schemes : array($schemes); 121 | } 122 | 123 | public function getSchemes() 124 | { 125 | return $this->schemes; 126 | } 127 | 128 | public function setMethods($methods) 129 | { 130 | $this->methods = is_array($methods) ? $methods : array($methods); 131 | } 132 | 133 | public function getMethods() 134 | { 135 | return $this->methods; 136 | } 137 | 138 | public function setCondition($condition) 139 | { 140 | $this->condition = $condition; 141 | } 142 | 143 | public function getCondition() 144 | { 145 | return $this->condition; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/Resources/config/dbal.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | BeSimple\I18nRoutingBundle\Routing\Translator\DoctrineDBALTranslator 9 | BeSimple\I18nRoutingBundle\Routing\Translator\DoctrineDBAL\SchemaListener 10 | Doctrine\Common\Cache\ArrayCache 11 | Doctrine\Common\Cache\XcacheCache 12 | Doctrine\Common\Cache\ApcCache 13 | Doctrine\Common\Cache\MemcacheCache 14 | Memcache 15 | localhost 16 | 11211 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | %be_simple_i18n_routing.doctrine_dbal.connection_name% 27 | 28 | 29 | 30 | 31 | %be_simple_i18n_routing.doctrine_dbal.cache.namespace% 32 | 33 | 34 | 35 | 36 | 37 | %be_simple_i18n_routing.doctrine_dbal.cache.namespace% 38 | 39 | 40 | 41 | 42 | 43 | %be_simple_i18n_routing.doctrine_dbal.cache.namespace% 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | %be_simple_i18n_routing.doctrine_dbal.cache.namespace% 53 | 54 | 55 | 56 | 57 | 58 | %be_simple_i18n_routing.doctrine_dbal.cache.memcache_host% 59 | %be_simple_i18n_routing.doctrine_dbal.cache.memcache_port% 60 | 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /src/Resources/config/routing.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | BeSimple\I18nRoutingBundle\Routing\Router 11 | BeSimple\I18nRoutingBundle\Routing\Loader\XmlFileLoader 12 | BeSimple\I18nRoutingBundle\Routing\Loader\YamlFileLoader 13 | BeSimple\I18nRoutingBundle\Routing\Translator\TranslationTranslator 14 | 15 | 16 | 17 | 18 | 19 | 20 | %be_simple_i18n_routing.default_locale% 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 48 | 49 | 50 | 51 | 54 | 55 | %be_simple_i18n_routing.locales% 56 | 57 | 58 | 61 | 62 | %be_simple_i18n_routing.locales% 63 | 64 | 65 | 66 | 67 | 68 | 71 | 72 | 73 | 76 | %be_simple_i18n_routing.default_locale% 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /src/Routing/Translator/DoctrineDBALTranslator.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class DoctrineDBALTranslator implements AttributeTranslatorInterface 17 | { 18 | /** 19 | * @var Doctrine\DBAL\Connection 20 | */ 21 | private $connection; 22 | 23 | /** 24 | * 25 | * @var Doctrine\Common\Cache\Cache 26 | */ 27 | private $cache; 28 | 29 | /** 30 | * Prime the cache when using {@see addTranslation()} yes or no. 31 | * 32 | * @var bool 33 | */ 34 | private $primeCache; 35 | 36 | public function __construct(Connection $connection, Cache $cache, $primeCache = true) 37 | { 38 | $this->connection = $connection; 39 | $this->cache = $cache; 40 | $this->primeCache = $primeCache; 41 | } 42 | 43 | /** 44 | * Translate using Doctrine DBAL and a cache layer around it. 45 | * 46 | * @param string $route 47 | * @param string $locale 48 | * @param string $attribute 49 | * @param string $value 50 | * @return string 51 | */ 52 | public function translate($route, $locale, $attribute, $value) 53 | { 54 | // values can potentially be large, so we hash them and prevent collisions 55 | $hashKey = $route . "__" . $locale . "__" . $attribute . "__" . $value; 56 | $cacheKey = "besimplei18nroute__" . sha1($hashKey); 57 | $translatedValues = $this->cache->fetch($cacheKey); 58 | if ($translatedValues && isset($translatedValues[$hashKey])) { 59 | return $translatedValues[$hashKey]; 60 | } 61 | 62 | $query = "SELECT original_value FROM routing_translations ". 63 | "WHERE route = ? AND locale = ? AND attribute = ? AND localized_value = ?"; 64 | if ($translatedValue = $this->connection->fetchColumn($query, array($route, $locale, $attribute, $value))) { 65 | $value = $translatedValue; 66 | } 67 | 68 | $translatedValues[$hashKey] = $value; 69 | $this->cache->save($cacheKey, $translatedValues); 70 | 71 | return $value; 72 | } 73 | 74 | /** 75 | * Reverse Translate a value into its current locale. 76 | * 77 | * This feature can optionally be used when generating route urls by passing 78 | * the "translate" parameter to RouterInterface::generate() 79 | * specifying which attributes should be translated. 80 | * 81 | * @param string $route 82 | * @param string $locale 83 | * @param string $attribute 84 | * @param string $originalValue 85 | * @return string 86 | */ 87 | public function reverseTranslate($route, $locale, $attribute, $value) 88 | { 89 | // values can potentially be large, so we hash them and prevent collisions 90 | $hashKey = $route . "__" . $locale . "__" . $attribute . "__" . $value; 91 | $cacheKey = "besimplei18nroute__reverse__" . sha1($hashKey); 92 | $reverseTranslatedValues = $this->cache->fetch($cacheKey); 93 | if ($reverseTranslatedValues && isset($reverseTranslatedValues[$hashKey])) { 94 | return $reverseTranslatedValues[$hashKey]; 95 | } 96 | 97 | $query = "SELECT localized_value FROM routing_translations ". 98 | "WHERE route = ? AND locale = ? AND attribute = ? AND original_value = ?"; 99 | if ($lovalizedValue = $this->connection->fetchColumn($query, array($route, $locale, $attribute, $value))) { 100 | $value = $lovalizedValue; 101 | } 102 | 103 | $reverseTranslatedValues[$hashKey] = $value; 104 | $this->cache->save($cacheKey, $reverseTranslatedValues); 105 | 106 | return $value; 107 | } 108 | 109 | public function addTranslation($route, $locale, $attribute, $localizedValue, $originalValue) 110 | { 111 | $query = "SELECT id FROM routing_translations WHERE route = ? AND locale = ? AND attribute = ?"; 112 | $id = $this->connection->fetchColumn($query, array($route, $locale, $attribute)); 113 | 114 | if ($id) { 115 | $this->connection->update('routing_translations', array( 116 | 'localized_value' => $localizedValue, 117 | 'original_value' => $originalValue, 118 | ), array('id' => $id)); 119 | } else { 120 | $this->connection->insert('routing_translations', array( 121 | 'route' => $route, 122 | 'locale' => $locale, 123 | 'attribute' => $attribute, 124 | 'localized_value' => $localizedValue, 125 | 'original_value' => $originalValue, 126 | )); 127 | } 128 | 129 | // prime the cache! 130 | if ($this->primeCache) { 131 | $hashKey = $route . "__" . $locale . "__" . $attribute . "__" . $localizedValue; 132 | $cacheKey = "besimplei18nroute__" . sha1($hashKey); 133 | $translatedValues = $this->cache->fetch($cacheKey); 134 | if (!$translatedValues) { 135 | $translatedValues = array(); 136 | } 137 | $translatedValues[$hashKey][$originalValue]; 138 | $this->cache->save($cacheKey, $translatedValues); 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/Routing/RouteGenerator/I18nRouteGenerator.php: -------------------------------------------------------------------------------- 1 | routeNameInflector = $routeNameInflector ?: new PostfixInflector(); 22 | } 23 | 24 | /** 25 | * @inheritdoc 26 | */ 27 | public function generateRoutes($name, array $localesWithPaths, Route $baseRoute) 28 | { 29 | $collection = new RouteCollection(); 30 | 31 | foreach ($localesWithPaths as $locale => $path) { 32 | /** @var \Symfony\Component\Routing\Route $localeRoute */ 33 | $localeRoute = clone $baseRoute; 34 | $localeRoute->setDefault(self::LOCALE_PARAM, $locale); 35 | $localeRoute->setPath($path); 36 | 37 | $collection->add( 38 | $this->routeNameInflector->inflect($name, $locale), 39 | $localeRoute 40 | ); 41 | } 42 | 43 | return $collection; 44 | } 45 | 46 | /** 47 | * Generate a localized version of the given route collection. 48 | * 49 | * @param array|string $prefix 50 | * @param RouteCollection $baseCollection 51 | * @return RouteCollection 52 | */ 53 | public function generateCollection($prefix, RouteCollection $baseCollection) 54 | { 55 | $collection = clone $baseCollection; 56 | 57 | if (is_array($prefix)) { 58 | $prefixes = array(); 59 | foreach ($prefix as $locale => $localePrefix) { 60 | $prefixes[$locale] = trim(trim($localePrefix), '/'); 61 | } 62 | 63 | $this->localizeCollection($prefixes, $collection); 64 | } elseif (is_string($prefix) && preg_match(self::LOCALE_REGEX, $prefix)) { 65 | $originalPrefix = trim(trim($prefix), '/'); 66 | $this->localizeCollectionLocaleParameter($originalPrefix, $collection); 67 | } else { 68 | // A normal prefix so just add it and return the original collection 69 | $collection->addPrefix($prefix); 70 | } 71 | 72 | return $collection; 73 | } 74 | 75 | /** 76 | * Localize a route collection. 77 | * 78 | * @param array $prefixes 79 | * @param RouteCollection $collection 80 | */ 81 | protected function localizeCollection(array $prefixes, RouteCollection $collection) 82 | { 83 | $removeRoutes = array(); 84 | $newRoutes = new RouteCollection(); 85 | foreach ($collection->all() as $name => $route) { 86 | $routeLocale = $route->getDefault(self::LOCALE_PARAM); 87 | if ($routeLocale !== null) { 88 | if (!isset($prefixes[$routeLocale])) { 89 | throw new MissingRouteLocaleException(sprintf('Route `%s`: No prefix found for locale "%s".', $name, $routeLocale)); 90 | } 91 | 92 | $route->setPath('/' . $prefixes[$routeLocale] . $route->getPath()); 93 | 94 | continue; 95 | } 96 | 97 | // No locale found for the route so localize the route 98 | $removeRoutes[] = $name; 99 | 100 | foreach ($prefixes as $locale => $prefix) { 101 | /** @var \Symfony\Component\Routing\Route $localeRoute */ 102 | $localeRoute = clone $route; 103 | $localeRoute->setPath('/' . $prefix . $route->getPath()); 104 | $localeRoute->setDefault(self::LOCALE_PARAM, $locale); 105 | 106 | $newRoutes->add( 107 | $this->routeNameInflector->inflect($name, $locale), 108 | $localeRoute 109 | ); 110 | } 111 | } 112 | 113 | $collection->remove($removeRoutes); 114 | $collection->addCollection($newRoutes); 115 | } 116 | 117 | /** 118 | * Localize the prefix `_locale` of all routes. 119 | * 120 | * @param string $prefix A prefix containing _locale 121 | * @param RouteCollection $collection A RouteCollection instance 122 | */ 123 | protected function localizeCollectionLocaleParameter($prefix, RouteCollection $collection) 124 | { 125 | $localizedPrefixes = array(); 126 | foreach ($collection->all() as $name => $route) { 127 | $locale = $route->getDefault(self::LOCALE_PARAM); 128 | if ($locale === null) { 129 | // No locale so nothing to do 130 | $routePrefix = $prefix; 131 | } else { 132 | // A locale was found so localize the prefix 133 | if (!isset($localizedPrefixes[$locale])) { 134 | $localizedPrefixes[$locale] = preg_replace(static::LOCALE_REGEX, $locale, $prefix); 135 | } 136 | 137 | $routePrefix = $localizedPrefixes[$locale]; 138 | } 139 | 140 | $route->setPath('/' . $routePrefix . $route->getPath()); 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class Configuration implements ConfigurationInterface 18 | { 19 | /** 20 | * Generates the configuration tree. 21 | * 22 | * @return TreeBuilder 23 | */ 24 | public function getConfigTreeBuilder() 25 | { 26 | $treeBuilder = new TreeBuilder(); 27 | $rootNode = $treeBuilder->root('be_simple_i18n_routing'); 28 | 29 | $rootNode 30 | ->children() 31 | ->booleanNode('annotations')->defaultFalse()->end() 32 | ->scalarNode('route_name_inflector')->defaultValue('be_simple_i18n_routing.route_name_inflector.postfix')->end() 33 | ->end() 34 | ; 35 | 36 | $this->addLocalesSection($rootNode); 37 | $this->addAttributeTranslatorSection($rootNode); 38 | 39 | return $treeBuilder; 40 | } 41 | 42 | private function addLocalesSection(ArrayNodeDefinition $rootNode) 43 | { 44 | $rootNode 45 | ->children() 46 | ->arrayNode('locales') 47 | ->addDefaultsIfNotSet() 48 | ->children() 49 | ->scalarNode('default_locale')->defaultNull()->end() 50 | ->arrayNode('supported') 51 | ->treatNullLike(array()) 52 | ->beforeNormalization() 53 | ->ifTrue(function ($v) { return !is_array($v); }) 54 | ->then(function ($v) { return array($v); }) 55 | ->end() 56 | ->prototype('scalar')->end() 57 | ->end() 58 | 59 | ->booleanNode('filter') 60 | ->defaultFalse() 61 | ->info( 62 | "set to true to filter out any unknown locales\n". 63 | "set to false to disable filtering locales" 64 | ) 65 | ->end() 66 | 67 | ->scalarNode('strict') 68 | ->defaultFalse() 69 | ->validate() 70 | ->ifTrue(function ($v) { return $v !== null && !is_bool($v); }) 71 | ->thenInvalid('Invalid type for path "strict". Expected boolean or null, but got %s.') 72 | ->end() 73 | ->info( 74 | "set to true to throw a exception when a i18n route is found where the locale is unknown or where a locale is missing\n". 75 | "set to false to disable exceptions so no locale missing or unknown exception are thrown\n". 76 | "set to null to disable locale is missing for a route exception\n". 77 | "'true' is the preferred configuration in development mode, while 'false' or 'null' might be preferred in production" 78 | ) 79 | ->end() 80 | ->end() 81 | ->end() 82 | ->end(); 83 | } 84 | 85 | private function addAttributeTranslatorSection(ArrayNodeDefinition $rootNode) 86 | { 87 | $rootNode 88 | ->children() 89 | ->arrayNode('attribute_translator') 90 | ->canBeEnabled() 91 | ->children() 92 | ->enumNode('type') 93 | ->defaultValue('translator') 94 | ->values(array('service', 'doctrine_dbal', 'translator')) 95 | ->end() 96 | ->scalarNode('id')->end() 97 | ->scalarNode('connection')->defaultNull()->end() 98 | ->arrayNode('cache') 99 | ->addDefaultsIfNotSet() 100 | ->beforeNormalization() 101 | ->ifString() 102 | ->then(function ($value) { 103 | return array('type' => $value); 104 | }) 105 | ->end() 106 | ->children() 107 | ->enumNode('type') 108 | ->defaultValue('array') 109 | ->values(array('memcache', 'apc', 'array', 'xcache')) 110 | ->end() 111 | ->scalarNode('host')->end() 112 | ->scalarNode('port')->end() 113 | ->scalarNode('instance_class')->end() 114 | ->scalarNode('class')->end() 115 | ->end() 116 | ->end() 117 | ->end() 118 | ->validate() 119 | ->ifTrue(function ($value) { 120 | return 'service' === $value['type'] && !isset($value['id']); 121 | }) 122 | ->thenInvalid('The id has to be specified to use a service as attribute translator') 123 | ->end() 124 | ->end() 125 | ->end(); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/Routing/Router.php: -------------------------------------------------------------------------------- 1 | router = $router; 47 | $this->translator = $translator; 48 | $this->defaultLocale = $defaultLocale; 49 | $this->routeNameInflector = $routeNameInflector ?: new PostfixInflector(); 50 | } 51 | 52 | /** 53 | * Generates a URL from the given parameters. 54 | * 55 | * @param string $name The name of the route 56 | * @param array $parameters An array of parameters 57 | * @param bool|int $referenceType The type of reference to be generated (one of the constants) 58 | * 59 | * @return string The generated URL 60 | * 61 | * @throws \InvalidArgumentException When the route doesn't exists 62 | */ 63 | public function generate($name, $parameters = array(), $referenceType = self::ABSOLUTE_PATH) 64 | { 65 | if (isset($parameters['locale']) || isset($parameters['translate'])) { 66 | $locale = $this->getLocale($parameters); 67 | 68 | if (isset($parameters['locale'])) { 69 | unset($parameters['locale']); 70 | } 71 | 72 | if (null === $locale) { 73 | throw new MissingMandatoryParametersException('The locale must be available when using the "translate" option.'); 74 | } 75 | 76 | if (isset($parameters['translate'])) { 77 | if (null !== $this->translator) { 78 | foreach ((array) $parameters['translate'] as $translateAttribute) { 79 | $parameters[$translateAttribute] = $this->translator->reverseTranslate( 80 | $name, 81 | $locale, 82 | $translateAttribute, 83 | $parameters[$translateAttribute] 84 | ); 85 | } 86 | } 87 | unset($parameters['translate']); 88 | } 89 | 90 | return $this->generateI18n($name, $locale, $parameters, $referenceType); 91 | } 92 | 93 | try { 94 | return $this->router->generate($name, $parameters, $referenceType); 95 | } catch (RouteNotFoundException $e) { 96 | $locale = $this->getLocale($parameters); 97 | if (null !== $locale) { 98 | // at this point here we would never have $parameters['translate'] due to condition before 99 | return $this->generateI18n($name, $locale, $parameters, $referenceType); 100 | } 101 | 102 | throw $e; 103 | } 104 | } 105 | 106 | /** 107 | * {@inheritDoc} 108 | */ 109 | public function match($pathinfo) 110 | { 111 | $match = $this->router->match($pathinfo); 112 | 113 | // if a _locale parameter isset remove the .locale suffix that is appended to each route in I18nRoute 114 | if (!empty($match['_locale']) && preg_match('#^(.+)\.'.preg_quote($match['_locale'], '#').'+$#', $match['_route'], $route)) { 115 | $match['_route'] = $route[1]; 116 | 117 | // now also check if we want to translate parameters: 118 | if (null !== $this->translator && isset($match['_translate'])) { 119 | foreach ((array) $match['_translate'] as $attribute) { 120 | $match[$attribute] = $this->translator->translate( 121 | $match['_route'], 122 | $match['_locale'], 123 | $attribute, 124 | $match[$attribute] 125 | ); 126 | } 127 | } 128 | } 129 | 130 | return $match; 131 | } 132 | 133 | public function getRouteCollection() 134 | { 135 | return $this->router->getRouteCollection(); 136 | } 137 | 138 | public function setContext(RequestContext $context) 139 | { 140 | $this->router->setContext($context); 141 | } 142 | 143 | public function getContext() 144 | { 145 | return $this->router->getContext(); 146 | } 147 | 148 | /** 149 | * Overwrite the locale to be used by default if the current locale could 150 | * not be found when building the route 151 | * 152 | * @param string $locale 153 | */ 154 | public function setDefaultLocale($locale) 155 | { 156 | $this->defaultLocale = $locale; 157 | } 158 | 159 | /** 160 | * Generates a I18N URL from the given parameter 161 | * 162 | * @param string $name The name of the I18N route 163 | * @param string $locale The locale of the I18N route 164 | * @param array $parameters An array of parameters 165 | * @param bool|int $referenceType The type of reference to be generated (one of the constants) 166 | * 167 | * @return string The generated URL 168 | * 169 | * @throws RouteNotFoundException When the route doesn't exists 170 | */ 171 | protected function generateI18n($name, $locale, $parameters, $referenceType = self::ABSOLUTE_PATH) 172 | { 173 | try { 174 | return $this->router->generate( 175 | $this->routeNameInflector->inflect($name, $locale), 176 | $parameters, 177 | $referenceType 178 | ); 179 | } catch (RouteNotFoundException $e) { 180 | throw new RouteNotFoundException(sprintf('I18nRoute "%s" (%s) does not exist.', $name, $locale)); 181 | } 182 | } 183 | 184 | /** 185 | * Determine the locale to be used with this request 186 | * 187 | * @param array $parameters the parameters determined by the route 188 | * 189 | * @return string 190 | */ 191 | protected function getLocale($parameters) 192 | { 193 | if (isset($parameters['locale'])) { 194 | return $parameters['locale']; 195 | } 196 | 197 | if ($this->getContext()->hasParameter('_locale')) { 198 | return $this->getContext()->getParameter('_locale'); 199 | } 200 | 201 | return $this->defaultLocale; 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/Routing/Loader/AnnotatedRouteControllerLoader.php: -------------------------------------------------------------------------------- 1 | routeGenerator = $routeGenerator ?: new I18nRouteGenerator(); 29 | $this->setRouteAnnotationClass('BeSimple\\I18nRoutingBundle\\Routing\\Annotation\\I18nRoute'); 30 | } 31 | 32 | protected function addRoute(RouteCollection $collection, $annot, $globals, \ReflectionClass $class, \ReflectionMethod $method) 33 | { 34 | /** @var \BeSimple\I18nRoutingBundle\Routing\Annotation\I18nRoute $annot */ 35 | $name = $annot->getName(); 36 | if (null === $name) { 37 | $name = $this->getDefaultRouteName($class, $method); 38 | } 39 | 40 | $defaults = array_replace($globals['defaults'], $annot->getDefaults()); 41 | foreach ($method->getParameters() as $param) { 42 | if (!isset($defaults[$param->getName()]) && $param->isDefaultValueAvailable()) { 43 | $defaults[$param->getName()] = $param->getDefaultValue(); 44 | } 45 | } 46 | $requirements = array_replace($globals['requirements'], $annot->getRequirements()); 47 | $options = array_replace($globals['options'], $annot->getOptions()); 48 | $schemes = array_merge($globals['schemes'], $annot->getSchemes()); 49 | $methods = array_merge($globals['methods'], $annot->getMethods()); 50 | 51 | $host = $annot->getHost(); 52 | if (null === $host) { 53 | $host = $globals['host']; 54 | } 55 | 56 | $condition = $annot->getCondition(); 57 | if (null === $condition && isset($globals['condition'])) { 58 | $condition = $globals['condition']; 59 | } 60 | 61 | $path = ''; 62 | $localesWithPaths = $annot->getLocales(); 63 | if (is_scalar($localesWithPaths)) { 64 | $routePath = $localesWithPaths; 65 | 66 | if (!is_array($globals['locales'])) { 67 | // This is a normal route 68 | $path = $globals['locales'].$localesWithPaths; 69 | $localesWithPaths = null; 70 | } else { 71 | // Global contains the locales 72 | $localesWithPaths = array(); 73 | foreach ($globals['locales'] as $locale => $localePath) { 74 | $localesWithPaths[$locale] = $localePath.$routePath; 75 | } 76 | } 77 | } elseif (is_array($localesWithPaths) && !empty($globals['locales'])) { 78 | if (!is_array($globals['locales'])) { 79 | // Global is a normal prefix 80 | foreach ($localesWithPaths as $locale => $localePath) { 81 | $localesWithPaths[$locale] = $globals['locales'].$localePath; 82 | } 83 | } else { 84 | foreach ($localesWithPaths as $locale => $localePath) { 85 | if (!isset($globals['locales'][$locale])) { 86 | throw new MissingLocaleException(sprintf('Locale "%s" for controller %s::%s is expected to be part of the global configuration at class level.', $locale, $class->getName(), $method->getName())); 87 | } 88 | $localesWithPaths[$locale] = $globals['locales'][$locale].$localePath; 89 | } 90 | } 91 | } elseif (!is_array($localesWithPaths)) { 92 | throw new MissingRouteLocaleException(sprintf('Missing locales for controller %s::%s', $class->getName(), $method->getName())); 93 | } 94 | 95 | $route = $this->createRoute($path, $defaults, $requirements, $options, $host, $schemes, $methods, $condition); 96 | 97 | $this->configureRoute($route, $class, $method, $annot); 98 | 99 | if (null === $localesWithPaths) { 100 | // Standard route 101 | $collection->add($name, $route); 102 | 103 | return; 104 | } 105 | 106 | $collection->addCollection( 107 | $this->routeGenerator->generateRoutes($name, $localesWithPaths, $route) 108 | ); 109 | } 110 | 111 | /** 112 | * @inheritdoc 113 | */ 114 | protected function getGlobals(\ReflectionClass $class) 115 | { 116 | $globals = array( 117 | 'locales' => '', 118 | 'requirements' => array(), 119 | 'options' => array(), 120 | 'defaults' => array(), 121 | 'schemes' => array(), 122 | 'methods' => array(), 123 | 'host' => '', 124 | 'condition' => '', 125 | ); 126 | 127 | /** @var \BeSimple\I18nRoutingBundle\Routing\Annotation\I18nRoute $annot */ 128 | if ($annot = $this->reader->getClassAnnotation($class, $this->routeAnnotationClass)) { 129 | if (null !== $annot->getLocales()) { 130 | $globals['locales'] = $annot->getLocales(); 131 | } 132 | 133 | if (null !== $annot->getRequirements()) { 134 | $globals['requirements'] = $annot->getRequirements(); 135 | } 136 | 137 | if (null !== $annot->getOptions()) { 138 | $globals['options'] = $annot->getOptions(); 139 | } 140 | 141 | if (null !== $annot->getDefaults()) { 142 | $globals['defaults'] = $annot->getDefaults(); 143 | } 144 | 145 | if (null !== $annot->getSchemes()) { 146 | $globals['schemes'] = $annot->getSchemes(); 147 | } 148 | 149 | if (null !== $annot->getMethods()) { 150 | $globals['methods'] = $annot->getMethods(); 151 | } 152 | 153 | if (null !== $annot->getHost()) { 154 | $globals['host'] = $annot->getHost(); 155 | } 156 | 157 | if (null !== $annot->getCondition()) { 158 | $globals['condition'] = $annot->getCondition(); 159 | } 160 | } 161 | 162 | return $globals; 163 | } 164 | 165 | /** 166 | * Configures the _controller default parameter of a given Route instance. 167 | * 168 | * @param Route $route A route instance 169 | * @param \ReflectionClass $class A ReflectionClass instance 170 | * @param \ReflectionMethod $method A ReflectionClass method 171 | * @param mixed $annot The annotation class instance 172 | * 173 | * @throws \LogicException When the service option is specified on a method 174 | */ 175 | protected function configureRoute(Route $route, \ReflectionClass $class, \ReflectionMethod $method, $annot) 176 | { 177 | $route->setDefault('_controller', $class->getName().'::'.$method->getName()); 178 | } 179 | 180 | /** 181 | * @inheritdoc 182 | * 183 | * @see \Sensio\Bundle\FrameworkExtraBundle\Routing\AnnotatedRouteControllerLoader::getDefaultRouteName 184 | */ 185 | protected function getDefaultRouteName(\ReflectionClass $class, \ReflectionMethod $method) 186 | { 187 | $routeName = parent::getDefaultRouteName($class, $method); 188 | 189 | return preg_replace(array( 190 | '/(bundle|controller)_/', 191 | '/action(_\d+)?$/', 192 | '/__/', 193 | ), array( 194 | '_', 195 | '\\1', 196 | '_', 197 | ), $routeName); 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/DependencyInjection/BeSimpleI18nRoutingExtension.php: -------------------------------------------------------------------------------- 1 | load('routing.xml'); 26 | 27 | $configuration = new Configuration(); 28 | $config = $this->processConfiguration($configuration, $configs); 29 | 30 | $this->configureLocales($config, $container); 31 | $this->configureAttributeTranslator($config, $container, $loader); 32 | $this->configureRouteNameInflector($config, $container); 33 | $this->configureAnnotations($config, $container, $loader); 34 | 35 | if (PHP_VERSION_ID < 70000) { 36 | $this->addClassesToCompile(array( 37 | 'BeSimple\\I18nRoutingBundle\\Routing\\Router', 38 | 'BeSimple\\I18nRoutingBundle\\Routing\\RouteGenerator\\NameInflector\\RouteNameInflectorInterface' 39 | )); 40 | } 41 | } 42 | 43 | /** 44 | * Configures the attribute translator 45 | * 46 | * @param array $config 47 | * @param ContainerBuilder $container 48 | * @param LoaderInterface $loader 49 | */ 50 | private function configureAttributeTranslator(array $config, ContainerBuilder $container, LoaderInterface $loader) 51 | { 52 | if (!isset($config['attribute_translator'])) { 53 | throw new \InvalidArgumentException('Expected attribute "attribute_translator" to be set'); 54 | } 55 | 56 | $config = $config['attribute_translator']; 57 | if ($config['type'] === null) { 58 | return; 59 | } 60 | 61 | switch ($config['type']) { 62 | case 'service': 63 | $container->setAlias('be_simple_i18n_routing.translator', $config['id']); 64 | return; 65 | 66 | case 'doctrine_dbal': 67 | $container->setParameter('be_simple_i18n_routing.doctrine_dbal.connection_name', $config['connection']); 68 | 69 | $loader->load('dbal.xml'); 70 | 71 | // BC support for symfony factory 72 | $def = $container->getDefinition('be_simple_i18n_routing.doctrine_dbal.connection'); 73 | 74 | if (method_exists($def, 'setFactory')) { 75 | $def->setFactory(array(new Reference('doctrine'), 'getConnection')); 76 | } else { 77 | $def->setFactoryService('doctrine') 78 | ->setFactoryMethod('getConnection'); 79 | } 80 | 81 | $this->configureDbalCacheDefinition($config['cache'], $container); 82 | $container->setAlias('be_simple_i18n_routing.translator', 'be_simple_i18n_routing.translator.doctrine_dbal'); 83 | 84 | $attributes = array('event' => 'postGenerateSchema'); 85 | if (null !== $config['connection']) { 86 | $attributes['connection'] = $config['connection']; 87 | } 88 | $def = $container->getDefinition('be_simple_i18n_routing.translator.doctrine_dbal.schema_listener'); 89 | $def->addTag('doctrine.event_listener', $attributes); 90 | return; 91 | 92 | case 'translator': 93 | $container->setAlias('be_simple_i18n_routing.translator', 'be_simple_i18n_routing.translator.translation'); 94 | return; 95 | } 96 | 97 | throw new \InvalidArgumentException(sprintf('Unsupported attribute translator type "%s"', $config['type'])); 98 | } 99 | 100 | /** 101 | * Configures the Doctrine cache definition 102 | * 103 | * @param array $cacheDriver 104 | * @param ContainerBuilder $container 105 | */ 106 | private function configureDbalCacheDefinition(array $cacheDriver, ContainerBuilder $container) 107 | { 108 | if ($cacheDriver['type'] === 'memcache') { 109 | if (!empty($cacheDriver['class'])) { 110 | $container->setParameter('be_simple_i18n_routing.doctrine_dbal.cache.memcache.class', $cacheDriver['class']); 111 | } 112 | if (!empty($cacheDriver['instance_class'])) { 113 | $container->setParameter('be_simple_i18n_routing.doctrine_dbal.cache.memcache_instance.class', $cacheDriver['instance_class']); 114 | } 115 | if (!empty($cacheDriver['host'])) { 116 | $container->setParameter('be_simple_i18n_routing.doctrine_dbal.cache.memcache_host', $cacheDriver['host']); 117 | } 118 | if (!empty($cacheDriver['port'])) { 119 | $container->setParameter('be_simple_i18n_routing.doctrine_dbal.cache.memcache_port', $cacheDriver['port']); 120 | } 121 | } 122 | 123 | $container->setAlias('be_simple_i18n_routing.doctrine_dbal.cache', sprintf('be_simple_i18n_routing.doctrine_dbal.cache.%s', $cacheDriver['type'])); 124 | 125 | // generate a unique namespace for the given application 126 | $container->setParameter('be_simple_i18n_routing.doctrine_dbal.cache.namespace', 'be_simple_i18n_'.md5($container->getParameter('kernel.root_dir'))); 127 | } 128 | 129 | /** 130 | * Configures the supported locales 131 | * 132 | * @param array $config 133 | * @param ContainerBuilder $container 134 | */ 135 | private function configureLocales(array $config, ContainerBuilder $container) 136 | { 137 | if (!isset($config['locales'])) { 138 | throw new \InvalidArgumentException('Expected attribute "locales" to be set'); 139 | } 140 | $config = $config['locales']; 141 | 142 | $container->setParameter('be_simple_i18n_routing.default_locale', $config['default_locale']); 143 | $container->setParameter('be_simple_i18n_routing.locales', $config['supported']); 144 | 145 | // Configure the route generator 146 | $routeGenerator = 'be_simple_i18n_routing.route_generator.i18n'; 147 | if ($config['strict'] !== false) { 148 | $container->getDefinition('be_simple_i18n_routing.route_generator.strict') 149 | ->replaceArgument(0, new Reference($routeGenerator)); 150 | 151 | if ($config['strict'] === null) { 152 | $container->getDefinition('be_simple_i18n_routing.route_generator.strict') 153 | ->addMethodCall('allowFallback', array(true)); 154 | } 155 | 156 | $routeGenerator = 'be_simple_i18n_routing.route_generator.strict'; 157 | } 158 | if ($config['filter']) { 159 | $container->getDefinition('be_simple_i18n_routing.route_generator.filter') 160 | ->replaceArgument(0, new Reference($routeGenerator)); 161 | 162 | $routeGenerator = 'be_simple_i18n_routing.route_generator.filter'; 163 | } 164 | 165 | $container->setAlias('be_simple_i18n_routing.route_generator', $routeGenerator); 166 | } 167 | 168 | /** 169 | * Configures the route name inflector 170 | * 171 | * @param ContainerBuilder $container 172 | * @param $config 173 | */ 174 | private function configureRouteNameInflector(array $config, ContainerBuilder $container) 175 | { 176 | if (isset($config['route_name_inflector'])) { 177 | $container->setAlias('be_simple_i18n_routing.route_name_inflector', $config['route_name_inflector']); 178 | } 179 | 180 | if (PHP_VERSION_ID < 70000) { 181 | // Try and register the route name inflector to compilation/caching 182 | try { 183 | $def = $container->findDefinition('be_simple_i18n_routing.route_name_inflector'); 184 | if ($def->getClass() !== null) { 185 | $this->addClassesToCompile(array($def->getClass())); 186 | } 187 | } catch (ServiceNotFoundException $e) { 188 | // This happens when the alias is set to a external service 189 | } catch (InvalidArgumentException $e) { 190 | // This happens when the alias is set to a external service in Symfony 2.3 191 | } 192 | } 193 | } 194 | 195 | /** 196 | * Configures the route annotations loader etc. 197 | * 198 | * @param array $config 199 | * @param ContainerBuilder $container 200 | * @param LoaderInterface $loader 201 | */ 202 | private function configureAnnotations(array $config, ContainerBuilder $container, LoaderInterface $loader) 203 | { 204 | if (!isset($config['annotations']) || !$config['annotations']) { 205 | return; 206 | } 207 | 208 | $loader->load('annotation.xml'); 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /src/Routing/Loader/YamlFileLoader.php: -------------------------------------------------------------------------------- 1 | routeGenerator = $routeGenerator ?: new I18nRouteGenerator(); 36 | } 37 | 38 | /** 39 | * Loads a Yaml file. 40 | * 41 | * @param string $file A Yaml file path 42 | * @param string|null $type The resource type 43 | * 44 | * @return RouteCollection A RouteCollection instance 45 | * 46 | * @throws \InvalidArgumentException When a route can't be parsed because YAML is invalid 47 | */ 48 | public function load($file, $type = null) 49 | { 50 | $path = $this->locator->locate($file); 51 | 52 | if (!stream_is_local($path)) { 53 | throw new \InvalidArgumentException(sprintf('This is not a local file "%s".', $path)); 54 | } 55 | 56 | if (!file_exists($path)) { 57 | throw new \InvalidArgumentException(sprintf('File "%s" not found.', $path)); 58 | } 59 | 60 | if (null === $this->yamlParser) { 61 | $this->yamlParser = new YamlParser(); 62 | } 63 | 64 | try { 65 | $parsedConfig = $this->yamlParser->parse(file_get_contents($path)); 66 | } catch (ParseException $e) { 67 | throw new \InvalidArgumentException(sprintf('The file "%s" does not contain valid YAML.', $path), 0, $e); 68 | } 69 | 70 | $collection = new RouteCollection(); 71 | $collection->addResource(new FileResource($path)); 72 | 73 | // empty file 74 | if (null === $parsedConfig) { 75 | return $collection; 76 | } 77 | 78 | // not an array 79 | if (!is_array($parsedConfig)) { 80 | throw new \InvalidArgumentException(sprintf('The file "%s" must contain a YAML array.', $path)); 81 | } 82 | 83 | foreach ($parsedConfig as $name => $config) { 84 | if (isset($config['pattern'])) { 85 | if (isset($config['path'])) { 86 | throw new \InvalidArgumentException(sprintf('The file "%s" cannot define both a "path" and a "pattern" attribute. Use only "path".', $path)); 87 | } 88 | 89 | $config['path'] = $config['pattern']; 90 | unset($config['pattern']); 91 | } 92 | 93 | $this->validate($config, $name, $path); 94 | 95 | if (isset($config['resource'])) { 96 | $this->parseImport($collection, $config, $path, $file); 97 | } else { 98 | $this->parseRoute($collection, $name, $config, $path); 99 | } 100 | } 101 | 102 | return $collection; 103 | } 104 | 105 | /** 106 | * @inheritdoc 107 | */ 108 | public function supports($resource, $type = null) 109 | { 110 | return 'be_simple_i18n' === $type && is_string($resource) && in_array(pathinfo($resource, PATHINFO_EXTENSION), array('yml', 'yaml'), true); 111 | } 112 | 113 | /** 114 | * Parses a route and adds it to the RouteCollection. 115 | * 116 | * @param RouteCollection $collection A RouteCollection instance 117 | * @param string $name Route name 118 | * @param array $config Route definition 119 | * @param string $path Full path of the YAML file being processed 120 | */ 121 | protected function parseRoute(RouteCollection $collection, $name, array $config, $path) 122 | { 123 | $defaults = isset($config['defaults']) ? $config['defaults'] : array(); 124 | $requirements = isset($config['requirements']) ? $config['requirements'] : array(); 125 | $options = isset($config['options']) ? $config['options'] : array(); 126 | $host = isset($config['host']) ? $config['host'] : ''; 127 | $schemes = isset($config['schemes']) ? $config['schemes'] : array(); 128 | $methods = isset($config['methods']) ? $config['methods'] : array(); 129 | $condition = isset($config['condition']) ? $config['condition'] : null; 130 | 131 | if (isset($config['locales'])) { 132 | $collection->addCollection( 133 | $this->routeGenerator->generateRoutes( 134 | $name, 135 | $config['locales'], 136 | new Route('', $defaults, $requirements, $options, $host, $schemes, $methods, $condition) 137 | ) 138 | ); 139 | } else { 140 | $route = new Route($config['path'], $defaults, $requirements, $options, $host, $schemes, $methods, $condition); 141 | $collection->add($name, $route); 142 | } 143 | } 144 | 145 | /** 146 | * Parses an import and adds the routes in the resource to the RouteCollection. 147 | * 148 | * @param RouteCollection $collection A RouteCollection instance 149 | * @param array $config Route definition 150 | * @param string $path Full path of the YAML file being processed 151 | * @param string $file Loaded file name 152 | */ 153 | protected function parseImport(RouteCollection $collection, array $config, $path, $file) 154 | { 155 | $type = isset($config['type']) ? $config['type'] : null; 156 | $prefix = isset($config['prefix']) ? $config['prefix'] : ''; 157 | $defaults = isset($config['defaults']) ? $config['defaults'] : array(); 158 | $requirements = isset($config['requirements']) ? $config['requirements'] : array(); 159 | $options = isset($config['options']) ? $config['options'] : array(); 160 | $host = isset($config['host']) ? $config['host'] : null; 161 | $condition = isset($config['condition']) ? $config['condition'] : null; 162 | $schemes = isset($config['schemes']) ? $config['schemes'] : null; 163 | $methods = isset($config['methods']) ? $config['methods'] : null; 164 | 165 | $this->setCurrentDir(dirname($path)); 166 | 167 | $subCollection = $this->import($config['resource'], $type, false, $file); 168 | /* @var $subCollection \Symfony\Component\Routing\RouteCollection */ 169 | $subCollection = $this->routeGenerator->generateCollection($prefix, $subCollection); 170 | if (null !== $host) { 171 | $subCollection->setHost($host); 172 | } 173 | if (null !== $condition) { 174 | $subCollection->setCondition($condition); 175 | } 176 | if (null !== $schemes) { 177 | $subCollection->setSchemes($schemes); 178 | } 179 | if (null !== $methods) { 180 | $subCollection->setMethods($methods); 181 | } 182 | $subCollection->addDefaults($defaults); 183 | $subCollection->addRequirements($requirements); 184 | $subCollection->addOptions($options); 185 | 186 | $collection->addCollection($subCollection); 187 | } 188 | 189 | /** 190 | * @inheritDoc 191 | */ 192 | protected function validate($config, $name, $path) 193 | { 194 | if (!is_array($config)) { 195 | throw new \InvalidArgumentException(sprintf('The definition of "%s" in "%s" must be a YAML array.', $name, $path)); 196 | } 197 | if ($extraKeys = array_diff(array_keys($config), self::$availableKeys)) { 198 | throw new \InvalidArgumentException(sprintf( 199 | 'The routing file "%s" contains unsupported keys for "%s": "%s". Expected one of: "%s".', 200 | $path, 201 | $name, 202 | implode('", "', $extraKeys), 203 | implode('", "', self::$availableKeys) 204 | )); 205 | } 206 | if (isset($config['resource']) && isset($config['path'])) { 207 | throw new \InvalidArgumentException(sprintf( 208 | 'The routing file "%s" must not specify both the "resource" key and the "path" key for "%s". Choose between an import and a route definition.', 209 | $path, 210 | $name 211 | )); 212 | } 213 | if (!isset($config['resource']) && isset($config['type'])) { 214 | throw new \InvalidArgumentException(sprintf( 215 | 'The "type" key for the route definition "%s" in "%s" is unsupported. It is only available for imports in combination with the "resource" key.', 216 | $name, 217 | $path 218 | )); 219 | } 220 | if (!isset($config['resource']) && !isset($config['path']) && !isset($config['locales'])) { 221 | throw new \InvalidArgumentException(sprintf( 222 | 'You must define a "path" for the route "%s" in file "%s".', 223 | $name, 224 | $path 225 | )); 226 | } 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /src/Routing/Loader/XmlFileLoader.php: -------------------------------------------------------------------------------- 1 | 17 | * @author Francis Besset 18 | * @author Andrej Hudec 19 | */ 20 | class XmlFileLoader extends FileLoader 21 | { 22 | const NAMESPACE_URI = 'http://besim.pl/schema/i18n_routing'; 23 | const SCHEME_PATH = '/schema/routing/routing-1.0.xsd'; 24 | /** 25 | * @var RouteGeneratorInterface 26 | */ 27 | private $routeGenerator; 28 | 29 | public function __construct(FileLocatorInterface $locator, RouteGeneratorInterface $routeGenerator = null) 30 | { 31 | parent::__construct($locator); 32 | 33 | $this->routeGenerator = $routeGenerator ?: new I18nRouteGenerator(); 34 | } 35 | 36 | /** 37 | * Loads an XML file. 38 | * 39 | * @param string $file An XML file path 40 | * @param string|null $type The resource type 41 | * 42 | * @return RouteCollection A RouteCollection instance 43 | * 44 | * @throws \InvalidArgumentException When the file cannot be loaded or when the XML cannot be 45 | * parsed because it does not validate against the scheme. 46 | */ 47 | public function load($file, $type = null) 48 | { 49 | $path = $this->locator->locate($file); 50 | 51 | $xml = $this->loadFile($path); 52 | 53 | $collection = new RouteCollection(); 54 | $collection->addResource(new FileResource($path)); 55 | 56 | // process routes and imports 57 | foreach ($xml->documentElement->childNodes as $node) { 58 | if (!$node instanceof \DOMElement) { 59 | continue; 60 | } 61 | 62 | $this->parseNode($collection, $node, $path, $file); 63 | } 64 | 65 | return $collection; 66 | } 67 | 68 | /** 69 | * Parses a node from a loaded XML file. 70 | * 71 | * @param RouteCollection $collection Collection to associate with the node 72 | * @param \DOMElement $node Element to parse 73 | * @param string $path Full path of the XML file being processed 74 | * @param string $file Loaded file name 75 | * 76 | * @throws \InvalidArgumentException When the XML is invalid 77 | */ 78 | protected function parseNode(RouteCollection $collection, \DOMElement $node, $path, $file) 79 | { 80 | if (self::NAMESPACE_URI !== $node->namespaceURI) { 81 | return; 82 | } 83 | 84 | switch ($node->localName) { 85 | case 'route': 86 | $this->parseRoute($collection, $node, $path); 87 | break; 88 | case 'import': 89 | $this->parseImport($collection, $node, $path, $file); 90 | break; 91 | default: 92 | throw new \InvalidArgumentException(sprintf('Unknown tag "%s" used in file "%s". Expected "route" or "import".', $node->localName, $path)); 93 | } 94 | } 95 | 96 | /** 97 | * @inheritdoc 98 | */ 99 | public function supports($resource, $type = null) 100 | { 101 | return 'be_simple_i18n' === $type && is_string($resource) && 'xml' === pathinfo($resource, PATHINFO_EXTENSION); 102 | } 103 | 104 | /** 105 | * Parses a route and adds it to the RouteCollection. 106 | * 107 | * @param RouteCollection $collection RouteCollection instance 108 | * @param \DOMElement $node Element to parse that represents a Route 109 | * @param string $path Full path of the XML file being processed 110 | * 111 | * @throws \InvalidArgumentException When the XML is invalid 112 | */ 113 | protected function parseRoute(RouteCollection $collection, \DOMElement $node, $path) 114 | { 115 | if ('' === ($id = $node->getAttribute('id'))) { 116 | throw new \InvalidArgumentException(sprintf('The element in file "%s" must have an "id" attribute.', $path)); 117 | } 118 | 119 | if ($node->hasAttribute('pattern')) { 120 | if ($node->hasAttribute('path')) { 121 | throw new \InvalidArgumentException(sprintf('The element in file "%s" cannot define both a "path" and a "pattern" attribute. Use only "path".', $path)); 122 | } 123 | 124 | $node->setAttribute('path', $node->getAttribute('pattern')); 125 | $node->removeAttribute('pattern'); 126 | } 127 | 128 | $schemes = preg_split('/[\s,\|]++/', $node->getAttribute('schemes'), -1, PREG_SPLIT_NO_EMPTY); 129 | $methods = preg_split('/[\s,\|]++/', $node->getAttribute('methods'), -1, PREG_SPLIT_NO_EMPTY); 130 | 131 | list($defaults, $requirements, $options, $condition, $localesWithPaths) = $this->parseConfigs($node, $path); 132 | 133 | if ($localesWithPaths) { 134 | $collection->addCollection( 135 | $this->routeGenerator->generateRoutes( 136 | $id, 137 | $localesWithPaths, 138 | new Route('', $defaults, $requirements, $options, $node->getAttribute('host'), $schemes, $methods, $condition) 139 | ) 140 | ); 141 | } else { 142 | if (!$node->hasAttribute('pattern') && !$node->hasAttribute('path')) { 143 | throw new \InvalidArgumentException(sprintf('The element in file "%s" must have an "path" attribute.', $path)); 144 | } 145 | 146 | $route = new Route($node->getAttribute('path'), $defaults, $requirements, $options, $node->getAttribute('host'), $schemes, $methods, $condition); 147 | $collection->add($id, $route); 148 | } 149 | } 150 | 151 | /** 152 | * Parses an import and adds the routes in the resource to the RouteCollection. 153 | * 154 | * @param RouteCollection $collection RouteCollection instance 155 | * @param \DOMElement $node Element to parse that represents a Route 156 | * @param string $path Full path of the XML file being processed 157 | * @param string $file Loaded file name 158 | * 159 | * @throws \InvalidArgumentException When the XML is invalid 160 | */ 161 | protected function parseImport(RouteCollection $collection, \DOMElement $node, $path, $file) 162 | { 163 | if ('' === $resource = $node->getAttribute('resource')) { 164 | throw new \InvalidArgumentException(sprintf('The element in file "%s" must have a "resource" attribute.', $path)); 165 | } 166 | 167 | $type = $node->getAttribute('type'); 168 | $prefix = $node->getAttribute('prefix'); 169 | $host = $node->hasAttribute('host') ? $node->getAttribute('host') : null; 170 | $schemes = $node->hasAttribute('schemes') ? preg_split('/[\s,\|]++/', $node->getAttribute('schemes'), -1, PREG_SPLIT_NO_EMPTY) : null; 171 | $methods = $node->hasAttribute('methods') ? preg_split('/[\s,\|]++/', $node->getAttribute('methods'), -1, PREG_SPLIT_NO_EMPTY) : null; 172 | 173 | list($defaults, $requirements, $options, $condition, $localesWithPaths) = $this->parseConfigs($node, $path); 174 | 175 | $this->setCurrentDir(dirname($path)); 176 | 177 | $subCollection = $this->import($resource, ('' !== $type ? $type : null), false, $file); 178 | /* @var $subCollection \Symfony\Component\Routing\RouteCollection */ 179 | $subCollection = $this->routeGenerator->generateCollection( 180 | empty($localesWithPaths) ? $prefix : $localesWithPaths, 181 | $subCollection 182 | ); 183 | if (null !== $host) { 184 | $subCollection->setHost($host); 185 | } 186 | if (null !== $condition) { 187 | $subCollection->setCondition($condition); 188 | } 189 | if (null !== $schemes) { 190 | $subCollection->setSchemes($schemes); 191 | } 192 | if (null !== $methods) { 193 | $subCollection->setMethods($methods); 194 | } 195 | $subCollection->addDefaults($defaults); 196 | $subCollection->addRequirements($requirements); 197 | $subCollection->addOptions($options); 198 | 199 | $collection->addCollection($subCollection); 200 | } 201 | 202 | /** 203 | * Loads an XML file. 204 | * 205 | * @param string $file An XML file path 206 | * 207 | * @return \DOMDocument 208 | * 209 | * @throws \InvalidArgumentException When loading of XML file fails because of syntax errors 210 | * or when the XML structure is not as expected by the scheme - 211 | * see validate() 212 | */ 213 | protected function loadFile($file) 214 | { 215 | return XmlUtils::loadFile($file, __DIR__ . static::SCHEME_PATH); 216 | } 217 | 218 | /** 219 | * Parses the config elements (default, requirement, option). 220 | * 221 | * @param \DOMElement $node Element to parse that contains the configs 222 | * @param string $path Full path of the XML file being processed 223 | * 224 | * @return array An array with the defaults as first item, requirements as second and options as third. 225 | * 226 | * @throws \InvalidArgumentException When the XML is invalid 227 | */ 228 | private function parseConfigs(\DOMElement $node, $path) 229 | { 230 | $defaults = array(); 231 | $requirements = array(); 232 | $options = array(); 233 | $condition = null; 234 | $locales = array(); 235 | 236 | foreach ($node->getElementsByTagNameNS(self::NAMESPACE_URI, '*') as $n) { 237 | switch ($n->localName) { 238 | case 'default': 239 | if ($this->isElementValueNull($n)) { 240 | $defaults[$n->getAttribute('key')] = null; 241 | } else { 242 | $defaults[$n->getAttribute('key')] = trim($n->textContent); 243 | } 244 | 245 | break; 246 | case 'requirement': 247 | $requirements[$n->getAttribute('key')] = trim($n->textContent); 248 | break; 249 | case 'option': 250 | $options[$n->getAttribute('key')] = trim($n->textContent); 251 | break; 252 | case 'condition': 253 | $condition = trim($n->textContent); 254 | break; 255 | case 'locale': 256 | $locales[$n->getAttribute('key')] = trim((string) $n->nodeValue); 257 | break; 258 | default: 259 | throw new \InvalidArgumentException(sprintf('Unknown tag "%s" used in file "%s". Expected "default", "requirement" or "option".', $n->localName, $path)); 260 | } 261 | } 262 | 263 | return array($defaults, $requirements, $options, $condition, $locales); 264 | } 265 | 266 | private function isElementValueNull(\DOMElement $element) 267 | { 268 | $namespaceUri = 'http://www.w3.org/2001/XMLSchema-instance'; 269 | 270 | if (!$element->hasAttributeNS($namespaceUri, 'nil')) { 271 | return false; 272 | } 273 | 274 | return 'true' === $element->getAttributeNS($namespaceUri, 'nil') || '1' === $element->getAttributeNS($namespaceUri, 'nil'); 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | I18nRoutingBundle, generate your I18N Routes for Symfony2 2 | ========================================================= 3 | 4 | If you have a website multilingual, this bundle avoids of copy paste your routes 5 | for different languages. Additionally it allows to translate given routing parameters 6 | between languages in Router#match and UrlGenerator#generate using either a Symfony Translator 7 | or a Doctrine DBAL (+Cache) based backend. 8 | 9 | [![Latest Version on Packagist][ico-version]][link-packagist] 10 | [![Software License][ico-license]](src/Resources/meta/LICENSE) 11 | [![Build Status][ico-travis]][link-travis] 12 | [![Total Downloads][ico-downloads]][link-packagist] 13 | 14 | ## Information 15 | 16 | When you create an I18N route and you go on it with your browser, the locale will be updated. 17 | 18 | ## Installation 19 | 20 | ```bash 21 | composer.phar require besimple/i18n-routing-bundle 22 | ``` 23 | 24 | ```php 25 | //app/AppKernel.php 26 | public function registerBundles() 27 | { 28 | $bundles = array( 29 | //... 30 | new BeSimple\I18nRoutingBundle\BeSimpleI18nRoutingBundle(), 31 | ); 32 | } 33 | ``` 34 | 35 | ### Update your configuration 36 | 37 | ```yaml 38 | # app/config/config.yml 39 | be_simple_i18n_routing: ~ 40 | ``` 41 | 42 | ## Create your routing 43 | 44 | To define internationalized routes in XML or YAML, you need to import the 45 | routing file by using the ``be_simple_i18n`` type: 46 | 47 | ```yaml 48 | my_yaml_i18n_routes: 49 | resource: "@MyWebsiteBundle/Resources/config/routing/i18n.yml" 50 | type: be_simple_i18n 51 | prefix: 52 | en: /website 53 | fr: /site 54 | de: /webseite 55 | my_xml_i18n_routes: 56 | resource: "@MyWebsiteBundle/Resources/config/routing/i18n.xml" 57 | type: be_simple_i18n 58 | ``` 59 | 60 | You can optionally specify a prefix or translated prefixes as shown above. 61 | 62 | ### Yaml routing file 63 | 64 | ```yaml 65 | homepage: 66 | locales: { en: "/welcome", fr: "/bienvenue", de: "/willkommen" } 67 | defaults: { _controller: MyWebsiteBundle:Frontend:index } 68 | ``` 69 | 70 | ### XML routing file 71 | 72 | ```xml 73 | 74 | 77 | 78 | 79 | /welcome 80 | /bienvenue 81 | /willkommen 82 | MyWebsiteBundle:Frontend:index 83 | 84 | 85 | ``` 86 | 87 | Note that the XML file uses a different namespace than when using the core 88 | loader: ``http://besim.pl/schema/i18n_routing``. 89 | 90 | ### PHP routing file 91 | 92 | ```php 93 | addCollection( 102 | $generator->generateRoutes( 103 | 'homepage', 104 | array('en' => '/welcome', 'fr' => '/bienvenue', 'de' => '/willkommen'), 105 | new Route('', array( 106 | '_controller' => 'MyWebsiteBundle:Frontend:index' 107 | )) 108 | ) 109 | ); 110 | 111 | return $collection; 112 | ``` 113 | 114 | ### Controller annotations 115 | 116 | Annotation loading is only supported for Symfony 2.5 and greater and needs to be enabled as followed. 117 | ```YAML 118 | # app/config/config.yml 119 | be_simple_i18n_routing: 120 | annotations: true 121 | ``` 122 | 123 | ```PHP 124 | use BeSimple\I18nRoutingBundle\Routing\Annotation\I18nRoute; 125 | 126 | class NoPrefixController 127 | { 128 | /** 129 | * @I18nRoute({ "en": "/welcome", "fr": "/bienvenue", "de": "/willkommen" }, name="homepage") 130 | */ 131 | public function indexAction() { } 132 | } 133 | ``` 134 | 135 | ### You can insert classic route in your routing 136 | 137 | #### Yaml routing file 138 | 139 | ```yaml 140 | homepage: 141 | locales: { en: "/en/", fr: "/fr/", de: "/de/" } 142 | defaults: { _controller: HelloBundle:Frontend:homepage } 143 | 144 | welcome: 145 | locales: { en: "/welcome/{name}", fr: "/bienvenue/{name}", de: "/willkommen/{name}" } 146 | defaults: { _controller: MyWebsiteBundle:Frontend:welcome } 147 | ``` 148 | 149 | #### XML routing file 150 | 151 | ```xml 152 | 153 | 154 | 157 | 158 | 159 | HelloBundle:Hello:index 160 | 161 | 162 | /welcome/{name} 163 | /bienvenue/{name} 164 | /willkommen/{name} 165 | MyWebsiteBundle:Frontend:index 166 | 167 | 168 | ``` 169 | 170 | #### PHP routing file 171 | 172 | ```php 173 | add('hello', new Route('/hello/{name}', array( 183 | '_controller' => 'HelloBundle:Hello:index', 184 | ))); 185 | $collection->addCollection( 186 | $generator->generateRoutes( 187 | 'homepage', 188 | array('en' => '/welcome/{name}', 'fr' => '/bienvenue/{name}', 'de' => '/willkommen/{name}'), 189 | new Route('', array( 190 | '_controller' => 'MyWebsiteBundle:Frontend:index', 191 | )) 192 | ) 193 | ); 194 | 195 | return $collection; 196 | ``` 197 | 198 | ### Advanced locale support 199 | 200 | By default this bundle allows any locale to be used and there is no check if a locale is missing for a specific route. 201 | This is great but sometimes you may wish to be strict, let take a look at the following configuration: 202 | ```YAML 203 | be_simple_i18n_routing: 204 | locales: 205 | supported: ['en', 'nl'] 206 | filter: true 207 | strict: true 208 | ``` 209 | 210 | The `locales.supported` specifies which locales are supported. 211 | 212 | The `locales.filter` option is responsible for filtering out any unknown locales so only routes for 'en' and 'nl' are available. 213 | 214 | The `locales.strict` option when set to `true` is responsible for throwing a exception when a i18n route is found where the locale is unknown or where a locale is missing. 215 | This option can also be set to `null` to disable locale is missing for a route exception and `false` to disable exceptions. 216 | 217 | ### Route naming 218 | 219 | By default all routes that are imported are named '.' but sometimes you may want to change this behaviour. 220 | To do this you can specify a route name inflector service in your configuration as followed. 221 | ```YAML 222 | be_simple_i18n_routing: 223 | route_name_inflector: 'my_route_name_inflector_service' 224 | ``` 225 | *The service must implement the `BeSimple\I18nRoutingBundle\Routing\RouteGenerator\NameInflector\RouteNameInflectorInterface` interface.* 226 | 227 | There are currently 2 inflectors available by default [`be_simple_i18n_routing.route_name_inflector.postfix`](src/Routing/RouteGenerator/NameInflector/PostfixInflector.php) and [`be_simple_i18n_routing.route_name_inflector.default_postfix`](src/Routing/RouteGenerator/NameInflector/DefaultPostfixInflector.php). 228 | 229 | #### Default postfix inflector 230 | The default postfix inflector changed the behaviour of to only add a locale postfix when the locale is not the default locale. 231 | A example configuration is as followed. 232 | ```YAML 233 | be_simple_i18n_routing: 234 | route_name_inflector: 'my_route_name_inflector_service' 235 | locales: 236 | default_locale: '%kernel.default_locale%' 237 | ``` 238 | 239 | ## Generate a route in your templates 240 | 241 | ### Specify a locale 242 | 243 | #### Twig 244 | 245 | {{ path('homepage.en') }} 246 | {{ path('homepage', { 'locale': 'en' }) }} 247 | {{ path('homepage.fr') }} 248 | {{ path('homepage', { 'locale': 'fr' }) }} 249 | {{ path('homepage.de') }} 250 | {{ path('homepage', { 'locale': 'de' }) }} 251 | 252 | #### PHP 253 | 254 | ```php 255 | generate('homepage.en') ?> 256 | generate('homepage', array('locale' => 'en')) ?> 257 | generate('homepage.fr') ?> 258 | generate('homepage', array('locale' => 'fr')) ?> 259 | generate('homepage.de') ?> 260 | generate('homepage', array('locale' => 'de')) ?> 261 | ``` 262 | 263 | ### Use current locale of user 264 | 265 | #### Twig 266 | 267 | {{ path('homepage') }} 268 | 269 | #### PHP 270 | 271 | ```php 272 | generate('homepage') ?> 273 | ``` 274 | 275 | ## Translating the route attributes 276 | 277 | If the static parts of your routes are translated you get to the point really 278 | fast when dynamic parts such as product slugs, category names or other dynamic 279 | routing parameters should be translated. The bundle provides 2 implementations. 280 | 281 | After configuring the backend you want to use (see below for each one), you 282 | can define a to be translated attribute in your route defaults: 283 | 284 | ```yaml 285 | product_view: 286 | locales: { en: "/product/{slug}", de: "/produkt/{slug}" } 287 | defaults: { _controller: "ShopBundle:Product:view", _translate: "slug" } 288 | product_view2: 289 | locales: { en: "/product/{category}/{slug}", de: "/produkt/{category}/{slug}" } 290 | defaults: 291 | _controller: "ShopBundle:Product:view" 292 | _translate: ["slug", "category"] 293 | ``` 294 | 295 | The same goes with generating routes, now backwards: 296 | 297 | {{ path("product_view", {"slug": product.slug, "translate": "slug"}) }} 298 | {{ path("product_view2", {"slug": product.slug, "translate": ["slug", "category]}) }} 299 | 300 | The reverse translation is only necessary if you have the "original" values 301 | in your templates. If you have access to the localized value of the current 302 | locale then you can just pass this and do not hint to translate it with the 303 | "translate" key. 304 | 305 | ### Doctrine DBAL Backend 306 | 307 | Configure the use of the DBAL backend 308 | 309 | ```yaml 310 | # app/config/config.yml 311 | be_simple_i18n_routing: 312 | attribute_translator: 313 | type: doctrine_dbal 314 | connection: default # Doctrine DBAL connection name. Using null (default value) will use the default connection 315 | cache: apc 316 | ``` 317 | 318 | The Doctrine Backend has the following table structure: 319 | 320 | ```sql 321 | CREATE TABLE routing_translations ( 322 | id INT NOT NULL, 323 | route VARCHAR(255) NOT NULL, 324 | locale VARCHAR(255) NOT NULL, 325 | attribute VARCHAR(255) NOT NULL, 326 | localized_value VARCHAR(255) NOT NULL, 327 | original_value VARCHAR(255) NOT NULL, 328 | UNIQUE INDEX UNIQ_291BA3522C420794180C698FA7AEFFB (route, locale, attribute), 329 | INDEX IDX_291BA352D951F3E4 (localized_value), 330 | PRIMARY KEY(id) 331 | ) ENGINE = InnoDB; 332 | ``` 333 | 334 | Lookups are made through the combination of route name, locale and attribute 335 | of the route to be translated. 336 | 337 | Every lookup is cached in a Doctrine\Common\Cache\Cache instance that you 338 | should configure to be APC, Memcache or Xcache for performance reasons. 339 | 340 | If you are using Doctrine it automatically registers a listener for SchemaTool 341 | to create the routing_translations table for your database backend, you only 342 | have to call: 343 | 344 | ./app/console doctrine:schema:update --dump-sql 345 | ./app/console doctrine:schema:update --force 346 | 347 | ### Translator backend 348 | 349 | This implementation uses the Symfony2 translator to translate the attributes. 350 | The translation domain will be created using the pattern `_` 351 | 352 | ```yaml 353 | # app/config/config.yml 354 | be_simple_i18n_routing: 355 | attribute_translator: 356 | type: translator 357 | ``` 358 | 359 | ### Custom backend 360 | 361 | If you want to use a different implementation, simply create a service implementing 362 | `BeSimple\I18nRoutingBundle\Routing\Translator\AttributeTranslatorInterface`. 363 | 364 | ```yaml 365 | # app/config/config.yml 366 | be_simple_i18n_routing: 367 | attribute_translator: 368 | type: service 369 | id: my_attribute_translator 370 | ``` 371 | 372 | 373 | ## License 374 | 375 | This bundle is under the MIT License (MIT). Please see [License File](src/Resources/meta/LICENSE) for more information. 376 | 377 | [ico-version]: https://img.shields.io/packagist/v/BeSimple/i18n-routing-bundle.svg?style=flat-square 378 | [ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square 379 | [ico-travis]: https://img.shields.io/travis/BeSimple/BeSimpleI18nRoutingBundle/master.svg?style=flat-square 380 | [ico-downloads]: https://img.shields.io/packagist/dt/BeSimple/i18n-routing-bundle.svg?style=flat-square 381 | 382 | [link-packagist]: https://packagist.org/packages/BeSimple/i18n-routing-bundle 383 | [link-travis]: https://travis-ci.org/BeSimple/BeSimpleI18nRoutingBundle 384 | --------------------------------------------------------------------------------