Skip to content
87 changes: 59 additions & 28 deletions docs/dictionary-manager-consolidation.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,27 @@

## Overview

The `DictionaryManager` class has been successfully implemented to provide a unified solution for key replacement functionality. The implementation uses direct JSON fetching from placeholders.json files and integrates seamlessly with the existing event-libs architecture.
The `DictionaryManager` class has been successfully implemented to provide a unified solution for key replacement functionality. The implementation fetches a multi-sheet dictionary from `event-libs/assets/configs/dictionary.json` using `import.meta.url` and integrates seamlessly with the existing event-libs architecture.

## Current Implementation

### 1. DictionaryManager Class

The `DictionaryManager` class in `event-libs/v1/utils/dictionary-manager.js` provides:

- **`initialize(config, sheet)`**: Convenience method to set up the manager and fetch the dictionary
- **`getValue(key)`**: Get value for a key from the dictionary
- **`fetchDictionary({ config, sheet })`: Fetch dictionary from placeholders.json
- **`initialize()`**: Load all dictionary sheets (one fetch loads everything)
- **`loadAllSheets()`**: Load all sheets from the dictionary JSON
- **`getValue(key, sheet)`**: Get value for a key from a specific dictionary sheet
- **`fetchDictionary()`**: Static method to fetch the multi-sheet dictionary.json (cached)
- **`getDictionaryPath()`**: Static method to get dictionary URL using import.meta.url

### 2. Integration with decorate.js

The `decorateArea` function initializes the `DictionaryManager` with configuration:
The `initRSVPHandler` function initializes the `DictionaryManager`:

```javascript
export default async function decorateArea(area = document) {
// Initialize DictionaryManager with configuration
try {
const { miloConfig } = getEventConfig();
await dictionaryManager.initialize(miloConfig);
} catch (error) {
window.lana?.log(`Failed to initialize DictionaryManager:\n${JSON.stringify(error, null, 2)}`);
}
async function initRSVPHandler(link) {
await dictionaryManager.initialize();
// ... rest of the function
}
```
Expand All @@ -46,11 +42,12 @@ const closedText = dictionaryManager.getValue('event-full-cta-text');
## Benefits of the Current Implementation

1. **Unified Interface**: All key replacement functionality is handled through a single `DictionaryManager` class
2. **Simplified Architecture**: Uses direct JSON fetching from placeholders.json files
3. **Better Performance**: Provides efficient dictionary lookups
4. **Simplified Code**: Clean, maintainable implementation
5. **Reduced Dependencies**: No external library dependencies
6. **Seamless Integration**: Works with existing event-libs architecture
2. **Domain from Code**: Uses import.meta.url to get the domain where the code is hosted
3. **Locale-Aware**: Uses getLocale to determine the correct prefix for localized dictionaries
4. **Single Fetch, All Sheets**: One fetch loads all available sheets automatically
5. **Optimized Performance**: Smart caching with promise deduplication
6. **Simple API**: Just call `initialize()` once, no need to manage individual sheets
7. **Seamless Integration**: Works with existing event-libs architecture

## Usage

Expand All @@ -61,35 +58,69 @@ Use the `DictionaryManager` directly:
```javascript
import { dictionaryManager } from './dictionary-manager.js';

// Initialize (usually done once in decorateArea)
await dictionaryManager.initialize(config);
// Initialize - loads all sheets in one fetch
await dictionaryManager.initialize();

// Use for key replacement
// Get value from data sheet (default)
const value = dictionaryManager.getValue('my-key');

// Get value from specific sheet
const fieldLabel = dictionaryManager.getValue('First name', 'rsvp-fields');
```

### Configuration

The `DictionaryManager` expects a configuration object with:
The `DictionaryManager` fetches dictionaries from `${domain}${prefix}/event-libs/assets/configs/dictionary.json`, where:
- `domain` is extracted from `import.meta.url` (e.g., `https://main--milo--adobecom.aem.page`)
- `prefix` is determined by locale using milo's `getLocale()` (e.g., `/fr`, `/de`, or empty string for en-US)

The dictionary.json file contains a multi-sheet structure:

```javascript
{
locale: {
contentRoot: '/path/to/content/root'
}
"data": {
"total": 12,
"offset": 0,
"limit": 12,
"data": [
{ "key": "registered-cta-text", "value": "I'm going" },
{ "key": "waitlisted-cta-text", "value": "Added to waitlist" },
// ... more entries
]
},
"rsvp-fields": {
"total": 137,
"offset": 0,
"limit": 137,
"data": [
{ "key": "First name", "value": "First name" },
{ "key": "Last name", "value": "Last name" },
// ... more entries
]
},
":version": 3,
":names": ["data", "rsvp-fields"],
":type": "multi-sheet"
}
```

The system uses 'data' for general dictionary entries and 'rsvp-fields' for form-specific translations.

## Testing

A test file `test/unit/scripts/dictionary-manager.test.js` verifies the functionality works correctly. The tests cover:

