From 4870ac98f68a1d8d2ba04fd5dba841f388a14e89 Mon Sep 17 00:00:00 2001 From: Rickard Falk Date: Thu, 28 Mar 2024 12:06:49 +0100 Subject: [PATCH 1/4] feat: add handlebars language link helper --- README.md | 74 +++++++++++++++ .../__snapshots__/languageLink.test.js.snap | 35 +++++++ lib/handlebars/helpers/languageLink.js | 80 ++++++++++++++++ lib/handlebars/helpers/languageLink.test.js | 91 +++++++++++++++++++ 4 files changed, 280 insertions(+) create mode 100644 lib/handlebars/helpers/__snapshots__/languageLink.test.js.snap create mode 100644 lib/handlebars/helpers/languageLink.js create mode 100644 lib/handlebars/helpers/languageLink.test.js diff --git a/README.md b/README.md index 2b0047d..5e686dc 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,80 @@ 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. + +- `label_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_ +- `label_button_close`: The label for the close button in the dialog element. Only used if there’s a dialog. +- `label_not_translated`: The label for the dialog element's text. Only used if there’s a dialog. + +```javascript +// Example in i18n/messages.se.js + +label_lang_en: 'English', +label_not_translated: 'Den här sidan är ej översatt', +label_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 + + +``` + +### Use handlebars helper in head + +Include the handlebars helper in the template. + +```handlebars + + +{{{languageLink lang}}} +``` + +### 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 `label_lang_sv: 'Svenska'` and `label_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: diff --git a/lib/handlebars/helpers/__snapshots__/languageLink.test.js.snap b/lib/handlebars/helpers/__snapshots__/languageLink.test.js.snap new file mode 100644 index 0000000..cf935e2 --- /dev/null +++ b/lib/handlebars/helpers/__snapshots__/languageLink.test.js.snap @@ -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`] = ` +"Svenska + +
+ + Den här sidan är inte översatt + Startsida på svenska +
+
" +`; + +exports[`languageLink should return dialog with custom link and default to opposite language label (sv -> en) 1`] = ` +"English + +
+ + This page is not translated + Start page in English +
+
" +`; + +exports[`languageLink should return link with query parameter and anchorMessageKey in opposite language (en -> sv) 1`] = `"Custom"`; + +exports[`languageLink should return link with query parameter and anchorMessageKey in opposite language (sv -> en) 1`] = `"Anpassad"`; + +exports[`languageLink should return link with query parameter and default to opposite language label (en -> sv) 1`] = `"Svenska"`; + +exports[`languageLink should return link with query parameter and default to opposite language label (sv -> en) 1`] = `"English"`; diff --git a/lib/handlebars/helpers/languageLink.js b/lib/handlebars/helpers/languageLink.js new file mode 100644 index 0000000..2b86b1b --- /dev/null +++ b/lib/handlebars/helpers/languageLink.js @@ -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(`label_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 = `${label}` + return output +} + +function dialogElement(lang, link, dialogMessageKey) { + const output = ` + +
+ + ${i18n.message('label_not_translated', lang)} + ${i18n.message(dialogMessageKey, lang)} +
+
` + return output +} + +/** + * Generates a language link and an optional dialog element for language selection. + * + * Used i18n keys: + * - `label_lang_[sv/en]` - Default label for the anchor element's text, if a custom one isn’t provided. + * - `label_button_close` - The label for the close button in the dialog element. + * - `label_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 +} diff --git a/lib/handlebars/helpers/languageLink.test.js b/lib/handlebars/helpers/languageLink.test.js new file mode 100644 index 0000000..e923999 --- /dev/null +++ b/lib/handlebars/helpers/languageLink.test.js @@ -0,0 +1,91 @@ +const handlebars = require('handlebars') + +const mockTranslations = { + label_lang_en_sv: 'English', + label_lang_en_en: 'Engelska', + label_lang_sv_sv: 'Swedish', + label_lang_sv_en: 'Svenska', + label_custom_sv: 'Anpassad', + label_custom_en: 'Custom', + label_locale_select_link_title_sv: 'Show translation', + label_locale_select_link_title_en: 'Visa översättning', + label_button_close_sv: 'Stäng', + label_button_close_en: 'Close', + label_not_translated_en: 'Den här sidan är inte översatt', + label_not_translated_sv: 'This page is not translated', + label_start_page_sv: 'Start page in English', + label_start_page_en: 'Startsida på svenska', +} + +jest.mock('kth-node-i18n', () => ({ + message: (key, lang) => mockTranslations[`${key}_${lang}`], +})) + +const { registerLanguageLinkHelper, languageLink } = require('./languageLink') + +jest.mock('handlebars') +jest.mock() + +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() + }) +}) From f3c9f28a0f9d53c0814d3312f8c746d70a0a51e0 Mon Sep 17 00:00:00 2001 From: Rickard Falk Date: Tue, 2 Apr 2024 15:35:16 +0200 Subject: [PATCH 2/4] test: cleanup and improve languageLink tests --- lib/handlebars/helpers/languageLink.test.js | 33 +++++++++++---------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/lib/handlebars/helpers/languageLink.test.js b/lib/handlebars/helpers/languageLink.test.js index e923999..aec8c57 100644 --- a/lib/handlebars/helpers/languageLink.test.js +++ b/lib/handlebars/helpers/languageLink.test.js @@ -1,30 +1,31 @@ const handlebars = require('handlebars') const mockTranslations = { - label_lang_en_sv: 'English', - label_lang_en_en: 'Engelska', - label_lang_sv_sv: 'Swedish', - label_lang_sv_en: 'Svenska', - label_custom_sv: 'Anpassad', - label_custom_en: 'Custom', - label_locale_select_link_title_sv: 'Show translation', - label_locale_select_link_title_en: 'Visa översättning', - label_button_close_sv: 'Stäng', - label_button_close_en: 'Close', - label_not_translated_en: 'Den här sidan är inte översatt', - label_not_translated_sv: 'This page is not translated', - label_start_page_sv: 'Start page in English', - label_start_page_en: 'Startsida på svenska', + en: { + label_lang_sv: 'Svenska', + label_custom: 'Custom', + label_locale_select_link_title: 'Visa översättning', + label_button_close: 'Close', + label_not_translated: 'Den här sidan är inte översatt', + label_start_page: 'Startsida på svenska', + }, + sv: { + label_lang_en: 'English', + label_custom: 'Anpassad', + label_locale_select_link_title: 'Show translation', + label_button_close: 'Stäng', + label_not_translated: 'This page is not translated', + label_start_page: 'Start page in English', + }, } jest.mock('kth-node-i18n', () => ({ - message: (key, lang) => mockTranslations[`${key}_${lang}`], + message: (key, lang) => mockTranslations[lang][key], })) const { registerLanguageLinkHelper, languageLink } = require('./languageLink') jest.mock('handlebars') -jest.mock() describe('registerLanguageLinkHelper', () => { it('registers language link helper', () => { From 5d34795c9b4923e50fb1c191276a3dfa7c27523c Mon Sep 17 00:00:00 2001 From: Rickard Falk Date: Tue, 2 Apr 2024 15:54:55 +0200 Subject: [PATCH 3/4] docs: updated instructions for languageLink helper --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index 5e686dc..a48f96c 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,21 @@ Include the handlebars helper in the template. {{{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 + + +{{{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: From 2051653bb32a531f397f3d31a24aa27abe0e57cf Mon Sep 17 00:00:00 2001 From: Rickard Falk Date: Wed, 3 Apr 2024 08:43:37 +0200 Subject: [PATCH 4/4] fix: rename language keys --- README.md | 14 +++++++------- lib/handlebars/helpers/languageLink.js | 12 ++++++------ lib/handlebars/helpers/languageLink.test.js | 12 ++++++------ 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index a48f96c..ae68e5c 100644 --- a/README.md +++ b/README.md @@ -60,16 +60,16 @@ registerLanguageLinkHelper() All language constants are optional. -- `label_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_ -- `label_button_close`: The label for the close button in the dialog element. Only used if there’s a dialog. -- `label_not_translated`: The label for the dialog element's text. Only used if there’s a dialog. +- `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 -label_lang_en: 'English', -label_not_translated: 'Den här sidan är ej översatt', -label_button_close: 'Stäng', +language_link_lang_en: 'English', +language_link_not_translated: 'Den här sidan är ej översatt', +language_link_button_close: 'Stäng', ``` ### Styling @@ -126,7 +126,7 @@ The most common use case is probably that a translated page can be reached by si 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 `label_lang_sv: 'Svenska'` and `label_lang_en: 'English'` to `messages.en.js` and `messages.sv.js` respectively +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. diff --git a/lib/handlebars/helpers/languageLink.js b/lib/handlebars/helpers/languageLink.js index 2b86b1b..29edd2b 100644 --- a/lib/handlebars/helpers/languageLink.js +++ b/lib/handlebars/helpers/languageLink.js @@ -13,7 +13,7 @@ function resolveTranslationLanguage(lang) { function resolveDefaultLabel(lang) { const translationLang = resolveTranslationLanguage(lang) - return i18n.message(`label_lang_${translationLang}`, lang) + return i18n.message(`language_link_lang_${translationLang}`, lang) } function anchorElement(lang, anchorMessageKey, link) { @@ -29,9 +29,9 @@ function dialogElement(lang, link, dialogMessageKey) {
- ${i18n.message('label_not_translated', lang)} + ${i18n.message('language_link_not_translated', lang)} ${i18n.message(dialogMessageKey, lang)}
` @@ -42,9 +42,9 @@ function dialogElement(lang, link, dialogMessageKey) { * Generates a language link and an optional dialog element for language selection. * * Used i18n keys: - * - `label_lang_[sv/en]` - Default label for the anchor element's text, if a custom one isn’t provided. - * - `label_button_close` - The label for the close button in the dialog element. - * - `label_not_translated` - The label for the dialog element's text. + * - `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. diff --git a/lib/handlebars/helpers/languageLink.test.js b/lib/handlebars/helpers/languageLink.test.js index aec8c57..99aa053 100644 --- a/lib/handlebars/helpers/languageLink.test.js +++ b/lib/handlebars/helpers/languageLink.test.js @@ -2,19 +2,19 @@ const handlebars = require('handlebars') const mockTranslations = { en: { - label_lang_sv: 'Svenska', + language_link_lang_sv: 'Svenska', label_custom: 'Custom', label_locale_select_link_title: 'Visa översättning', - label_button_close: 'Close', - label_not_translated: 'Den här sidan är inte översatt', + language_link_button_close: 'Close', + language_link_not_translated: 'Den här sidan är inte översatt', label_start_page: 'Startsida på svenska', }, sv: { - label_lang_en: 'English', + language_link_lang_en: 'English', label_custom: 'Anpassad', label_locale_select_link_title: 'Show translation', - label_button_close: 'Stäng', - label_not_translated: 'This page is not translated', + language_link_button_close: 'Stäng', + language_link_not_translated: 'This page is not translated', label_start_page: 'Start page in English', }, }