1// Import English strings statically (they are used as fallback regardless of 2// the language). 3import * as stringsEN from "./strings-en.js"; 4 5// This must be kept in sync with the list of supported languages 6// in several places in different codebases: 7// 8// * roomsxyz/auto-translate/supported-languages.txt 9// * IntlSystem.cs 10// * roomsxyz-cloudfun/functions/translator.js 11const SUPPORTED_LANGUAGES = [ 12 "en", "fr", "it", "de", "es", "pt", "zh-Hant", "zh-Hans", "ko", "ja", 13 "ru", "vi", 14 "uk", "tr", "pl", "nl" 15]; 16 17// Promise that resolves to the internationalized strings table 18// when it loads. 19let intlLoadPromiseResolve = null; 20const intlLoadPromise = new Promise(resolve => { 21 intlLoadPromiseResolve = resolve; 22}); 23let stringTable = null; // Loaded asynchronously 24 25// Must be called when the page loads. 26export function init() { 27 console.log('INTL: init...'); 28 asyncLoadStrings(); 29} 30 31// Returns a Promise that resolves when internationalization is loaded. 32// Usage: 33// whenIntlLoaded().then(....) 34// OR: 35// await whenIntlLoaded() 36// 37// After loaded, you can call getString synchronously to get a string. 38export function whenIntlLoaded() { return intlLoadPromise; } 39 40function getFallbackStringTemplate(stringId) { 41 const entry = stringsEN.STRINGS[stringId]; 42 if (typeof(entry) === "string") return entry; 43 if (entry && typeof(entry) === "object" && typeof(entry.text) === "string") return entry.text; 44 return "?" + stringId; 45} 46 47// ATTENTION: this is a synchronous function that can only be called after 48// internationalization is done loading (await on whenIntlLoaded() if needed). 49export function getString(stringId, ...args) { 50 let string = "?" + stringId; 51 52 if (stringTable) { 53 const entry = stringTable[stringId]; 54 if (typeof(entry) === "string") { 55 string = entry; 56 } else if (entry && typeof(entry) === "object" && typeof(entry.text) === "string") { 57 string = entry.text; 58 } else { 59 console.warn(`INTL: string not found ${stringId}`); 60 // Fall back to English 61 string = getFallbackStringTemplate(stringId); 62 } 63 } else { 64 console.error("INTL: Attempt to call getString before strings are done loading!"); 65 // Fall back to English 66 string = getFallbackStringTemplate(stringId); 67 } 68 69 // Replace special {R} placeholder (app name). 70 // We expand {R} to "Rooms\u00AE" (Rooms followed by the ® registered 71 // trademark symbol) so that every localized string referring to the brand 72 // gets the trademark mark automatically. 73 string = string.replaceAll('{R}', "Rooms\u00AE"); 74 string = string.replaceAll('{T}', "Things, Inc"); 75 76 // Replace placeholders like {0}, {1}, {2}, etc. 77 return string.replace(/{(\d+)}/g, (match, index) => { 78 return typeof(args[index]) !== 'undefined' ? args[index] : match; 79 }); 80} 81 82export function getLangCode() { 83 let lang = navigator.language || "en"; 84 85 // Check for stored preference first 86 const storedLang = localStorage.getItem('preferredLanguage'); 87 if (storedLang && SUPPORTED_LANGUAGES.includes(storedLang)) { 88 return storedLang; 89 } 90 91 // If the URL contains an 'hl' parameter, honor it 92 if (window.location && window.location.search) { 93 const urlParams = new URLSearchParams(window.location.search); 94 const urlLang = urlParams.get('hl'); 95 if (urlLang) { 96 lang = urlLang; 97 } 98 } 99 100 // Supported directly? 101 if (SUPPORTED_LANGUAGES.includes(lang)) return lang; 102 103 // Okay, let's try to be creative. 104 // Special case for China 105 if (lang.startsWith("zh")) { 106 return (lang === "zh-CN" || lang === "zh-SG" || lang === "zh-hans") ? 107 "zh-Hans" : "zh-Hant"; 108 } 109 110 // Otherwise, try looking only at the part before the hyphen. 111 if (lang.includes('-')) { 112 lang = lang.split('-')[0]; 113 } 114 if (SUPPORTED_LANGUAGES.includes(lang)) return lang; 115 116 lang = lang.toLowerCase(); 117 return SUPPORTED_LANGUAGES.includes(lang) ? lang : "en"; 118} 119 120async function asyncLoadStrings() { 121 const langCode = getLangCode(); 122 console.log(`INTL: User language: ${navigator.language}, ` + 123 `our supported langCode is: ${langCode}, loading...`); 124 try { 125 stringTable = (await import("/js-intl/strings-" + langCode + ".js")).STRINGS; 126 if (!stringTable) throw new Error(`Loaded module for ${langCode} but it had no STRINGS`); 127 console.log(`INTL: Successfully loaded string table for langCode ${langCode}, ${Object.keys(stringTable).length} strings.`); 128 } catch (e) { 129 console.error("INTL: Failed to load internationalization for langCode " + langCode); 130 console.error(e); 131 return; 132 } 133 134 // Populate translated text on static HTML elements. 135 let numEls = 0; 136 for (const el of document.querySelectorAll('[data-string-id]')) { 137 const stringId = el.getAttribute('data-string-id'); 138 if (!stringId) continue; 139 el.innerHTML = getString(stringId); 140 ++numEls; 141 } 142 console.log(`INTL: auto-translated ${numEls} elements`); 143 144 // Add a class to the body to indicate what language we're translated to. 145 document.body.classList.add("intl-" + langCode); 146 147 intlLoadPromiseResolve(); 148}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.