|
| 1 | +.. index:: |
| 2 | + single: Multi Language; Book |
| 3 | + |
| 4 | +Handling Multi-Language Documents |
| 5 | +================================= |
| 6 | + |
| 7 | +The default storage layer of the CMF, PHPCR-ODM, can handle translations |
| 8 | +transparently. |
| 9 | + |
| 10 | +.. tip:: |
| 11 | + |
| 12 | + This chapter assumes you already have an idea how to interact with |
| 13 | + PHPCR-ODM, which was introduced in the :doc:`Database chapter <database_layer>`. |
| 14 | + |
| 15 | +.. caution:: |
| 16 | + |
| 17 | + You also need the ``intl`` php extension installed and enabled (otherwise |
| 18 | + composer will tell you it can't find ext-intl). If you get issues that some |
| 19 | + locales can not be loaded, have a look at `this discussion about ICU`_. |
| 20 | + |
| 21 | +Initial Language Choice: Lunetics LocaleBundle |
| 22 | +---------------------------------------------- |
| 23 | + |
| 24 | +The CMF recommends to rely on the `LuneticsLocaleBundle`_ |
| 25 | +to handle requests to ``/`` on your website. This bundle provides the tools |
| 26 | +to select the best locale for the user based on various criteria. |
| 27 | + |
| 28 | +When you configure ``lunetics_locale``, it is recommended to use a parameter |
| 29 | +for the locales, as you need to configure the locales for other bundles |
| 30 | +(e.g. the CoreBundle) too. |
| 31 | + |
| 32 | +.. configuration-block:: |
| 33 | + |
| 34 | + .. code-block:: yaml |
| 35 | +
|
| 36 | + lunetics_locale: |
| 37 | + allowed_locales: "%locales%" |
| 38 | +
|
| 39 | + .. code-block:: xml |
| 40 | +
|
| 41 | + <?xml version="1.0" charset="UTF-8" ?> |
| 42 | + <container xmlns="http://symfony.com/schema/dic/services"> |
| 43 | +
|
| 44 | + <config xmlns="http://example.org/schema/dic/lunetics_locale"> |
| 45 | + <allowed-locales>%locales%</allowed-locales> |
| 46 | + </config> |
| 47 | + </container> |
| 48 | +
|
| 49 | + .. code-block:: php |
| 50 | +
|
| 51 | + $container->loadFromExtension('lunetics_locale', array( |
| 52 | + 'allowed_locales' => '%locales%', |
| 53 | + )); |
| 54 | +
|
| 55 | +Configuring Available Locales |
| 56 | +----------------------------- |
| 57 | + |
| 58 | +The CoreBundle needs to be configure with the available locales. If it is |
| 59 | +not configured with locales, it registeres a listener that removes all |
| 60 | +translation mapping from PHPCR-ODM documents. |
| 61 | + |
| 62 | +.. configuration-block:: |
| 63 | + |
| 64 | + .. code-block:: yaml |
| 65 | +
|
| 66 | + cmf_core: |
| 67 | + multilang: |
| 68 | + locales: "%locales%" |
| 69 | +
|
| 70 | + .. code-block:: xml |
| 71 | +
|
| 72 | + <?xml version="1.0" charset="UTF-8" ?> |
| 73 | + <container xmlns="http://symfony.com/schema/dic/services"> |
| 74 | +
|
| 75 | + <config xmlns="http://cmf.symfony.com/schema/dic/core"> |
| 76 | + <multilang>%locales%</multilang> |
| 77 | + </config> |
| 78 | + </container> |
| 79 | +
|
| 80 | + .. code-block:: php |
| 81 | +
|
| 82 | + $container->loadFromExtension('cmf_core', array( |
| 83 | + 'multilang' => array( |
| 84 | + 'locales' => '%locales%', |
| 85 | + ), |
| 86 | + )); |
| 87 | +
|
| 88 | +PHPCR-ODM multi-language Documents |
| 89 | +---------------------------------- |
| 90 | + |
| 91 | +You can mark any properties as being translatable and have the document |
| 92 | +manager store and load the correct language for you. Note that translation |
| 93 | +always happens on a document level, not on the individual translatable fields. |
| 94 | + |
| 95 | +.. code-block:: php |
| 96 | +
|
| 97 | + <?php |
| 98 | +
|
| 99 | + // src/Acme/DemoBundle/Document/MyPersistentClass.php |
| 100 | +
|
| 101 | + use Doctrine\ODM\PHPCR\Mapping\Annotations as PHPCR; |
| 102 | +
|
| 103 | + /** |
| 104 | + * @PHPCR\Document(translator="attribute") |
| 105 | + */ |
| 106 | + class MyPersistentClass |
| 107 | + { |
| 108 | + /** |
| 109 | + * Translated property |
| 110 | + * @PHPCR\String(translated=true) |
| 111 | + */ |
| 112 | + private $topic; |
| 113 | +
|
| 114 | + // ... |
| 115 | + } |
| 116 | +
|
| 117 | +.. seealso:: |
| 118 | + |
| 119 | + Read more about multi-language documents in the |
| 120 | + `PHPCR-ODM documentation on multi-language`_ and see |
| 121 | + :doc:`../bundles/phpcr_odm/multilang` for information how to configure |
| 122 | + PHPCR-ODM correctly. |
| 123 | + |
| 124 | +The default documents provided by the CMF bundles are translated documents. |
| 125 | +The CoreBundle removes the translation mapping if ``multilang`` is not |
| 126 | +configured. |
| 127 | + |
| 128 | +The routes are handled differently, as you can read in the next section. |
| 129 | + |
| 130 | +Routing |
| 131 | +------- |
| 132 | + |
| 133 | +The ``DynamicRouter`` uses a route source to get routes that could match a |
| 134 | +request. The concept of the default PHPCR-ODM source is to map the request URL |
| 135 | +onto an id, which in PHPCR terms is the repository path to a node. This allows |
| 136 | +for a very efficient lookup without needing a full search over the repository. |
| 137 | +But a PHPCR node has exactly one path, therefore you need a separate route |
| 138 | +document per locale. The cool thing with this is that you can localize |
| 139 | +the URL for each language. Simply create one route document per locale. |
| 140 | + |
| 141 | +As all routes point to the same content, the route generator can handle |
| 142 | +picking the correct route for you when you generate the route from the |
| 143 | +content. See also |
| 144 | +":ref:`ContentAwareGenerator and Locales <component-route-generator-and-locales>`". |
| 145 | + |
| 146 | +.. _book_handling-multilang_sonata-admin: |
| 147 | + |
| 148 | +Sonata PHPCR-ODM Admin |
| 149 | +---------------------- |
| 150 | + |
| 151 | +.. note:: |
| 152 | + |
| 153 | + Using sonata admin is one way to make your content editable. A book |
| 154 | + chapter on sonata admin is planned. Meanwhile, read |
| 155 | + :doc:`Sonata Admin <../cookbook/creating_a_cms/sonata-admin>` in the |
| 156 | + "Creating a CMS" tutorial. |
| 157 | + |
| 158 | +The first step is to configure sonata admin. You should place the |
| 159 | +LuneticsLocaleBundle language switcher in the ``topnav`` bar. To do this, |
| 160 | +configure the template for the ``user_block``: |
| 161 | + |
| 162 | +.. configuration-block:: |
| 163 | + |
| 164 | + .. code-block:: yaml |
| 165 | +
|
| 166 | + # app/config/config.yml |
| 167 | + sonata_admin: |
| 168 | + # ... |
| 169 | + templates: |
| 170 | + user_block: AcmeCoreBundle:Admin:admin_topnav.html.twig |
| 171 | +
|
| 172 | + .. code-block:: xml |
| 173 | +
|
| 174 | + <!-- app/config/config.xml --> |
| 175 | + <?xml version="1.0" encoding="UTF-8" ?> |
| 176 | + <container xmlns="http://symfony.com/schema/dic/services"> |
| 177 | + <config xmlns="http://sonata-project.org/schema/dic/admin"> |
| 178 | + <template user-block="AcmeCoreBundle:Admin:admin_topnav.html.twig"/> |
| 179 | + </config> |
| 180 | + </container> |
| 181 | +
|
| 182 | +
|
| 183 | + .. code-block:: php |
| 184 | +
|
| 185 | + // app/config/config.php |
| 186 | + $container->loadFromExtension('sonata_admin', array( |
| 187 | + 'templates' => array( |
| 188 | + 'user_block' => 'AcmeCoreBundle:Admin:admin_topnav.html.twig', |
| 189 | + ), |
| 190 | + )); |
| 191 | +
|
| 192 | +And the template looks like this: |
| 193 | + |
| 194 | +.. code-block:: jinja |
| 195 | +
|
| 196 | + {# src/Acme/CoreBundle/Resources/views/Admin/admin_topnav.html.twig #} |
| 197 | + {% extends 'SonataAdminBundle:Core:user_block.html.twig' %} |
| 198 | +
|
| 199 | + {% block user_block %} |
| 200 | + {{ locale_switcher(null, null, 'AcmeCoreBundle:Admin:switcher_links.html.twig') }} |
| 201 | + {{ parent() }} |
| 202 | + {% endblock %} |
| 203 | +
|
| 204 | +You need to tell the ``locale_switcher`` to use a custom template to display |
| 205 | +the links, which looks like this: |
| 206 | + |
| 207 | +.. code-block:: jinja |
| 208 | +
|
| 209 | + {# src/Acme/CoreBundle/Resources/views/Admin/switcher_links.html.twig #} |
| 210 | + Switch to : |
| 211 | + {% for locale in locales %} |
| 212 | + {% if loop.index > 1 %} | {% endif %}<a href="{{ locale.link }}" title="{{ locale.locale_target_language }}">{{ locale.locale_target_language }}</a> |
| 213 | + {% endfor %} |
| 214 | +
|
| 215 | +Now what is left to do is to update the sonata routes to become locale aware: |
| 216 | + |
| 217 | +.. configuration-block:: |
| 218 | + |
| 219 | + .. code-block:: yaml |
| 220 | +
|
| 221 | + # app/config/routing.yml |
| 222 | +
|
| 223 | + admin_dashboard: |
| 224 | + pattern: /{_locale}/admin/ |
| 225 | + defaults: |
| 226 | + _controller: FrameworkBundle:Redirect:redirect |
| 227 | + route: sonata_admin_dashboard |
| 228 | + permanent: true # this for 301 |
| 229 | +
|
| 230 | + admin: |
| 231 | + resource: '@SonataAdminBundle/Resources/config/routing/sonata_admin.xml' |
| 232 | + prefix: /{_locale}/admin |
| 233 | +
|
| 234 | + sonata_admin: |
| 235 | + resource: . |
| 236 | + type: sonata_admin |
| 237 | + prefix: /{_locale}/admin |
| 238 | +
|
| 239 | + # redirect routes for the non-locale routes |
| 240 | + admin_without_locale: |
| 241 | + pattern: /admin |
| 242 | + defaults: |
| 243 | + _controller: FrameworkBundle:Redirect:redirect |
| 244 | + route: sonata_admin_dashboard |
| 245 | + permanent: true # this for 301 |
| 246 | +
|
| 247 | + admin_dashboard_without_locale: |
| 248 | + pattern: /admin/dashboard |
| 249 | + defaults: |
| 250 | + _controller: FrameworkBundle:Redirect:redirect |
| 251 | + route: sonata_admin_dashboard |
| 252 | + permanent: true |
| 253 | +
|
| 254 | + .. code-block:: xml |
| 255 | +
|
| 256 | + <?xml version="1.0" encoding="UTF-8" ?> |
| 257 | + <routes xmlns="http://symfony.com/schema/dic/routing"> |
| 258 | +
|
| 259 | + <route id="admin_dashboard" pattern="/{_locale}/admin/"> |
| 260 | + <default key="_controller">FrameworkBundle:Redirect:redirect</default> |
| 261 | + <default key="route">sonata_admin_dashboard</default> |
| 262 | + <default "permanent">true</default> |
| 263 | + </route> |
| 264 | +
|
| 265 | + <import resource="@SonataAdminBundle/Resources/config/routing/sonata_admin.xml" |
| 266 | + prefix="/{_locale}/admin" |
| 267 | + /> |
| 268 | +
|
| 269 | + <import resource="." type="sonata_admin" prefix="/{_locale}/admin"/> |
| 270 | +
|
| 271 | + <!-- redirect routes for the non-locale routes --> |
| 272 | + <route id="admin_without_locale" pattern="/admin"> |
| 273 | + <default key="_controller">FrameworkBundle:Redirect:redirect</default> |
| 274 | + <default key="route">sonata_admin_dashboard</default> |
| 275 | + <default "permanent">true</default> |
| 276 | + </route> |
| 277 | + <route id="admin_dashboard_without_locale" pattern="/admin/dashboard"> |
| 278 | + <default key="_controller">FrameworkBundle:Redirect:redirect</default> |
| 279 | + <default key="route">sonata_admin_dashboard</default> |
| 280 | + <default "permanent">true</default> |
| 281 | + </route> |
| 282 | + </routes> |
| 283 | +
|
| 284 | + .. code-block:: php |
| 285 | +
|
| 286 | + // app/config/routing.php |
| 287 | +
|
| 288 | + $collection = new RouteCollection(); |
| 289 | +
|
| 290 | + $collection->add('admin_dashboard', new Route('/{_locale}/admin/', array( |
| 291 | + '_controller' => 'FrameworkBundle:Redirect:redirect', |
| 292 | + 'route' => 'sonata_admin_dashboard', |
| 293 | + 'permanent' => true, |
| 294 | + ))); |
| 295 | +
|
| 296 | + $sonata = $loader->import('@SonataAdminBundle/Resources/config/routing/sonata_admin.xml'); |
| 297 | + $sonata->addPrefix('/{_locale}/admin'); |
| 298 | + $collection->addCollection($sonata); |
| 299 | +
|
| 300 | + $sonata = $loader->import('.', 'sonata_admin'); |
| 301 | + $sonata->addPrefix('/{_locale}/admin'); |
| 302 | + $collection->addCollection($sonata); |
| 303 | +
|
| 304 | + $collection->add('admin_without_locale', new Route('/admin', array( |
| 305 | + '_controller' => 'FrameworkBundle:Redirect:redirect', |
| 306 | + 'route' => 'sonata_admin_dashboard', |
| 307 | + 'permanent' => true, |
| 308 | + ))); |
| 309 | +
|
| 310 | + $collection->add('admin_dashboard_without_locale', new Route('/admin/dashboard', array( |
| 311 | + '_controller' => 'FrameworkBundle:Redirect:redirect', |
| 312 | + 'route' => 'sonata_admin_dashboard', |
| 313 | + 'permanent' => true, |
| 314 | + ))); |
| 315 | +
|
| 316 | + return $collection |
| 317 | +
|
| 318 | +Now reload the admin dashboard. The URL should be prefixed with the |
| 319 | +default locale, for example ``/de/admin/dashboard``. When clicking on the |
| 320 | +language switcher, the page reloads and displays the correct content for the |
| 321 | +requested language. |
| 322 | + |
| 323 | +If your documents implement the TranslatableInterface, you can |
| 324 | +:ref:`configure the translatable admin extension <bundle-core-translatable-admin-extension>` |
| 325 | +to get a language choice field to let the administrator |
| 326 | +choose in which language to store the content. |
| 327 | + |
| 328 | +Frontend Editing and multi-language |
| 329 | +----------------------------------- |
| 330 | + |
| 331 | +When using the CreateBundle, you do not need to do anything at all to get |
| 332 | +multi-language support. PHPCR-ODM will deliver the document in the requested |
| 333 | +language, and the callback URL is generated in the request locale, leading to |
| 334 | +save the edited document in the same language as it was loaded. |
| 335 | + |
| 336 | +.. note:: |
| 337 | + |
| 338 | + If a translation is missing, language fallback kicks in, both when viewing |
| 339 | + the page but also when saving the changes, so you always update the |
| 340 | + current locale. |
| 341 | + |
| 342 | + It would make sense to offer the user the choice whether they want to |
| 343 | + create a new translation or update the existing one. There is this |
| 344 | + `issue`_ in the CreateBundle issue tracker. |
| 345 | + |
| 346 | +.. _`LuneticsLocaleBundle`: https://github.com/lunetics/LocaleBundle/ |
| 347 | +.. _`this discussion about ICU`: https://github.com/symfony/symfony/issues/5279#issuecomment-11710480 |
| 348 | +.. _`cmf-sandbox config.yml file`: https://github.com/symfony-cmf/cmf-sandbox/blob/master/app/config/config.yml |
| 349 | +.. _`PHPCR-ODM documentation on multi-language`: http://docs.doctrine-project.org/projects/doctrine-phpcr-odm/en/latest/reference/multilang.html |
| 350 | +.. _`issue`: https://github.com/symfony-cmf/CreateBundle/issues/39 |
0 commit comments