Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.MD
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## [Unreleased]

- TMS-1237: Add a custom Google Translate functionality to navigation

## [1.71.2] - 2026-02-25

- TMS-1255: Check for redipress class before indexing contact field
Expand Down
4 changes: 4 additions & 0 deletions assets/icons/icon-translate.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions assets/icons/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ import './icon-tehtava.svg';
import './icon-teltta.svg';
import './icon-timantti.svg';
import './icon-tori.svg';
import './icon-translate.svg';
import './icon-wifi.svg';
import './line.svg';
import './link.svg';
Expand Down
285 changes: 285 additions & 0 deletions assets/scripts/gtranslate-dropdown.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
/**
* Google Translate Dropdown JS controller.
*/

// Use jQuery as $ within this file scope.
const $ = jQuery;

/**
* Class GtranslateDropdown
*/
export default class GtranslateDropdown {

constructor() {
this.isGoogleLoaded = false;
this.eventsAttached = false;
this.gtranslateCheckRetries = 0;
this.gtranslateCheckMaxRetries = 3;
this.gtranslateCheckDelay = 1500;
this.cookiebotEventsBound = false;
}

/**
* Check if Cookiebot consent allows loading Google Translate
*
* @return {boolean} True when consent permits loading Google Translate.
*/
hasConsentForGtranslate() {
if ( typeof window.Cookiebot === 'undefined' ) {
return true;
}

if ( ! window.Cookiebot.consent ) {
return false;
}

return Boolean( window.Cookiebot.consent.preferences );
}

/**
* Toggle gtranslate text visibility
*
* @param {boolean} isVisible - Show or hide non-cookie paragraphs
* @return {void}
*/
setDropdownVisibility( isVisible ) {
const $dropdownContent = $( '.gtranslate-dropdown__content' );
const $cookieTextContainer = $( '.gtranslate-cookie-text-container' );

if ( ! $dropdownContent.length ) {
return;
}

const paragraphs = $dropdownContent.find( 'p:not(.gtranslate-cookie-text)' );

if ( isVisible ) {
paragraphs.removeClass( 'is-hidden' );
$cookieTextContainer.addClass( 'is-hidden' );
}
else {
paragraphs.addClass( 'is-hidden' );
$cookieTextContainer.removeClass( 'is-hidden' );
}
}

/**
* Attach dropdown event handlers once
*
* @return {void}
*/
attachEventsIfNeeded() {
if ( this.eventsAttached || ! $( '.gtranslate-trigger' ).length ) {
return;
}

// Handle gtranslate dropdown toggle
$( '.gtranslate-trigger' ).on( 'click', this.toggleGtranslateDropdown.bind( this ) );

// Close gtranslate dropdown when clicking outside
$( document ).on( 'click', this.closeGtranslateOnOutsideClick.bind( this ) );

// Handle ESC key
$( document ).on( 'keydown', this.handleEscKey.bind( this ) );

this.eventsAttached = true;
}

/**
* Handle consent-dependent loading and visibility
*
* @return {void}
*/
handleConsentFlow() {
const hasConsent = this.hasConsentForGtranslate();

this.attachEventsIfNeeded();
this.setDropdownVisibility( hasConsent );

if ( ! hasConsent ) {
return;
}

this.loadGoogleTranslateAPI();
}

/**
* Bind Cookiebot consent events once
*
* @return {void}
*/
bindCookiebotEvents() {
if ( this.cookiebotEventsBound ) {
return;
}

window.addEventListener( 'CookiebotOnConsentReady', this.handleConsentFlow.bind( this ) );
window.addEventListener( 'CookiebotOnAccept', this.handleConsentFlow.bind( this ) );
window.addEventListener( 'CookiebotOnDecline', this.handleConsentFlow.bind( this ) );

this.cookiebotEventsBound = true;
}

/**
* Toggle gtranslate dropdown
*
* @param {Event} event - Click event
* @return {void}
*/
toggleGtranslateDropdown( event ) {
event.preventDefault();
const $dropdown = $( '#js-gtranslate-dropdown' );
const $trigger = $( event.currentTarget );

if ( $dropdown.hasClass( 'is-hidden' ) ) {
$dropdown.removeClass( 'is-hidden' );
$trigger.attr( 'aria-expanded', 'true' );
}
else {
$dropdown.addClass( 'is-hidden' );
$trigger.attr( 'aria-expanded', 'false' );
}
}

/**
* Close gtranslate dropdown when clicking outside
*
* @param {Event} event - Click event
* @return {void}
*/
closeGtranslateOnOutsideClick( event ) {
const $dropdown = $( '#js-gtranslate-dropdown' );
const $wrapper = $( '.gtranslate-wrapper' );

if ( ! $dropdown.hasClass( 'is-hidden' )
&& ! $wrapper.is( event.target )
&& $wrapper.has( event.target ).length === 0 ) {
$dropdown.addClass( 'is-hidden' );
$( '.gtranslate-trigger' ).attr( 'aria-expanded', 'false' );
}
}

/**
* Handle ESC key to close dropdown
*
* @param {Event} event - Keydown event
* @return {void}
*/
handleEscKey( event ) {
if ( event.key === 'Escape' || event.keyCode === 27 ) {
const $dropdown = $( '#js-gtranslate-dropdown' );
if ( ! $dropdown.hasClass( 'is-hidden' ) ) {
$dropdown.addClass( 'is-hidden' );
$( '.gtranslate-trigger' ).attr( 'aria-expanded', 'false' ).focus();
}
}
}

/**
* Load Google Translate API
*
* @return {void}
*/
loadGoogleTranslateAPI() {
if ( this.isGoogleLoaded || document.querySelector( 'script[src*="translate.google.com"]' ) ) {
return;
}

const script = document.createElement( 'script' );
script.src = 'https://translate.google.com/translate_a/element.js?cb=googleTranslateElementInit';
script.setAttribute( 'data-cookieconsent', 'preferences' );
script.async = true;
document.head.appendChild( script );
this.isGoogleLoaded = true;

// Define the callback function globally
window.googleTranslateElementInit = this.initGoogleTranslate.bind( this );

// Check if the script loaded successfully after a short delay
setTimeout( () => {
this.checkGoogleTranslateLoaded();
}, this.gtranslateCheckDelay );
Comment on lines +187 to +200
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Google Translate callback (window.googleTranslateElementInit) is assigned after the script tag is appended. Because the API script calls the cb= function as soon as it executes, there’s a race where the callback may be undefined. Define the global callback before appending the script (or set script.onload and initialize from there).

Copilot uses AI. Check for mistakes.
}

/**
* Initialize Google Translate
*
* @return {void}
*/
initGoogleTranslate() {
const container = document.getElementById( 'google_translate_element_custom' );

// eslint-disable-next-line no-undef
if ( ! container || typeof google === 'undefined' || ! google.translate ) {
return;
}

// Get current language from data attribute or default to 'fi'
const currentLang = container.dataset.lang || 'fi';

// eslint-disable-next-line no-undef
new google.translate.TranslateElement( {
pageLanguage: currentLang,
autoDisplay: false,
}, 'google_translate_element_custom' );
}

/**
* Check if Google Translate loaded successfully, show cookie message if not
*
* @return {void}
*/
checkGoogleTranslateLoaded() {
const container = document.getElementById( 'google_translate_element_custom' );

if ( container ) {
// Check if the container has been populated with Google Translate content
const hasGoogleContent = container.children.length > 0;

if ( ! hasGoogleContent && this.gtranslateCheckRetries < this.gtranslateCheckMaxRetries ) {
this.gtranslateCheckRetries += 1;
setTimeout( () => {
this.checkGoogleTranslateLoaded();
}, this.gtranslateCheckDelay );
return;
}

// If no Google Translate content, show cookie disabled message and hide other elements
if ( ! hasGoogleContent ) {
// Show the cookie message container
const cookieTextContainer = document.querySelector( '.gtranslate-cookie-text-container' );
if ( cookieTextContainer ) {
cookieTextContainer.classList.remove( 'is-hidden' );
}

// Hide other paragraph elements in the dropdown
const dropdownContent = document.querySelector( '.gtranslate-dropdown__content' );
if ( dropdownContent ) {
const paragraphs = dropdownContent.querySelectorAll( 'p:not(.gtranslate-cookie-text)' );
paragraphs.forEach( ( p ) => {
p.classList.add( 'is-hidden' );
} );
}
}
else {
const cookieTextContainer = document.querySelector( '.gtranslate-cookie-text-container' );
if ( cookieTextContainer ) {
cookieTextContainer.classList.add( 'is-hidden' );
}
}
}
}

/**
* Run when the document is ready.
*
* @return {void}
*/
docReady() {
if ( ! $( '#google_translate_element_custom' ).length ) {
return;
}

this.bindCookiebotEvents();
this.handleConsentFlow();
}
}
2 changes: 2 additions & 0 deletions assets/scripts/theme.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import GravityFormsPatch from './gravity-forms-patch';
import Countdown from './countdown';
import SearchFilters from './search-filters';
import FocusOnSearch from './focus-on-search';
import GtranslateDropdown from './gtranslate-dropdown';

