1+ import {
2+ DEFAULT_LANGUAGE ,
3+ ROOT_PATH ,
4+ DEFAULT_LOCALE ,
5+ } from '../scripts/global/constants.js' ;
16/**
27 * Get the current country and language codes label by matching the current
38 * location pathname to a regex.
@@ -10,54 +15,149 @@ export function getCurrentCountryLanguage() {
1015 return match ? match . slice ( 1 , 3 ) : [ '' , '' ] ;
1116}
1217
18+ /**
19+ * Get the current language code by matching the current location pathname to a regex.
20+ *
21+ * IMPORTANT: Assumes a "/language" page structure (no countries).
22+ * @returns {string } The current code on success (e.g. "en"), empty string otherwise.
23+ */
24+ export function getCurrentLanguage ( ) {
25+ const match = window . location . pathname
26+ . replace ( ROOT_PATH , '' )
27+ . match ( '^/([a-z]{2})' ) ;
28+ const currentLanguage = match ? match . at ( 1 ) : DEFAULT_LANGUAGE ;
29+ return currentLanguage || DEFAULT_LANGUAGE ;
30+ }
31+
1332/** @param {string[] } classes */
1433export function cx ( ...classes ) {
1534 return classes . filter ( Boolean ) . join ( ' ' ) ;
1635}
1736
18- /** @type {Promise<any> | null } */
19- let dictionaryPromise = null ;
20- export async function getDictionary ( ) {
21- const lang = document . documentElement . lang . toLowerCase ( ) || 'en-us' ;
22- if ( dictionaryPromise === null ) {
23- dictionaryPromise = fetch ( '/api/dictionary.json' )
24- . then ( ( res ) => res . json ( ) ) ;
37+ /**
38+ * Helper function to set a property on an object using a dot-separated path.
39+ * It creates nested objects as needed.
40+ * @param {object } obj The object to modify.
41+ * @param {string } path The dot-separated path (e.g., "path.to.destination").
42+ * @param {any } value The value to set at the destination.
43+ */
44+ function setNestedProperty ( obj , path , value ) {
45+ const keys = path . split ( '.' ) ;
46+ let current = obj ;
47+ // Iterate through the keys until the second-to-last key
48+ for ( let i = 0 ; i < keys . length - 1 ; i += 1 ) {
49+ const key = keys [ i ] ;
50+ // If the nested object doesn't exist, create it.
51+ if ( ! current [ key ] || typeof current [ key ] !== 'object' ) {
52+ current [ key ] = { } ;
53+ }
54+ // Move to the next level down.
55+ current = current [ key ] ;
2556 }
57+ // Set the value on the final key.
58+ current [ keys [ keys . length - 1 ] ] = value ;
59+ }
2660
27- /** @type Array<{ key:string} & Record<string, string>> */
28- const dictionary = ( await dictionaryPromise ) . data ;
29- const dictionaryLangValues = dictionary . filter ( ( item ) => Object . keys ( item ) . includes ( lang ) ) ;
30- const dictionaryLang = dictionaryLangValues . map ( ( item ) => {
31- const { key } = item ;
32- const value = item [ lang ] ;
33- return [ key , value ] ;
61+ /**
62+ * Extracts key-value pairs for a specific column and builds a nested object.
63+ * If the given column doesn't exist or any entry is empty for it,
64+ * the default language (first) and the default locale (second) are used as fallbacks.
65+ * @param {object } dataObject The full input data from the source.
66+ * @param {string } column The column to extract.
67+ * @returns {object } A new nested object with keys mapped to their translations.
68+ */
69+ function getTranslationsForDictionaryColumn ( dataObject , column ) {
70+ const finalObject = { } ;
71+ dataObject . data . forEach ( ( item ) => {
72+ // Check if the current item has a translation for the selected column.
73+ if ( Object . hasOwn ( item , column ) ) {
74+ setNestedProperty (
75+ finalObject ,
76+ item . key ,
77+ item [ column ] || item [ DEFAULT_LANGUAGE ] || item [ DEFAULT_LOCALE ] ,
78+ ) ;
79+ } else if ( Object . hasOwn ( item , DEFAULT_LOCALE ) ) {
80+ // Fallback to the "en" language or "com-en" locale
81+ setNestedProperty ( finalObject , item . key , item [ DEFAULT_LANGUAGE ] || item [ DEFAULT_LOCALE ] ) ;
82+ }
3483 } ) ;
84+ return finalObject ;
85+ }
3586
36- // key is e.g. blogpost.backtotop and value is 'Back to top'. We are creatating a
37- // new object split by '.' and creating nested objects.
38- const dictionaryLangNested = dictionaryLang . reduce (
39- ( /** @type {Record<string, any> } */ acc , [ key , value ] ) => {
40- const keys = key . split ( '.' ) ;
41- const last = keys . pop ( ) ;
42- if ( ! last ) {
43- return acc ;
44- }
45- const obj = acc ;
46- // eslint-disable-next-line no-shadow
47- keys . reduce ( ( acc , k ) => {
48- if ( ! ( k in acc ) ) {
49- acc [ k ] = { } ;
50- }
51- return acc [ k ] ;
52- } , obj ) ;
53- obj [ last ] = value ;
54- return acc ;
55- } ,
56- { } ,
57- ) ;
58- return dictionaryLangNested ;
87+ /**
88+ * Fetches the dictionary for a given column.
89+ * @param {string } column The column to extract.
90+ * @returns {Record<string, string> } The dictionary map for the column.
91+ */
92+ async function fetchDictionary ( column ) {
93+ try {
94+ const resp = await fetch ( `${ window . location . origin } ${ window . hlx . codeBasePath } /api/dictionary.json` ) ;
95+ const data = await resp . json ( ) ;
96+ return {
97+ promise : null ,
98+ data : getTranslationsForDictionaryColumn ( data , column ) ,
99+ } ;
100+ } catch ( error ) {
101+ // eslint-disable-next-line no-console
102+ console . warn ( `Could not fetch dictionary for column ${ column } due to:` , error ) ;
103+ return {
104+ promise : null ,
105+ data : { } ,
106+ } ;
107+ }
59108}
109+ /**
110+ * Gets the dictionary object for the given column.
111+ * @param {string? } column The target column.
112+ * @returns {Promise<object> } The dictionary object.
113+ */
114+ export async function getDictionaryColumn ( column = null ) {
115+ // Initialize window
116+ window . dictionary = window . dictionary || { } ;
117+ window . dictionary [ column ] = window . dictionary [ column ] || {
118+ data : null ,
119+ promise : null ,
120+ } ;
60121
122+ // IMPORTANT: never replace this entry but only mutate its fields to avoid a race condition!
123+ const stableCacheEntry = window . dictionary [ column ] ;
124+
125+ // Return dictionary if already loaded
126+ if ( stableCacheEntry . data ) {
127+ return stableCacheEntry . data ;
128+ }
129+
130+ // Return promise if dictionary is currently loading
131+ if ( stableCacheEntry . promise ) {
132+ return stableCacheEntry . promise ;
133+ }
134+
135+ // Fetch dictionary and store the promise
136+ stableCacheEntry . promise = fetchDictionary ( column ) . then ( ( result ) => {
137+ stableCacheEntry . data = result . data ;
138+ // Clear promise once resolved
139+ stableCacheEntry . promise = null ;
140+ return stableCacheEntry . data ;
141+ } ) ;
142+
143+ return stableCacheEntry . promise ;
144+ }
145+ /**
146+ * Gets the dictionary object for the current site language, with fallback to the default one.
147+ *
148+ * IMPORTANT: Assumes that the dictionary spreadsheet has language only columns
149+ * (e.g. "en", "zh").
150+ * @param {string? } language The language code (e.g. "en").
151+ * @returns {Promise<object> } The dictionary object.
152+ */
153+ export async function getDictionary ( language = null ) {
154+ const currentLanguage = language || getCurrentLanguage ( ) ;
155+ let dictionary = await getDictionaryColumn ( currentLanguage ) ;
156+ if ( ! dictionary || ! dictionary . length ) {
157+ dictionary = await getDictionaryColumn ( DEFAULT_LOCALE ) ;
158+ }
159+ return dictionary ;
160+ }
61161/**
62162 * Creates an HTML element with the specified tag name and attributes
63163 * @param {string } tag - The HTML tag name
@@ -171,21 +271,17 @@ export async function queryEntireIndex(indexFile, pageSize = 500) {
171271}
172272
173273/**
174- * Fetch query-index.json preferring localized path (/<country>-<lang>/query-index.json)
175- * with a fallback to the root (/query-index.json). The result is cached in
176- * the module-scoped queryIndexPromise.
177- * @returns {Promise<any> } Parsed JSON of the query index
274+ * Fetch query-index.json preferring localized path (/<lang>/query-index.json)
275+ * with a fallback to the root (/query-index.json).
276+ * @returns {Promise<import('./types.js').IndexedPageMetadata[]> } The parsed query index
178277 */
179- export function getQueryIndex ( ) {
180- let queryIndexPromise = null ;
181- if ( queryIndexPromise === null ) {
182- const [ currentCountry , currentLanguage ] = getCurrentCountryLanguage ( ) ;
183- const localizedUrl = `/${ currentCountry } -${ currentLanguage } /query-index.json` ;
184- const fallbackUrl = '/query-index' ;
185- queryIndexPromise = queryEntireIndex ( localizedUrl )
186- . then ( ( res ) => ( res . ok ? res : Promise . reject ( new Error ( 'Localized query-index not found' ) ) ) )
187- . then ( ( res ) => res . json ( ) )
188- . catch ( ( ) => queryEntireIndex ( fallbackUrl ) . then ( ( res ) => res . json ( ) ) ) ;
278+ export async function getQueryIndex ( ) {
279+ /** @type {import('./types.js').IndexedPageMetadata[]? } */
280+ let queryIndex = null ;
281+ try {
282+ queryIndex = ( await queryEntireIndex ( `${ getCurrentLanguage ( ) } /query-index` ) ) ?. data ?? [ ] ;
283+ } catch {
284+ queryIndex = ( await queryEntireIndex ( 'query-index' ) ) ?. data ?? [ ] ;
189285 }
190- return queryIndexPromise ;
286+ return ( queryIndex ?? [ ] ) ;
191287}
0 commit comments