- Basic `getValue` functionality
- Basic `getValue` functionality for both sheets
- Dictionary fetching and initialization
- Static method functionality
- Automatic loading of all sheets from single fetch
- Fetch caching and deduplication

## Architecture Notes

- **Direct Integration**: Dictionary functionality is integrated directly where needed
- **Clean Implementation**: Simple, focused class with clear responsibilities
- **Performance Optimized**: Uses frozen objects and efficient lookups
- **Performance Optimized**: Uses frozen objects, efficient lookups, and fetch caching
- **Single Fetch**: All sheets are fetched in a single request and cached statically
- **Domain from Code**: Uses import.meta.url to get the hosting domain
- **Locale Support**: Uses milo's getLocale to determine the prefix for multi-locale support
- **External Content**: Dictionary is hosted in the content repo at `${domain}${prefix}/event-libs/assets/configs/dictionary.json`
77 changes: 44 additions & 33 deletions event-libs/v1/blocks/events-form/events-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import BlockMediator from '../../deps/block-mediator.min.js';
import { signIn, decorateEvent } from '../../utils/decorate.js';
import { dictionaryManager } from '../../utils/dictionary-manager.js';
import { getEventConfig, LIBS, getMetadata, getSusiOptions } from '../../utils/utils.js';
import { FALLBACK_LOCALES } from '../../utils/constances.js';

