Does anybody successfully implemented a language switch run Craft 3? Trying the Craft 2 variant slightly adjusted but still running into issues: Invalid site handle: nl

{% set otherLocales = craft.app.i18n.siteLocaleIds|without(craft.locale) %}
{% for locale in otherLocales %}
   {% set localeEntry = craft.entries.id(entry.id).locale(locale).one() %}
    {#  set localeCategory = craft.categories.group('worksCat').locale(locale).first #}
    {% if localeEntry.locale is defined and localeEntry.locale == locale %}
        <a href="{{ localeEntry.getUrl() }}">
            <img src="/{{ craft.app.i18n.getLocaleById(locale) }}.svg"  {{ craft.app.i18n.getLocaleById(locale).nativeName }}
    {% else %}
        <a href="{{ localeCategory.getUrl() }}">
            <img src="/{{ craft.app.i18n.getLocaleById(locale) }}.svg" > {{ craft.app.i18n.getLocaleById(locale).nativeName }}
    {% endif %}
{% endfor %}

5 Answers 5


The concept of localization has changed in Craft 3, there are not only locales anymore - now you have different sites and site groups.

Example without multisite - only different locals

You can get all available sites for an entry with

{% set supportedSites = entry.getSupportedSites %}

This will return you an array of all supported sites for the entry.

{% for siteForEntry in entry.getSupportedSites %}
    {# get the site #}
    {% set site = craft.app.getSites.getSiteById(siteForEntry.siteId) %}
    {# check if it's the current site #}
    {% if site.id == entry.siteId %}
        active site = {{ site.id }} {{ site.language }} 
    {% else %}
        {# if it's not the current site, get the entry for this site #}
        {% set entryForOtherSite = craft.entries.id(entry.id).site(site).one() %}
        link to site with id {{ entryForOtherSite.siteId }} <a href="{{ entryForOtherSite.getUrl() }}">{{ site.language }} - {{ entryForOtherSite.title }}</a>
    {% endif %}
{% endfor %}

When you take a look at your /admin/utilities/deprecation-errors you'll see many records because all your locale functions will do this

public function locale(string $value)
    Craft::$app->getDeprecator()->log('ElementQuery::locale()', 'The “locale” element query param has been deprecated. Use “site” or “siteId” instead.');

    return $this;

They tell you: "This function is deprecated, use "site" instead"

Example with multisite - different sites and locales

If you are going to have multiple different sites with multiple locales you can manage each site in one group and insert all the locales for these sites in each group.

- Site group A
    - language A (siteId 1)
    - Language B (siteId 2)
- Site group B
    - Language A (siteId 3)
    - Language C (siteId 4)

For Example if the user is in site group A you want to show only language A and language B but not Language C even if the entry exists in all sites. So you can grab the group of the current site and loop through all sites of the group.

{% set currentSite = craft.app.sites.currentSite %}
{% for siteInGroup in currentSite.getGroup.getSites() %}
    {% if siteInGroup.id == entry.siteId %}
        active site = {{ siteInGroup.id }}{{ siteInGroup.language }}
    {% else %}
        {% set entryForOtherSite = craft.entries.id(entry.id).site(siteInGroup).one() %}
        link to site {{ entryForOtherSite.siteId }} <a href="{{ entryForOtherSite.getUrl() }}">{{ siteInGroup.language }} - {{ entryForOtherSite.title }}</a>
    {% endif %}
{% endfor %}
  • I really like this answer, however often you don't have an entry available in the templates. So you should check for entry, category and maybe fallback to the homepage?
    – Jan_dh
    Commented May 14, 2018 at 13:05
  • 1
    It's great that you have to come to StackExchange to figure out how to use a core feature.
    – joep
    Commented Feb 1, 2019 at 16:29

Little addition with a fallback to the homepage and check if entry exist. currentSite is a global variable already defined in craft3 https://docs.craftcms.com/v3/templating/global-variables.html#currentsite

  {% for site in currentSite.group.sites %}
    <li class="{% if currentSite.id == site.id %} is-active{% endif %}">
      {% set url = alias(site.baseUrl) %}
      {% if entry is defined %}
        {% set entryForOtherSite = craft.entries.id(entry.id).site(site).one() %}
        {% if entryForOtherSite %}
          {% set url = entryForOtherSite.getUrl() %}
        {% endif %}
      {% endif %}
      <a href="{{ url }}">{{ site.name }}</a>
  {% endfor %}

As of today, Site Switcher is now available in the plugin store! *

It was called "Language Link" in Craft 2, but the premise is the same... It allows you to switch between sites while staying on the same page.

{% set element = (category ?? entry ?? null) %}

    <li><a href="{{ siteSwitcher('english', element) }}">English</a></li>
    <li><a href="{{ siteSwitcher('spanish', element) }}">Español</a></li>
    <li><a href="{{ siteSwitcher('french', element) }}">Français</a></li>
    <li><a href="{{ siteSwitcher('german', element) }}">Deutsch</a></li>


* Disclaimer: I am the author of Site Switcher.


If you're using SEOmatic for Craft CMS 3, there's a built-in function that will make it super easy to do:

seomatic.helper.getLocalizedUrls(URI, SITE_ID) - Return a list of localized URLs for a given URI that are in the SITE_ID site's group. Both URI and SITE_ID are optional, and will use the current request's URI and the current site's SITE_ID if omitted.

c.f.: https://github.com/nystudio107/craft-seomatic#helper-functions-seomatichelper

  • Any chance any of you knows how to output the language's name instead of code? English instead of EN, like we could do with locale.nativeName. Commented May 9, 2018 at 21:13

Here is my language switcher for Craft CMS 3:


It works for Entries, categories and other elements with their own URLs and has an option for an automatic appending country flag to language links.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.