Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

multi language support for content? #513

Closed
DerManoMann opened this issue Aug 6, 2013 · 32 comments
Closed

multi language support for content? #513

DerManoMann opened this issue Aug 6, 2013 · 32 comments

Comments

@DerManoMann
Copy link

I haven't really seen anything about it, so here is my questions: Is there any support for having the same content in multiple languages?

If not, is there any way of simulating that? The minimum viable feature would be to have a unique key that consists of a content-id and a locale.

I am considering bolt as a backend for managing content in an application that needs to be multilingual. So I would need something that can manage the same content in multiple languages.

ta, mano

@bobdenotter
Copy link
Member

This one pops up regularly. I know it's something a lot of people would like, but i know from experience that doing multilingual is hard if you want to do it right. I mean, we could kludge together something quickly, but I know that doing that will only lead to more support and frustration down the road. Either we do it properly, or we don't do it all for now.
See this issue for a lot more thoughts about this: #234

@DerManoMann
Copy link
Author

Thanks a lot. I rather suspected your answer might go that way....

Any chance of getting my hands on the extra code / extension you mentioned in the other issue? That might be enough for me to close this issue ;)

@DerManoMann
Copy link
Author

@ bobdenotter So, any change to you releasing that extension code you mentioned in on #234?

@bobdenotter
Copy link
Member

That code is hopelessly outdated. :-)

How I should do it now, is like this:

  • create contenttypes, like 'pages' and 'paginas' or 'seiten' or whatever localised version of the contenttypes you'd like.
  • The contenttypes themselves can be identical, with regards to the used fields
  • Optionally create a 'relationship' between the contenttypes, so that it's easier to link between them.

Now, in the backend you'll have the different contenttypes and the possibility to link them together. However, using this you will get clunky URLs like example.org/pagina/over-ons and example.org/page/about-us. In bolt 1.2 we have introduced proper routing, so we can change the URLs to nl.example.org/over-ons or example.org/en/about-us.
To make a language-switch, you should add a bit of code in the templates that checks if the page has a relation to a page in one of the other contenttypes. If so, you can link to the matching page directly, or otherwise just link to the frontpage in the other language.
If you use menu.yml, you can just add more menus, one each per language, and use that in the templates.

I know I've promised to write a bit about this. I'd like to get 1.2 out of the door soon, so that I can actually tell people to use the new routing to set up multilingual pages.

@DerManoMann
Copy link
Author

Thanks. sounds good to me. I am happy to wait for 1.2 for now :)

@terwey
Copy link
Contributor

terwey commented Jan 12, 2014

Hi,

I've been playing with Bolt for a bit now but really running into some issues with multi-language content. It could be a bug, it could be the way I'm using it... not sure yet.

Basically if I have a {language} parameter in the route with a {slug} in the same URL it has serious issues. It suddenly throws crazy errors on how it can't generate the correct Route URL.

e.g. the following:

path:               /{language}/{contenttypeslug}/{slug}

Is not possible to use, it basically throws a crazy tantrum.
If I add 'language': 'nl' to the defaults for the Route it works, but it then always uses the Defaults for the language in the URL generation. Kinda defeats the purpose...

It finds the correct page because it of course uses the {slug} but on the arrived page the Language is set wrong and the Menu doesn't make sense anymore.

Any ideas?

Of course things always 'suddenly' work if you start reporting an issue. I will continue with this and see if it actually works now or if I was just lucky for 5 minutes...

@bobdenotter
Copy link
Member

What you've described, should work. That is, as long as all of the pages have a language set. You should add perhaps add a requirement for the languages to the routing:

pages:
  path: /{language}/{contenttypeslug}/{slug}
  defaults: { _controller: 'Bolt\Controllers\Frontend::record', 'contenttypeslug': 'page' }
  requirements:
    language: 'nl|en|de'
  contenttype: pages

Or, when you're developing, and like to throw less errors when developing, allow 'empty' as option:

    language: 'nl|en|de|'

@terwey
Copy link
Contributor

terwey commented Jan 13, 2014