const eventConfig = getEventConfig();
const miloLibs = eventConfig?.miloConfig?.miloLibs ? eventConfig.miloConfig.miloLibs : LIBS;
Expand Down Expand Up @@ -473,7 +472,7 @@ async function loadConsent(form, consentData) {
termsWrapper.innerHTML = '';
termsWrapper.classList.add('transparent');

const termsFragLink = createTag('a', { href: path, target: '_blank' }, path, { parent: termsWrapper });
const termsFragLink = createTag('a', { href: `${new URL(path, import.meta.url).href}`, target: '_blank' }, path, { parent: termsWrapper });

await loadFragment(termsFragLink);

Expand Down Expand Up @@ -660,8 +659,8 @@ async function addConsentSuite(form) {

fieldWrapper.append(label, countrySelect);

const queryIndexUrl = new URL('/event-libs/system/consent-query-index.json', import.meta.url);
const consentStringsIndex = await fetch(queryIndexUrl).then((r) => r.json());
const queryIndexUrl = new URL('/event-libs/assets/consents/consent-query-index.json', import.meta.url);
const consentStringsIndex = await fetch(queryIndexUrl.href).then((r) => r.json());

if (consentStringsIndex) {
const { data } = consentStringsIndex;
Expand Down Expand Up @@ -729,19 +728,14 @@ async function createForm(bp, formData) {
window.lana?.log(`Failed to parse partners metadata:\n${JSON.stringify(error, null, 2)}`);
}

const { pathname } = new URL(form.href);
let json = formData;
/* c8 ignore next 4 */
if (!formData) {
const resp = await fetch(pathname);
const resp = await fetch(form.href);
json = await resp.json();
}

const config = getConfig();
await Promise.all([
dictionaryManager.addSheet({ config, sheet: 'default' }),
dictionaryManager.addSheet({ config, sheet: 'rsvp-fields' }),
]);
await dictionaryManager.initialize();

if (rsvpFieldsData) {
const { required, visible } = rsvpFieldsData;
Expand All @@ -763,7 +757,7 @@ async function createForm(bp, formData) {

const formEl = createTag('form');
const rules = [];
const [action] = pathname.split('.json');
const [action] = new URL(form.href).pathname.split('.json');
formEl.dataset.action = action;

const typeToElement = {
Expand Down Expand Up @@ -1005,28 +999,29 @@ async function decorateToastArea() {
return toastArea;
}

async function getFormLink(block, bp) {
const eventConfig = getEventConfig();
const { miloConfig, cmsType } = eventConfig;
const miloLibs = miloConfig?.miloLibs ? miloConfig.miloLibs : LIBS;
const { getLocale } = await import(`${miloLibs}/utils/utils.js`);
const { prefix } = getLocale(miloConfig?.locales || FALLBACK_LOCALES);
function getRsvpConfigUrl() {
// Get the domain from import.meta.url
const moduleUrl = new URL(import.meta.url);
const domain = `${moduleUrl.protocol}//${moduleUrl.host}`;

// Get cloud type (creativecloud or experiencecloud)
const cloudType = getMetadata('cloud-type');
if (!cloudType) {
throw new Error('cloud-type metadata is required');
}

return `${domain}/event-libs/assets/configs/rsvp/${cloudType.toLowerCase()}.json`;
}

function getFormLink(block, bp) {
const legacyLink = block.querySelector(':scope > div:nth-of-type(2) a[href$=".json"]');

const cloudType = getMetadata('cloud-type');
const configLocation = getMetadata('rsvp-config-location');
const form = createTag('a');

if (!configLocation && cmsType === 'SP') {
form.href = `/events/default/rsvp-form-configs/${cloudType.toLowerCase()}.json`;
} else {
form.href = `${prefix}${configLocation.startsWith('/') ? configLocation : `/${configLocation}`}`;
}

if (!form.href) {
const configUrl = new URL(`/events/default/rsvp-form-configs/${cloudType.toLowerCase()}.json`, import.meta.url);
form.href = configUrl.toString();
try {
form.href = getRsvpConfigUrl();
} catch (error) {
window.lana?.log(`Error getting RSVP config URL: ${JSON.stringify(error)}`);
throw error;
}

if (legacyLink) {
Expand All @@ -1040,19 +1035,35 @@ async function getFormLink(block, bp) {

export default async function decorate(block, formData = null) {
block.classList.add('loading');

const toastArea = await decorateToastArea();

const eventHero = block.querySelector(':scope > div:nth-of-type(1)');
const hasLegacyLink = block.querySelector(':scope > div:nth-of-type(2) a[href$=".json"]');
let formContainer;
if (hasLegacyLink) {
formContainer = block.querySelector(':scope > div:nth-of-type(2)');
} else {
const hasEmptyFormContainer = block.querySelector(':scope > div:nth-of-type(2)').innerHTML.trim() === '';
if (hasEmptyFormContainer) {
formContainer = block.querySelector(':scope > div:nth-of-type(2)');
} else {
formContainer = createTag('div');
eventHero.after(formContainer);
}
}

const bp = {
block,
toastArea,
eventHero: block.querySelector(':scope > div:nth-of-type(1)'),
formContainer: block.querySelector(':scope > div:nth-of-type(2)'),
eventHero,
formContainer,
terms: block.querySelector(':scope > div:nth-of-type(3)'),
rsvpSuccessScreen: block.querySelector(':scope > div:nth-of-type(4)'),
waitlistSuccessScreen: block.querySelector(':scope > div:nth-of-type(5)'),
};

bp.form = await getFormLink(block, bp);
bp.form = getFormLink(block, bp);

await onProfile(bp, formData);
}
52 changes: 37 additions & 15 deletions event-libs/v1/blocks/promotional-content/promotional-content.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
import { getMetadata, getEventConfig, LIBS } from '../../utils/utils.js';
import { FALLBACK_LOCALES } from '../../utils/constances.js';

async function getPromotionalContentUrl() {
const eventConfig = getEventConfig();
const { miloConfig } = eventConfig;
const miloLibs = miloConfig?.miloLibs ? miloConfig.miloLibs : LIBS;
const { getLocale } = await import(`${miloLibs}/utils/utils.js`);

const { prefix } = getLocale(miloConfig?.locales || FALLBACK_LOCALES);

// Get the domain from import.meta.url
const moduleUrl = new URL(import.meta.url);
const domain = `${moduleUrl.protocol}//${moduleUrl.host}`;

return `${domain}${prefix}/event-libs/assets/configs/promotional-content.json`;
}

async function getPromotionalContent() {
let promotionalItems = [];
const eventPromotionalItemsMetadata = getMetadata('promotional-items');
Expand All @@ -24,25 +39,32 @@ async function getPromotionalContent() {
return [];
}

const eventConfig = getEventConfig();
const { miloConfig } = eventConfig;
const miloLibs = miloConfig?.miloLibs ? miloConfig.miloLibs : LIBS;
const { getLocale } = await import(`${miloLibs}/utils/utils.js`);
try {
const url = await getPromotionalContentUrl();
const response = await fetch(url);

if (!response.ok) {
throw new Error(`Failed to fetch promotional content: ${response.status}`);
}

const json = await response.json();
const data = json.data || [];

const { prefix } = getLocale(miloConfig?.locales || FALLBACK_LOCALES);
const { data } = await fetch(`${prefix}/events/default/promotional-content.json`).then((res) => res.json());
if (!data || data.length === 0) {
window.lana?.log(`Error: No promotional content found at ${url}`);
return [];
}

const rehydratedPromotionalItems = promotionalItems.map((item) => {
const promotionalItem = data.find((content) => content.name === item);
return promotionalItem;
});

if (!data) {
window.lana?.log(`Error: No promotional content found in ${prefix}/events/default/promotional-content.json`);
return rehydratedPromotionalItems;
} catch (error) {
window.lana?.log(`Error fetching promotional content: ${JSON.stringify(error)}`);
return [];
}

const rehydratedPromotionalItems = promotionalItems.map((item) => {
const promotionalItem = data.find((content) => content.name === item);
return promotionalItem;
});

return rehydratedPromotionalItems;
}

export function addMediaReversedClass(el) {
Expand Down
18 changes: 18 additions & 0 deletions event-libs/v1/libs-styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,21 @@
.section:has(> .section-metadata:only-child) {
display: none;
}

a.con-button.rsvp-btn {
display: flex;
gap: 4px;
align-items: center;
justify-content: center;
}

a.con-button.rsvp-btn.disabled {
opacity: 0.5;
pointer-events: none;
}

a.con-button.no-event {
user-select: none;
pointer-events: none;
opacity: 0.5;
}
Loading
Loading