const globalControllers = {
Common,
Expand Down Expand Up @@ -57,6 +58,7 @@ const globalControllers = {
Countdown,
SearchFilters,
FocusOnSearch,
GtranslateDropdown,
};

const templateControllers = {
Expand Down
50 changes: 50 additions & 0 deletions assets/styles/ui-components/header/_gtranslate-dropdown.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
.gtranslate-wrapper {
.is-open {
display: block !important;
}
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.is-open inside .gtranslate-wrapper is not used by the JS controller (it toggles is-hidden). This looks like dead/mismatched styling and the !important makes it harder to reason about. Either align the JS to toggle an is-open modifier or remove this rule and rely on the existing is-hidden toggling.

Suggested change
.is-open {
display: block !important;
}

Copilot uses AI. Check for mistakes.

.gtranslate-trigger {
border: none;
background-color: transparent;
width: 2.375rem;
height: 2.375rem;
border: 2px solid transparent;

&:hover {
cursor: pointer;
}

&:hover,
&:focus {
border-color: $lang-nav-link-border;
}

@include from($tablet) {
width: 2.2359rem;
height: 2.2359rem;
}
}

.gtranslate-dropdown {
background-color: $color-vaalean-harmaa;
right: 0;
z-index: 1;

.is-small {
font-size: 11px;
}

.gtranslate-cookie-text-container {
width: 15rem;

@include from($tablet) {
width: 20rem;
}
}
}
}

.gtranslate-wrapper,
.skiptranslate {
color: $black !important;
}
1 change: 1 addition & 0 deletions assets/styles/ui-components/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
@import "header/secondary-nav";
@import "header/fly-out-nav";
@import "header/notice";
@import "header/gtranslate-dropdown";
@import "footer";
@import "content-grid";
@import "duetdatepicker";
Expand Down
Binary file modified lang/fi.mo
Binary file not shown.
Loading
Loading