@bobdenotter the
language: 'nl|en|de' requirement is a nice touch, I didn't know I could define the requirement like this. I'll let you know how it turned out.

@terwey
Copy link
Contributor

terwey commented Jan 13, 2014

This is my current routing.yml

homepage:
  path:               /{language}
  defaults:           { _controller: 'Bolt\Controllers\Frontend::homepage', 'language': 'nl' }

search:
  path:               /search
  defaults:           { _controller: 'Bolt\Controllers\Frontend::search' }

preview:
  path:               /preview/{contenttypeslug}
  defaults:           { _controller: 'Bolt\Controllers\Frontend::preview' }
  requirements:
    contenttypeslug:  'Bolt\Controllers\Routing::getAnyContentTypeRequirement'

contentlink:
  path:               /{language}/{contenttypeslug}/{slug}
  defaults:           { _controller: 'Bolt\Controllers\Frontend::record' }
  requirements:
    contenttypeslug:  'Bolt\Controllers\Routing::getAnyContentTypeRequirement'
    language:         'nl|en'

taxonomylink:
  path:               /{taxonomytype}/{slug}
  defaults:           { _controller: 'Bolt\Controllers\Frontend::taxonomy' }
  requirements:
    taxonomytype:     'Bolt\Controllers\Routing::getAnyTaxonomyTypeRequirement'

contentlisting:
  path:               /{language}/{contenttypeslug}
  defaults:           { _controller: 'Bolt\Controllers\Frontend::listing' }
  requirements:
    contenttypeslug:  'Bolt\Controllers\Routing::getPluralContentTypeRequirement'
    language:         'nl|en'

And currently I am unable to reach the Backend. After I managed to login I get:

Twig_Error_Runtime

An exception has been thrown during the rendering of a template ("Parameter "language" for route "contentlink" must match "nl|en" ("" given) to generate a corresponding URL.") in "_listing-base.twig" at line 109.

When I set language: 'nl|en|' the backend loads but then the frontend again can just randomly accept another parameter.
I should probably reduce the scope that the {language} parameter is required in. The reason I need {language} for the / Route is simply because else I can't render the correct page. I could work with global vars but who wants to do that? ;)

@helderco
Copy link

Here's how I solved this.

I take advantage of Symfony's locale in routes: http://symfony.com/doc/current/book/translation.html#book-translation-locale-url

To avoid adding a requirements for this in every route, I added the enforcement in my before, using a list of accepted locales in config.

# config.yml
locale: pt_PT
i18n_locales:
  - en
  - pt
  - es
  - fr
  - de
// My\Controller.php

namespace My;

use Bolt\Application;
use Bolt\Controllers\Frontend as BoltFrontend;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class Controller
{
    /**
     * Enforce locale requirement
     */
    public static function before(Request $request, Application $app)
    {
        BoltFrontend::before($request, $app);
        $locales = $app['config']->get('general/i18n_locales', NULL);

        if ($locales && $request->getLocale()) {
            if (!in_array($request->getLocale(), $locales)) {
                $app->abort(404, $app['translator']->trans('Unknown or unsupported locale.'));
            }
        }
    }

    /**
     * Redirect / to /{default_locale}
     */
    public static function i18nFront(Request $request, Application $app)
    {
        $locales = $app['config']->get('general/i18n_locales', NULL);

        if ($locales) {
            return $app->redirect($app['paths']['root'] . $request->getPreferredLanguage($locales));
        }
    }
}

Notice how in i18nFront(), $request->getPreferredLanguage($locales) sets the locale from the user's browser preference, but for one in my accepted list.

I have this on a BaseController so it works in multilingual sites or not. On non-multilingual sites I just don't set i18n_locales in config.yml so this code isn't run.

Here's my routes:

# routing.yml

homepage:
  path: /
  defaults:
    _controller: 'My\Controller::i18nFront'

# Example for your own pages (notice how simple it is)
somepage:
  path: /{_locale}/somepage
  defaults:
    _controller: 'My\Controller::test'

...

