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 ( ) ;
const langs = await fetch ( '/locales/lang.json' ) . then ( response => response . json ( ) ) ;
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
var localeData = await getLocaleData ( localeFile ) ;
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-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 ( ) {
2023-08-22 09:37:18 +02:00
//read the translation from the language data
2023-12-02 19:04:51 +01:00
const keys = $ ( this ) . data ( 'i18n' ) . split ( ';' ) ; // Multi-key entries are ; delimited
2023-08-22 09:37:18 +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 ] ] ;
2024-05-07 08:11:59 +02:00
if ( localizedValue || localizedValue == '' ) {
2023-08-22 09:37:18 +02:00
$ ( this ) . attr ( attributeMatch [ 1 ] , localizedValue ) ;
}
} else { // No attribute tag, treat as 'text'
2024-03-10 00:03:51 +01:00
const localizedValue = localeData ? . [ key ] ;
2024-05-07 08:11:59 +02:00
if ( localizedValue || localizedValue == '' ) {
2023-08-22 09:37:18 +02:00
$ ( this ) . text ( localizedValue ) ;
}
}
}
} ) ;
if ( root !== document ) {
return $root . get ( 0 ) . body . innerHTML ;
}
}
2024-03-12 19:03:12 +01:00
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
}
}
2023-11-28 00:44:13 +01:00
export function initLocales ( ) {
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-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
}