Skip to content

Commit

Permalink
Merge pull request #56 from KTH/feat/language-link-helper
Browse files Browse the repository at this point in the history
Feat: add handlebars language link helper
  • Loading branch information
falric authored Apr 3, 2024
2 parents f0e614c + 2051653 commit e072b77
Show file tree
Hide file tree
Showing 4 changed files with 296 additions and 0 deletions.
89 changes: 89 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,95 @@ registerBreadcrumbHelper()
res.render(breadcrumbsPath: [{url: 'https://kth.se', label: 'KTH'}, ...], ...)
```

## Language Link Helper

Handlebars helper to generate a language link in the header. If language can’t be toggled with query parameter `l`, then there is an option to display a dialog with a custom link.

### Register helper

```javascript
// Typically server/views/helpers/index.js

const { registerLanguageLinkHelper } = require('@kth/kth-node-web-common/lib/handlebars/helpers/languageLink')

registerLanguageLinkHelper()
```

### Add language constants

All language constants are optional.

- `language_link_lang_[en/sv]`: Default label for the anchor element's text, if a custom one isn’t provided. Remember that it should be displayed in the opposite language, e.g. if the page is in English the label should be _Svenska_
- `language_link_button_close`: The label for the close button in the dialog element. Only used if there’s a dialog.
- `language_link_not_translated`: The label for the dialog element's text. Only used if there’s a dialog.

```javascript
// Example in i18n/messages.se.js

language_link_lang_en: 'English',
language_link_not_translated: 'Den här sidan är ej översatt',
language_link_button_close: 'Stäng',
```

### Styling

Make sure to include styling from KTH Style.

```css
/* Typically application’s main Sass-file */

@use '~@kth/style/scss/components/translation-panel';
```

### Initialize menu panel

Make sure to initialize the menu panel (dialog). _This might be moved to KTH Style._

```javascript
// Typically in server/views/layouts/publicLayout.handlebars

