2023-12-02 19:04:51 +01:00
import { registerDebugFunction } from './power-user.js' ;
2024-04-26 21:57:42 +02:00
import { updateSecretDisplay } from './secrets.js' ;
2023-08-22 09:37:18 +02:00
2023-12-02 19:04:51 +01:00
const storageKey = 'language' ;
2024-03-10 19:49:11 +01:00
const overrideLanguage = localStorage . getItem ( storageKey ) ;
const localeFile = String ( overrideLanguage || navigator . language || navigator . userLanguage || 'en' ) . toLowerCase ( ) ;
2024-07-10 17:56:01 +02:00
var langs ;
2024-05-13 14:56:37 +02:00
// Don't change to let/const! It will break module loading.
// eslint-disable-next-line prefer-const
2024-07-10 17:56:01 +02:00
var localeData ;
2024-03-10 19:49:11 +01:00
2024-10-28 10:01:48 +01:00
export const getCurrentLocale = ( ) => localeFile ;
2024-06-28 23:01:54 +02:00
/ * *
* An observer that will check if any new i18n elements are added to the document
* @ type { MutationObserver }
* /
const observer = new MutationObserver ( mutations => {
mutations . forEach ( mutation => {
mutation . addedNodes . forEach ( node => {
if ( node . nodeType === Node . ELEMENT _NODE && node instanceof Element ) {
if ( node . hasAttribute ( 'data-i18n' ) ) {
translateElement ( node ) ;
}
node . querySelectorAll ( '[data-i18n]' ) . forEach ( element => {
translateElement ( element ) ;
} ) ;
}
} ) ;
if ( mutation . attributeName === 'data-i18n' && mutation . target instanceof Element ) {
translateElement ( mutation . target ) ;
}
} ) ;
} ) ;
2024-06-28 23:53:25 +02:00
/ * *
* Translates a template string with named arguments
*
* Uses the template literal with all values replaced by index placeholder for translation key .
*
* @ example
* ` ` ` js
* toastr . warn ( t ` Tag ${ tagName } not found. ` ) ;
* ` ` `
* Should be translated in the translation files as :
* ` ` `
* Tag $ { 0 } not found . - > Tag $ { 0 } nicht gefunden .
* ` ` `
*
* @ param { TemplateStringsArray } strings - Template strings array
* @ param { ... any } values - Values for placeholders in the template string
* @ returns { string } Translated and formatted string
* /
export function t ( strings , ... values ) {
let str = strings . reduce ( ( result , string , i ) => result + string + ( values [ i ] !== undefined ? ` \$ { ${ i } } ` : '' ) , '' ) ;
let translatedStr = translate ( str ) ;
// Replace indexed placeholders with actual values
return translatedStr . replace ( /\$\{(\d+)\}/g , ( match , index ) => values [ index ] ) ;
}
/ * *
* Translates a given key or text
*
* If the translation is based on a key , that one is used to find a possible translation in the translation file .
* The original text still has to be provided , as that is the default value being returned if no translation is found .
*
* For in - code text translation on a format string , using the template literal ` t ` is preferred .
*
* @ param { string } text - The text to translate
* @ param { string ? } key - The key to use for translation . If not provided , text is used as the key .
* @ returns { string } - The translated text
* /
export function translate ( text , key = null ) {
const translationKey = key || text ;
return localeData ? . [ translationKey ] || text ;
}
2024-03-10 19:49:11 +01:00
/ * *
* Fetches the locale data for the given language .
* @ param { string } language Language code
* @ returns { Promise < Record < string , string >> } Locale data
* /
async function getLocaleData ( language ) {
2024-04-24 16:12:40 +02:00
let supportedLang = findLang ( language ) ;
2024-04-26 21:57:42 +02:00
if ( ! supportedLang ) {
return { } ;
}
2023-08-22 09:37:18 +02:00
2024-03-10 19:49:11 +01:00
const data = await fetch ( ` ./locales/ ${ language } .json ` ) . then ( response => {
console . log ( ` Loading locale data from ./locales/ ${ language } .json ` ) ;
if ( ! response . ok ) {
return { } ;
}
return response . json ( ) ;
} ) ;
return data ;
}
2024-04-24 16:12:40 +02:00
function findLang ( language ) {
2024-04-26 21:57:42 +02:00
var supportedLang = langs . find ( x => x . lang === language ) ;
2024-04-24 16:12:40 +02:00
if ( ! supportedLang ) {
console . warn ( ` Unsupported language: ${ language } ` ) ;
}
2024-04-26 21:57:42 +02:00
return supportedLang ;
2024-04-24 16:12:40 +02:00
}
2024-06-28 23:01:54 +02:00
/ * *
* Translates a given element based on its data - i18n attribute .
* @ param { Element } element The element to translate
* /
function translateElement ( element ) {
const keys = element . getAttribute ( 'data-i18n' ) . split ( ';' ) ; // Multi-key entries are ; delimited
for ( const key of keys ) {
const attributeMatch = key . match ( /\[(\S+)\](.+)/ ) ; // [attribute]key
if ( attributeMatch ) { // attribute-tagged key
const localizedValue = localeData ? . [ attributeMatch [ 2 ] ] ;
if ( localizedValue || localizedValue === '' ) {
element . setAttribute ( attributeMatch [ 1 ] , localizedValue ) ;
}
} else { // No attribute tag, treat as 'text'
const localizedValue = localeData ? . [ key ] ;
if ( localizedValue || localizedValue === '' ) {
element . textContent = localizedValue ;
}
}
}
}
2024-03-10 19:49:11 +01:00
async function getMissingTranslations ( ) {
2023-08-24 14:13:04 +02:00
const missingData = [ ] ;
2024-04-26 21:57:42 +02:00
// Determine locales to search for untranslated strings
const isNotSupported = ! findLang ( localeFile ) ;
const langsToProcess = ( isNotSupported || localeFile == 'en' ) ? langs : [ findLang ( localeFile ) ] ;
2024-04-24 16:12:40 +02:00
for ( const language of langsToProcess ) {
const localeData = await getLocaleData ( language . lang ) ;
2023-12-02 19:04:51 +01:00
$ ( document ) . find ( '[data-i18n]' ) . each ( function ( ) {
const keys = $ ( this ) . data ( 'i18n' ) . split ( ';' ) ; // Multi-key entries are ; delimited
2023-08-24 14:13:04 +02:00
for ( const key of keys ) {
const attributeMatch = key . match ( /\[(\S+)\](.+)/ ) ; // [attribute]key
if ( attributeMatch ) { // attribute-tagged key
2024-03-10 00:03:51 +01:00
const localizedValue = localeData ? . [ attributeMatch [ 2 ] ] ;
2023-08-24 14:13:04 +02:00
if ( ! localizedValue ) {
2024-04-24 16:12:40 +02:00
missingData . push ( { key , language : language . lang , value : $ ( this ) . attr ( attributeMatch [ 1 ] ) } ) ;
2023-08-24 14:13:04 +02:00
}
} else { // No attribute tag, treat as 'text'
2024-03-10 00:03:51 +01:00
const localizedValue = localeData ? . [ key ] ;
2023-08-24 14:13:04 +02:00
if ( ! localizedValue ) {
2024-04-24 16:12:40 +02:00
missingData . push ( { key , language : language . lang , value : $ ( this ) . text ( ) . trim ( ) } ) ;
2023-08-24 14:13:04 +02:00
}
}
}
} ) ;
}
// Remove duplicates
const uniqueMissingData = [ ] ;
for ( const { key , language , value } of missingData ) {
if ( ! uniqueMissingData . some ( x => x . key === key && x . language === language && x . value === value ) ) {
uniqueMissingData . push ( { key , language , value } ) ;
}
}
// Sort by language, then key
uniqueMissingData . sort ( ( a , b ) => a . language . localeCompare ( b . language ) || a . key . localeCompare ( b . key ) ) ;
// Map to { language: { key: value } }
2024-03-10 00:03:51 +01:00
let missingDataMap = { } ;
for ( const { key , value } of uniqueMissingData ) {
if ( ! missingDataMap ) {
missingDataMap = { } ;
2023-08-24 14:13:04 +02:00
}
2024-03-10 00:03:51 +01:00
missingDataMap [ key ] = value ;
2023-08-24 14:13:04 +02:00
}
console . table ( uniqueMissingData ) ;
console . log ( missingDataMap ) ;
2023-08-27 22:20:43 +02:00
toastr . success ( ` Found ${ uniqueMissingData . length } missing translations. See browser console for details. ` ) ;
}
2023-08-24 14:13:04 +02:00
2023-08-22 09:37:18 +02:00
export function applyLocale ( root = document ) {
2024-03-12 19:24:45 +01:00
if ( ! localeData || Object . keys ( localeData ) . length === 0 ) {
return root ;
}
2023-12-02 19:04:51 +01:00
const $root = root instanceof Document ? $ ( root ) : $ ( new DOMParser ( ) . parseFromString ( root , 'text/html' ) ) ;
2023-08-22 09:37:18 +02:00
//find all the elements with `data-i18n` attribute
2023-12-02 19:04:51 +01:00
$root . find ( '[data-i18n]' ) . each ( function ( ) {
2024-06-28 23:01:54 +02:00
translateElement ( this ) ;
2023-08-22 09:37:18 +02:00
} ) ;
if ( root !== document ) {
return $root . get ( 0 ) . body . innerHTML ;
}
}
2024-03-10 19:49:11 +01:00
function addLanguagesToDropdown ( ) {
2024-05-13 14:20:28 +02:00
const uiLanguageSelects = $ ( '#ui_language_select, #onboarding_ui_language_select' ) ;
2024-03-12 19:03:12 +01:00
for ( const langObj of langs ) { // Set the value to the language code
2023-08-22 09:37:18 +02:00
const option = document . createElement ( 'option' ) ;
2024-03-12 19:24:45 +01:00
option . value = langObj [ 'lang' ] ; // Set the value to the language code
option . innerText = langObj [ 'display' ] ; // Set the display text to the language name
2024-05-13 14:20:28 +02:00
uiLanguageSelects . append ( option ) ;
2023-08-22 09:37:18 +02:00
}
const selectedLanguage = localStorage . getItem ( storageKey ) ;
if ( selectedLanguage ) {
2024-05-13 14:20:28 +02:00
uiLanguageSelects . val ( selectedLanguage ) ;
2023-08-22 09:37:18 +02:00
}
}
2024-07-10 17:56:01 +02:00
export async function initLocales ( ) {
langs = await fetch ( '/locales/lang.json' ) . then ( response => response . json ( ) ) ;
localeData = await getLocaleData ( localeFile ) ;
2023-08-22 09:37:18 +02:00
applyLocale ( ) ;
addLanguagesToDropdown ( ) ;
2024-04-26 21:57:42 +02:00
updateSecretDisplay ( ) ;
2023-08-22 09:37:18 +02:00
2024-05-13 14:20:28 +02:00
$ ( '#ui_language_select, #onboarding_ui_language_select' ) . on ( 'change' , async function ( ) {
2023-08-22 13:30:49 +02:00
const language = String ( $ ( this ) . val ( ) ) ;
2023-08-22 09:37:18 +02:00
if ( language ) {
localStorage . setItem ( storageKey , language ) ;
} else {
localStorage . removeItem ( storageKey ) ;
}
location . reload ( ) ;
} ) ;
2023-08-27 22:20:43 +02:00
2024-06-28 23:01:54 +02:00
observer . observe ( document , {
childList : true ,
subtree : true ,
attributes : true ,
attributeFilter : [ 'data-i18n' ] ,
} ) ;
2024-04-24 16:12:40 +02:00
registerDebugFunction ( 'getMissingTranslations' , 'Get missing translations' , 'Detects missing localization data in the current locale and dumps the data into the browser console. If the current locale is English, searches all other locales.' , getMissingTranslations ) ;
2023-08-27 22:20:43 +02:00
registerDebugFunction ( 'applyLocale' , 'Apply locale' , 'Reapplies the currently selected locale to the page.' , applyLocale ) ;
2023-12-02 16:15:03 +01:00
}