Admin plugin page blueprint, field type “pages”: slug (value) ignores translation

Hi!

In my theme I have a blueprint for a custom page template (let’s call it custom.html.twig) that includes an additional field with a page selection. In the returned <select> list the option labels show the translated page titles, but the value attributes – which will be stored in the page’s frontmatter – always contain the slug for the default language.

Is there a way to show the translated slugs?

Example: an option in the select list of a page in the secondary language German looks something like this (ignoring the fancy select lists here)

<option value="/projects">Projekte</option>

But it should look like this:

<option value="/de/projekte">Projekte</option>

In the blueprint (mytheme/blueprints/custom.yaml) I’ve tried both type: pages as well as type: select with data-options@. The result is the same in both cases:

fields:
  internalLinkVariant1:
    type: pages
    size: large
    label: LINK_INTERNAL.LABEL
    show_modular: false
    show_all: false
    options:
      'none': LINK_INTERNAL.NONE
  internalLinkVariant2:
    type: select
    label: LINK_INTERNAL.LABEL
    data-default@: '\Grav\Plugin\Admin::route'
    data-options@: '\Grav\Common\Page\Pages::parentsRawRoutes'
    options:
      'none': LINK_INTERNAL.NONE

The page markdown (custom.de.md) then contains the following in its frontmatter:

internalLinkVariant1: /projects

hi, @clivebeckett, you can set the var in the page template
(as i understand you define var in the theme blueprints.yaml file, if so)
an example for a pages form field could be
{% set internalLinkVariant1 = page.find(theme_var('internalLinkVariant1')) %}
and then do what you need, like this
<a href="{{ internalLinkVariant1.url|e }}">{{ internalLinkVariant1.title|e }}</a>

Hi @b.da

Thank you! But this is not about the theme blueprints.yaml, but about the blueprint for a page template as stored in mytheme/blueprints/. I tried to clarify that in an edit for the post, sorry if it was a bit unclear before.

  1. so use header_var('internalLinkVariant1'), fallback to theme_var, or page.header.internalLinkVariant1 instead. Just test it )

{% set internalLinkVariant1 = page.find(header_var('internalLinkVariant1')) %}
or
{% set internalLinkVariant1 = page.find(page.header.internalLinkVariant1)) %}

  1. the declaration for custom var in page blueprint must start with header. that why i thought it is theme blueprint
fields:
  header.internalLinkVariant1:
    type: pages
    size: large
    label: LINK_INTERNAL.LABEL
    show_modular: false
    show_all: false
    options:
      'none': LINK_INTERNAL.NONE

Thank you! If I’m not overlooking anything, this is still not what I’m searching for. I had tried to strip everything down wanting to avoid unnecessary information. Apparently I did not manage to explain properly with this :slight_smile: – Let’s have another go:

custom.yaml

  • still only a small section
  • my internal link is part of a field type:list which is why I forgot to add “header” after I dropped the original dot
  • the alternative implementation of the page selector is commented out – the result in the form is the same
fields:
  header.call2ActionLinks:
    name: call2action
    type: list
    label: CALL2ACTION.HEADER
    style: vertical
    fields:
      .label:
        type: text
        label: CALL2ACTION.LABEL
      .internal:
        type: pages
        size: large
        label: CALL2ACTION.INTERNAL.LABEL
        show_modular: false
        show_all: false
        options:
          'none': CALL2ACTION.INTERNAL.NONE
      # .internal:
      #   type: select
      #   label: CALL2ACTION.INTERNAL.LABEL
      #   # show_root: false
      #   # show_modular: false
      #   data-default@: '\Grav\Plugin\Admin::route'
      #   data-options@: '\Grav\Common\Page\Pages::parentsRawRoutes'
      #   options:
      #     'none': CALL2ACTION.INTERNAL.NONE
      .external:
        type: text
        label: CALL2ACTION.EXTERNAL.LABEL

The problem is not in the Twig template (see below) – although it’s possible that it can be solved in the template. The problem is, that the form as defined in custom.yaml shows translated page titles – but not the translated slugs in the value attribute of the select list. So if I am in the form of a German page and select the page “Projekte” which has the slug “/de/projekte”, the form shows “/projects” as value instead – and this is what will be saved in the frontmatter/header section of the page’s markdown.

Desired admin form output (excerpt)

<option value="/de/projekte">Projekte</option>

Actual admin form output (excerpt)

<option value="/projects">Projekte</option>

Page’s markdown – frontmatter excerpt