contentlink:
  path: /{_locale}/{contenttypeslug}/{slug}
  defaults:
    _controller: 'Bolt\Controllers\Frontend::record'
    _before: 'My\Controller::before'
  requirements:
    contenttypeslug: 'Bolt\Controllers\Routing::getAnyContentTypeRequirement'

...

i18n_homepage:
  path: /{_locale}
  defaults:
    _controller: 'Bolt\Controllers\Frontend::homepage'
    _before: 'My\Controller::before'

@bobdenotter
Copy link
Member

@helderco Interesting! This looks pretty good! You're still using different contenttypes for different languages, I assume?

@helderco
Copy link

@bobdenotter Oh, forgot about that. No, as of now I'm adding internationalised fields to the same content type, which is easier to implement.

Example:

products:
    name: Products
    slug: products
    singular_name: Product
    singular_slug: product
    fields:
        title_en:
            type: text
            label: Name (English)
            class: large
        title_pt:
            type: text
            label: Name (Portuguese)
            class: large
        slug:
            type: slug
            label: URL
            uses: title_en
        body_en:
            type: html
            label: Description (English)
            height: 150px
        body_pt:
            type: html
            label: Description (Portuguese)
            height: 150px
        image:
            type: image
            label: Image
    record_template: product.twig
    listing_template: products.twig
    listing_records: 200
    sort: title

In Drupal you translate fields now, but behind the scenes it creates translations associations by have a table per field and having a column with the language and another with the translation set id.

So you can create a "node" in a specific language, and then translate to another when you're ready and the association between them, as well as knowing which fields are "synchronised" or not, are handled behind the scenes.

Would be interesting if we could the the following:

products:
    name: Products
    slug: products
    singular_name: Product
    singular_slug: product
    fields:
        title:
            type: text
            label: Name
            i18n: true
            class: large
        slug:
            type: slug
            label: URL
            uses: title_en
        body:
            type: html
            label: Description
            i18n: true
            height: 150px
        image:
            type: image
            label: Image
    record_template: product.twig
    listing_template: products.twig
    listing_records: 200
    sort: title

The available languages for translation could be the i18n_locales config variable. Translating labels (e.g., Name, Description....) and content type name (e.g., Product) would be done through the translator service. Slugs (URLs, e.g. product) could also be translated through the translator service.

@evan70
Copy link

evan70 commented Feb 27, 2014

GreaT!

@odlex
Copy link

odlex commented Feb 28, 2014

Excellent ! I am very interested in it

Maybe \Bolt\Content can extend a new class like \I18NContent to add i18n field support

in contenttype.yml

products:
    class: \I18NContent
    name: Products
    slug: products
    singular_name: Product
    singular_slug: product
    fields:
        title:
            type: text
            label: Name
            i18n: true
            class: large
        slug:
            type: slug
            label: URL
            uses: title_en
        body:
            type: html
            label: Description
            i18n: true
            height: 150px
        image:
            type: image
            label: Image
    record_template: product.twig
    listing_template: products.twig
    listing_records: 200
    sort: title

@klickreflex
Copy link

@helderco This looks cool and I'm very interested in getting it running myself.

How did you manage to only show the correct local field depending on the active language?

Another question regarding your configuration:
I managed to get language prefixes working, but only if I access my site with a prefix.
e.g. bolt.dev/en works correctly, but calling the root bolt.dev the following error is thrown

/Users/Daniel/sites/bolt.dev/vendor/symfony/http-kernel/Symfony/Component/HttpKernel/Event/FilterControllerEvent.php

LogicException
The controller must be a callable (Array(0 => Esperanto\Controller, 1 => i18nFront) given).

 *
 * @api
 */