<script type="module">
import {MenuPanel} from '{{ proxyPrefix }}/assets/js/index.js' MenuPanel.initTranslationModal(
document.querySelector(".kth-menu-item.language"), document.querySelector(".kth-translation") )
</script>
```

### Use handlebars helper in head

Include the handlebars helper in the template.

```handlebars
<!-- Typically in server/views/partials/kthHeader.handlebars -->
{{{languageLink lang}}}
```

If no translated page exists, a dialog should be shown on link clink. This can be achieved by passing additional arguments to the helper.

1. The first argument is `lang`, the current language. It is required.
2. The second argument is `anchorMessageKey`, the i18n key for the anchor element's text. Can be omitted (or pass `null`) for default label.
3. The third argument is `link`, the URL to navigate to when the anchor is clicked. If provided, a dialog element is also generated.
4. The fourth argument is `dialogMessageKey`, the i18n key for the dialog element's text. Required if `link` is provided.

```handlebars
<!-- Typically in server/views/partials/kthHeader.handlebars -->
{{{languageLink lang anchorMessageKey link dialogMessageKey}}}
```

Use any variable names, only the argument order matters. Remember that they don’t have to have values. The full signature can be used in the handlebars template, with values only being set in the controller when non-default behavior is needed.

### Common use case

The most common use case is probably that a translated page can be reached by simply adding the query parameter `l`, with a language key like `en`. To achieve this, follow these steps:

1. Include the style from KTH Style, `@use '~@kth/style/scss/components/translation-panel';`
2. Include the handlebars helper in the header partials template, `{{{languageLink lang}}}`
3. Add `language_link_lang_sv: 'Svenska'` and `language_link_lang_en: 'English'` to `messages.en.js` and `messages.sv.js` respectively
4. Verify that `lang` is passed to `render` in the controller.

A link to the opposite language page will now appear in the head.

## Cortina Blocks

Express middleware to fetch Cortina CMS blocks for requests with layouts requiring them:
Expand Down
35 changes: 35 additions & 0 deletions lib/handlebars/helpers/__snapshots__/languageLink.test.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`languageLink should return dialog with custom link and default to opposite language label (en -> sv) 1`] = `
"<a class="kth-menu-item language" hreflang="sv-SE" href="">Svenska</a>
<dialog class="kth-translation">
<div class="kth-translation__content">
<button class="kth-icon-button close">
<span class="kth-visually-hidden">Close</span>
</button>
<span>Den här sidan är inte översatt</span>
<a href="https://kth.se/sv">Startsida på svenska</a>
</div>
</dialog>"
`;

exports[`languageLink should return dialog with custom link and default to opposite language label (sv -> en) 1`] = `
"<a class="kth-menu-item language" hreflang="en-US" href="">English</a>
<dialog class="kth-translation">
<div class="kth-translation__content">
<button class="kth-icon-button close">
<span class="kth-visually-hidden">Stäng</span>
</button>
<span>This page is not translated</span>
<a href="https://kth.se/en">Start page in English</a>
</div>
</dialog>"
`;

exports[`languageLink should return link with query parameter and anchorMessageKey in opposite language (en -> sv) 1`] = `"<a class="kth-menu-item language" hreflang="sv-SE" href="?l=sv">Custom</a>"`;

exports[`languageLink should return link with query parameter and anchorMessageKey in opposite language (sv -> en) 1`] = `"<a class="kth-menu-item language" hreflang="en-US" href="?l=en">Anpassad</a>"`;

exports[`languageLink should return link with query parameter and default to opposite language label (en -> sv) 1`] = `"<a class="kth-menu-item language" hreflang="sv-SE" href="?l=sv">Svenska</a>"`;

exports[`languageLink should return link with query parameter and default to opposite language label (sv -> en) 1`] = `"<a class="kth-menu-item language" hreflang="en-US" href="?l=en">English</a>"`;
80 changes: 80 additions & 0 deletions lib/handlebars/helpers/languageLink.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
const Handlebars = require('handlebars')
const i18n = require('kth-node-i18n')

const VALID_LANGUAGES = ['sv', 'en']
const VALID_LOCALE = { sv: 'sv-SE', en: 'en-US' }

function resolveTranslationLanguage(lang) {
if (!VALID_LANGUAGES.includes(lang)) {
throw new Error(`[languageLink] helper requires first parameter to be a string matching a language, i.e. 'sv'.`)
}
return VALID_LANGUAGES[1 - VALID_LANGUAGES.indexOf(lang)]
}

function resolveDefaultLabel(lang) {
const translationLang = resolveTranslationLanguage(lang)
return i18n.message(`language_link_lang_${translationLang}`, lang)
}

function anchorElement(lang, anchorMessageKey, link) {
const label = typeof anchorMessageKey === 'string' ? i18n.message(anchorMessageKey, lang) : resolveDefaultLabel(lang)

const hreflang = VALID_LOCALE[resolveTranslationLanguage(lang)]
const output = `<a class="kth-menu-item language" hreflang="${hreflang}" href="${link}">${label}</a>`
return output
}

function dialogElement(lang, link, dialogMessageKey) {
const output = `
<dialog class="kth-translation">
<div class="kth-translation__content">
<button class="kth-icon-button close">
<span class="kth-visually-hidden">${i18n.message('language_link_button_close', lang)}</span>
</button>
<span>${i18n.message('language_link_not_translated', lang)}</span>
<a href="${link}">${i18n.message(dialogMessageKey, lang)}</a>
</div>
</dialog>`
return output
}

/**
* Generates a language link and an optional dialog element for language selection.
*
* Used i18n keys:
* - `language_link_lang_[sv/en]` - Default label for the anchor element's text, if a custom one isn’t provided.
* - `language_link_button_close` - The label for the close button in the dialog element.
* - `language_link_not_translated` - The label for the dialog element's text.
*
* @param {string} lang - The current language.
* @param {string} [anchorMessageKey] - The i18n key for the anchor element's text. Can be omitted for default label.
* @param {string} [link] - The URL to navigate to when the anchor is clicked. If provided, a dialog element is also generated.
* @param {string} [dialogMessageKey] - The i18n key for the dialog element's text. Required if `link` is provided.
*
* @returns {string} The generated HTML string containing the language link and optional dialog element.
*
* @throws {Error} If `lang` is not a valid language or if `link` is provided but `dialogMessageKey` is not.
*/
function languageLink(lang, anchorMessageKey, link, dialogMessageKey) {
// Custom link is missing, use a query parameter to change language
if (typeof link !== 'string') {
return anchorElement(lang, anchorMessageKey, `?l=${resolveTranslationLanguage(lang)}`)
}

// Link is provided, but dialog information is incomplete
if (typeof dialogMessageKey !== 'string') {
throw new Error(`[languageLink] helper requires a fourth parameter, if a third is provided.`)
}

// Link is provided, use custom link and dialog
return `${anchorElement(lang, anchorMessageKey, '')}${dialogElement(lang, link, dialogMessageKey)}`
}

function registerLanguageLinkHelper() {
Handlebars.registerHelper('languageLink', languageLink)
}

module.exports = {
registerLanguageLinkHelper,
languageLink, // Exported for testing
}
92 changes: 92 additions & 0 deletions lib/handlebars/helpers/languageLink.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
const handlebars = require('handlebars')

const mockTranslations = {
en: {
language_link_lang_sv: 'Svenska',
label_custom: 'Custom',
label_locale_select_link_title: 'Visa översättning',
language_link_button_close: 'Close',
language_link_not_translated: 'Den här sidan är inte översatt',
label_start_page: 'Startsida på svenska',
},
sv: {
language_link_lang_en: 'English',
label_custom: 'Anpassad',
label_locale_select_link_title: 'Show translation',
language_link_button_close: 'Stäng',
language_link_not_translated: 'This page is not translated',
label_start_page: 'Start page in English',
},
}

jest.mock('kth-node-i18n', () => ({
message: (key, lang) => mockTranslations[lang][key],
}))

const { registerLanguageLinkHelper, languageLink } = require('./languageLink')

jest.mock('handlebars')

describe('registerLanguageLinkHelper', () => {
it('registers language link helper', () => {
registerLanguageLinkHelper()
expect(handlebars.registerHelper).toHaveBeenCalledWith('languageLink', languageLink)
})
})

describe('languageLink', () => {
it('throws an error if lang parameter is missing', () => {
expect(() => languageLink()).toThrow(
new Error(`[languageLink] helper requires first parameter to be a string matching a language, i.e. 'sv'.`)
)
})

it('throws an error if link is provided, but not dialogMessageKey', () => {
const lang = 'en'
expect(() => languageLink(lang, '', 'https://kth.se')).toThrow(
new Error(`[languageLink] helper requires a fourth parameter, if a third is provided.`)
)
})

it('should return link with query parameter and default to opposite language label (en -> sv)', () => {
const lang = 'en'
const result = languageLink(lang)
expect(result).toMatchSnapshot()
})

it('should return link with query parameter and default to opposite language label (sv -> en)', () => {
const lang = 'sv'
const result = languageLink(lang)
expect(result).toMatchSnapshot()
})

it('should return link with query parameter and anchorMessageKey in opposite language (en -> sv)', () => {
const lang = 'en'
const anchorMessageKey = 'label_custom'
const result = languageLink(lang, anchorMessageKey)
expect(result).toMatchSnapshot()
})

it('should return link with query parameter and anchorMessageKey in opposite language (sv -> en)', () => {
const lang = 'sv'
const anchorMessageKey = 'label_custom'
const result = languageLink(lang, anchorMessageKey)
expect(result).toMatchSnapshot()
})

it('should return dialog with custom link and default to opposite language label (en -> sv)', () => {
const lang = 'en'
const link = 'https://kth.se/sv'
const dialogMessageKey = 'label_start_page'
const result = languageLink(lang, null, link, dialogMessageKey)
expect(result).toMatchSnapshot()
})

it('should return dialog with custom link and default to opposite language label (sv -> en)', () => {
const lang = 'sv'
const link = 'https://kth.se/en'
const dialogMessageKey = 'label_start_page'
const result = languageLink(lang, null, link, dialogMessageKey)
expect(result).toMatchSnapshot()
})
})

0 comments on commit e072b77

Please sign in to comment.