What's the best way to put in a link to a translated node in a D8 template?
The easiest method of providing language switching links would be using the language manager's getLanguageSwitchLinks() method. However, this might - depending on your language settings - also provide fallback links, if the node is not translated to the target language. An abstract example:

use Drupal\Core\Language\LanguageInterface;

function THEME_preprocess_node(&amp;$variables) {
  $node = $variables['node'];

  $language_manager = \Drupal::languageManager();
  $current_language = $language_manager-&gt;getCurrentLanguage()-&gt;getId();
  $links = $language_manager-&gt;getLanguageSwitchLinks(LanguageInterface::TYPE_CONTENT, $node-&gt;toUrl());

  $variables['current_language'] = $current_language;
  $variables['translations'] = [
    '#theme' =&gt; 'links',
    '#links' =&gt; $links,
    '#set_active_class' =&gt; TRUE,
  ];
}


For your use case - to show links to translated content only -, your approach to using getTranslationLanguages() of the TranslatableInterface is on the right track.

But be aware, that it will return entity translation instances also for non-published - or otherwise limited - content translations, to which the current user may not have view access to. Hence, he will get an "Access denied" error after clicking the links.

Additionally, the code should be generic enough to allow for further languages being added using the admin UI in a later stage of the application's life-cycle, without having to touch your preprocessing/templates anymore.

Therefore some suggested improvements for your solution:


Check, whether the current user has access to the translation.
Avoid using hard-coded language identifiers. As I suppose you did so to not add further links for the current language, you should use your $current_language variable to do the checks.
Avoid using hard-coded language link labels. Consider using \Drupal\Core\Language::getName() instead.
Use a list of language links instead of a dedicated variable for each language, and pass that list to the template. This way you can just iterate over the links.
Don't return markup (the links) from within your preprocess hook, but allow creating the links within the templates.


This sums up to something like this:

use Drupal\Core\Template\Attribute;

function THEME_preprocess_node(&amp;$variables) {
  $node = $variables['node'];

  $language_manager = \Drupal::languageManager();
  $current_language = $language_manager-&gt;getCurrentLanguage()-&gt;getId();

  $links = [];
  foreach ($node-&gt;getTranslationLanguages() as $langcode =&gt; $language) {
    // Skip any translation that cannot be viewed.
    $translation = $node-&gt;getTranslation($langcode);
    if (!$translation-&gt;access('view', NULL, TRUE)-&gt;isAllowed()) {
      continue;
    }
    // Get URL of the translation.
    $url = $node-&gt;toUrl('canonical')
      -&gt;setOption('language', $language)
      -&gt;setAbsolute()
      -&gt;toString();
    // Optionally provide an 'active' class.
    $classes = [];
    if ($langcode == $current_language) {
      $classes[] = 'active';
    }
    // Construct information for a link.
    $link = [
      'language' =&gt; $langcode,
      'is_current' =&gt; $langcode == $current_language,
      'url' =&gt; $url,
      'title' =&gt; $language-&gt;getName(),
      'attributes' =&gt; new Attribute(['class' =&gt; $classes]),
    ];
    $links[] = $link;
  }

  $variables['current_language'] = $current_language;
  $variables['translations'] = $links;
}


In your template, you can then create the links like this:

{% if translations %}
  {% for item in translations %}
    {# Optionally skip current language #}
    {% if not item.is_current %}
      &lt;a href="{{ item.url }}"{{ item.attributes }}&gt;{{ item.title }}&lt;/a&gt;
    {% endif %}
  {% endfor %}
{% endif %}