call2ActionLinks:
    -
        label: 'Projekte'
        internal: /projects
        external: null

custom.html.twig
I do have access to the header data and it works – but on the German website it links to the English (default) page – because of the above mentioned default slug in the form.

{% if page.header.call2ActionLinks is not empty %}
    <ul class="c2a">
        {% for c2a in page.header.call2ActionLinks %}
            {% if c2a.internal != 'none' %}
                <li><a href="{{ c2a.internal }}">{{ c2a.label }}</a></li>
            {% elseif c2a.external %}
                <li><a href="{{ c2a.external }}" target="_blank">{{ c2a.label }}</a></li>
            {% endif %}
        {% endfor %}
    </ul>
{% endif %}

@clivebeckett, Not sure if this fits your needs, but you could play with the following:

  • Using fresh Grav installation
  • system.yaml
    languages:
      supported: [en, de]
      include_default_lang: false
    
  • Pages:
    user/pages
    ├── 01.home 
    │   └── default.md
    ├── 02.typography
    │   ├── default.de.md
    │   └── default.en.md
    └── images
    
  • Page header 02.typography/default.de.md
    ---
    title: TypographyDE
    slug: slugDE
    ---
    
  • Create plugin MyPlugin
  • myplugin.php
    public static function getTranslatedSlugs(): array
    {
      /** @var Grav */
      $grav = Grav::instance();
    
      /** @var Admin */
      $admin = $grav['admin'];
      $admin->enablePages();
    
      /** @var Pages */
      $pages = $grav['pages'];
    
      /** @var Language */
      $language = $grav['language'];
      $activeLanguage = $language->getActive();
      $isDefaultLanguage = $language->getDefault() === $activeLanguage;
      $showDefaultLanguageInUrl = $grav['config']->get('system.languages.include_default_lang', false);
    
      $pageList = $pages->getList(null, 0, true, false, true, true, true, false);
    
      $slugs = [];
    
      foreach ($pageList as $key => $value) {
        $page = $pages->find($key);
    
        $slug = $page->route();
    
        if (!$isDefaultLanguage || ($isDefaultLanguage && $showDefaultLanguageInUrl)) {
          $slug = "/{$activeLanguage}{$slug}";
        }
    
        $slugs[$slug] = $page->title();
      }
    
      return $slugs;
    }  
    
  • blueprints/default.yaml
    header.internalLinkVariant2:
      type: select
      label: LINK_INTERNAL.LABEL
      data-options@: '\Grav\Plugin\MyPluginPlugin::getTranslatedSlugs'
    
  • Resulting array when editing page of the non-default German language:
    /de/home: "Home"
    /de/slugDE: "TypographyDE"

Thank you @anon76427325!

This works – with some changes. I wanted the default level marker (—-—-▸ Page Title) back in the list which was missing. Here’s my updated version for documentation. For easier readability it contains the original lines of code in comments:

public static function getTranslatedSlugs(): array
{
    /** @var Grav */
    $grav = Grav::instance();

    /** @var Admin */
    $admin = $grav['admin'];
    $admin->enablePages();

    /** @var Pages */
    $pages = $grav['pages'];

    /** @var Language */
    $language = $grav['language'];
    $activeLanguage = $language->getActive();
    $isDefaultLanguage = $language->getDefault() === $activeLanguage;
    $showDefaultLanguageInUrl = $grav['config']->get('system.languages.include_default_lang', false);

    // making sure $pages->getList() returns the level marker and hides modules
    // $pageList = $pages->getList(null, 0, true, false, true, true, true, false);
    $pageList = $pages->getList(null, 0, true, false, false, false, false, false);

    $translatedPages = [];

    foreach ($pageList as $key => $value) {
        $page = $pages->find($key);

        // this replacement is just a matter of personal taste
        // $slug = $page->route();
        // if (!$isDefaultLanguage || ($isDefaultLanguage && $showDefaultLanguageInUrl)) {
        //     $slug = "/{$activeLanguage}{$slug}";
        // }

        $slug = (!$isDefaultLanguage || ($isDefaultLanguage && $showDefaultLanguageInUrl))
            ? '/' . $activeLanguage . $page->route()
            : $page->route();

        // $value of $pageList now contains the level marker
        // $translatedPages[$slug] = $page->title();
        $translatedPages[$slug] = $value;
    }

    return $translatedPages;
}

Also make sure to include

use Grav\Common\Grav;

in your Plugin Class definition. This was not in the plugin skeleton created by devtools and it took me a while to figure out how to include it.