public function setController($controller)
{
    // controller must be a callable
    if (!is_callable($controller)) {
        throw new \LogicException(sprintf('The controller must be a callable (%s given).', $this->varToString($controller)));
    }

Here's my Controller:

<?php
namespace Esperanto;

use Bolt\Application;
use Bolt\Controllers\Frontend as BoltFrontend;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class Controller
{
    /**
     * Enforce locale requirement
     */
    public static function before(Request $request, Application $app)
    {
        BoltFrontend::before($request, $app);
        $locales = $app['config']->get('general/i18n_locales', NULL);

        if ($locales && $request->getLocale()) {
            if (!in_array($request->getLocale(), $locales)) {
                $app->abort(404, $app['translator']->trans('Unknown or unsupported locale.'));
            }
        }
    }

    /**
     * Redirect / to /{default_locale}
     */
    public static function i18nFront(Request $request, Application $app)
    {
        $locales = $app['config']->get('general/i18n_locales', NULL);

        if ($locales) {
            return $app->redirect($app['paths']['root'] . $request->getPreferredLanguage($locales));
        }
    }
}

And my routing.yml

homepage:
path: /
defaults: { _controller: 'Esperanto\Controller::i18nFront' }

search:
  path:               /search
  defaults:           { _controller: 'Bolt\Controllers\Frontend::search' }

preview:
  path:               /preview/{contenttypeslug}
  defaults:           { _controller: 'Bolt\Controllers\Frontend::preview' }
  requirements:
    contenttypeslug:  'Bolt\Controllers\Routing::getAnyContentTypeRequirement'

contentlink:
  path: /{_locale}/{contenttypeslug}/{slug}
  defaults:
    _controller: 'Bolt\Controllers\Frontend::record'
    _before: 'Esperanto\Controller::before'
  requirements:
    contenttypeslug: 'Bolt\Controllers\Routing::getAnyContentTypeRequirement'

taxonomylink:
  path:               /{_locale}/{taxonomytype}/{slug}
  defaults:           
    _controller: 'Bolt\Controllers\Frontend::taxonomy'
    _before: 'Esperanto\Controller::before'
  requirements:
    taxonomytype:     'Bolt\Controllers\Routing::getAnyTaxonomyTypeRequirement'

contentlisting:
  path:               /{_locale}/{contenttypeslug}
  defaults:           
    _controller: 'Bolt\Controllers\Frontend::listing'
    _before: 'Esperanto\Controller::before'
  requirements:
    contenttypeslug:  'Bolt\Controllers\Routing::getPluralContentTypeRequirement'

# ------------------------------------------------------------------------------
# Place your own routes here, that have a LOWER priority than the default routes.

i18n_homepage:
  path: /{_locale}
  defaults:
    _controller: 'Bolt\Controllers\Frontend::homepage'
    _before: 'Esperanto\Controller::before'

I'd be grateful for any help.

@klickreflex
Copy link

I guess just dropping my own controller into app/src/Bolt/controllers is not enough. Where should this go to?

@helderco
Copy link

helderco commented Apr 7, 2014

@klickreflex You're missing the / route from my example above:

homepage:
  path: /
  defaults:
    _controller: 'My\Controller::i18nFront'

Also, the Controller that I gave, can be extended, so it can be reused between projects and keeping it clean from your project specific controller code.

As for the local field, I concatenate the field name with the locale, something like:

{{ object['field_' ~ app.locale] }}

@helderco
Copy link

helderco commented Apr 7, 2014

I took another look and I see you added the / route, but it was out of the code block so I didn't notice.

@helderco
Copy link

helderco commented Apr 7, 2014

Is your Esperanto namespace loading correctly?

I mean, is it in your autoloader?

Here's my composer.json:

{
    "require": {
        "bolt/bolt": "dev-master"
    },
    "minimum-stability": "dev",
    "prefer-stable": true,
    "scripts": {
        "post-install-cmd": [
            "Bolt\\Composer\\ScriptHandler::installAssets"
        ],
        "post-update-cmd": [
            "Bolt\\Composer\\ScriptHandler::installAssets"
        ]
    },
    "extra": {
        "bolt-dir-mode": "0755"
    },
    "autoload": {
        "psr-4": {
            "Morfose\\": "app/src"
        }
    }
}

Here, Morfose = My\Controller in my example above. So I have my classes in app/src.

@helderco
Copy link

helderco commented Apr 7, 2014

Also, do you have the i18n_locales setting in config.yml? Can you show?

@klickreflex
Copy link

Hi @helderco and thanks a lot for your help.

No, I guess my Esperanto namespace is not loading correctly. Which composer.json should it be added to? Sorry, this is a bit of new terrain for me.

And yes, I've added i18n_locales to config.yml:

locale: de_DE

i18n_locales:
  - de
  - en
  - ru

Kind regards,
Daniel

@helderco
Copy link

helderco commented Apr 7, 2014

It's your project's root composer. I assume you're using composer and have not downloaded Bolt with the full vendors included.

I have Bolt in the vendor dir, so if you're using Bolt the default way (download from the site or git clone), then you're seeing Bolt's composer.json which will be different then mine.

In the latter case, you'll have something like the following at the end:

    "autoload": {
        "psr-0": {
            "Bolt": "app/src/",
            "Bolt\\Tests": "app/tests"
        }
    }

Just add yours there. If you have your Esperanto class in Bolt's app/src then your namespace should really be Bolt\Esperanto, in which case you don't need to do anything to composer.json.

@klickreflex
Copy link

Okay, I did not install via composer but just downloaded the archive.

Now I'm all confused about the directory structure. I can find tens of composer.json files, but none of them themes like Bolt's composer.json.

Also, I do have Bolt in the vendor directory, but inside that there's only a sub directory called Dumper. I did put Esperanto.php inside /app/src/Bolt/Controllers. So if I move it up to /app/src/it should be auto loaded? (Update: no, I moved it up and nothing changed)

Here's a screenshot of my dir structure:

bolt-structure

@helderco
Copy link

helderco commented Apr 7, 2014

Bolt itself, if installed in vendor would be in bolt/bolt. Dumper is another project from the Bolt team.

So, in your case, if you want to keep Esperanto.php in app/src/Bolt/Controllers, then change your namespace to: Bolt\Controllers.

That is, at the top of the Esperanto.php file and anywhere you call Esperanto, such as in your routing.yml:

homepage:
    path: /
    defaults: { _controller: 'Bolt\Controllers\Esperanto::i18nFront' }

@klickreflex
Copy link

I'm feeling like I have to ask stupid question, sorry.

I changed the namespace as suggested in both Esperanto.php as well as in my routing and now I get:

Fatal error: Cannot redeclare class Bolt\Controllers\Esperanto\Controller in /Users/Daniel/sites/bolt.dev/app/src/Bolt/Controllers/Esperanto.php on line 13

@helderco
Copy link

helderco commented Apr 7, 2014

Show the code.

@klickreflex
Copy link

app/src/Bolt/Controllers/Esperanto.php
https://gist.github.com/klickreflex/10021185

/app/config/routing.yml
https://gist.github.com/klickreflex/ec9e57bbeccc51b69415

/app/config/config.yml
https://gist.github.com/klickreflex/10021165

@helderco
Copy link

helderco commented Apr 7, 2014

The namespace should represent the folder it's at (i.e., namespace Bolt\Controllers;), and the class name should be the same as the file (i.e. class Esperanto).

@klickreflex
Copy link

That solved it, thanks a lot!

@lexislav
Copy link
Contributor

lexislav commented Oct 6, 2014

Works for me too, thanks.
Is there any soultions how to localize content slug?

@lexislav
Copy link
Contributor

lexislav commented Oct 9, 2014

I have one more question about this:
I have site in three languages cs/en/it, main-configuration set to cs_CZ.
I use some strings in my theme this way: {{ __('czech string') }}
I have translations in messages.en.yml, but they work correctly only when site is switched trought mainconfiguration to en_GB.
When the locale is set trought path from the tutorial above, the bolt doesn look into messages.en.yml for translation :-( It seems to check just contenttypes.en.yml, not into messages.en.yml.
Any idea how to make botl look to messages.en.yml too?
Thanks

@bobdenotter
Copy link
Member

Closing. Use only #234 for discussion on this.

See also this informational page on the docs: https://docs.bolt.cm/howto/building-multilingual-websites

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants