Skip to content
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
edb4791
MWPW-189894: fix: Lingo placeholders in Studio card preview (dictiona…
seanchoi-dev Apr 6, 2026
9ef385d
Revert "MWPW-189894: fix: Lingo placeholders in Studio card preview (…
seanchoi-dev Apr 6, 2026
4c1e9ff
MWPW-189894: Fix - Lingo placeholders in Studio card preview
seanchoi-dev Apr 7, 2026
fe6a34d
Merge branch 'main' into MWPW-189894
seanchoi-dev Apr 8, 2026
11c062f
MWPW-189894: fix - align Lingo placeholders with Akamai country and r…
seanchoi-dev Apr 8, 2026
7e76079
Revert "MWPW-189894: Fix - Lingo placeholders in Studio card preview"
seanchoi-dev Apr 8, 2026
d489b47
MWPW-189894: Fix - Lingo placeholders in Studio card preview
seanchoi-dev Apr 9, 2026
78f560a
Merge branch 'main' into MWPW-189894
seanchoi-dev Apr 9, 2026
38e9894
MWPW-189894: Applying Nicolas's comment.
seanchoi-dev Apr 9, 2026
2f67cd8
Merge branch 'main' into MWPW-189894
seanchoi-dev Apr 9, 2026
67e81a1
MWPW-189894: nit - format fix
seanchoi-dev Apr 9, 2026
ada1bea
Merge branch 'main' into MWPW-189894
seanchoi-dev Apr 10, 2026
afaf786
MWPW-189894: Applying Nicolas's comment 2.
seanchoi-dev Apr 10, 2026
bebe16c
Merge branch 'main' into MWPW-189894
seanchoi-dev Apr 13, 2026
5f890a1
Revert "MWPW-189894: Fix - Lingo placeholders in Studio card preview"
seanchoi-dev Apr 13, 2026
9b6408f
MWPW-189894: Applying Nicolas's comment 3.
seanchoi-dev Apr 13, 2026
31ea9b5
MWPW-189894: Nit: more named variable.
seanchoi-dev Apr 13, 2026
396dadb
Merge branch 'main' into MWPW-189894
seanchoi-dev Apr 14, 2026
f9cd222
MWPW-189894: Removing Akamai/geo merge
seanchoi-dev Apr 15, 2026
82eef2b
MWPW-189894: Reverting the cache.js country-aware key and checking fo…
seanchoi-dev Apr 15, 2026
92445f8
MWPW-189894: unit test update, fetchResult update
seanchoi-dev Apr 15, 2026
f0380ee
MWPW-189894: Applied npeltier’s suggestion on getRequestInfos
seanchoi-dev Apr 15, 2026
de3a795
MWPW-189894: For npeltier's comment
seanchoi-dev Apr 15, 2026
c87863a
MWPW-189894: nit: remove re-export
seanchoi-dev Apr 15, 2026
1cf2108
MWPW-189894: polishing logic including remove double loading and remo…
seanchoi-dev Apr 15, 2026
1132aee
MWPW-189894: nit: update some comments.
seanchoi-dev Apr 15, 2026
70e6927
MWPW-189894: Splitting fetchFragment into two pipeline transformers.
seanchoi-dev Apr 15, 2026
7d45741
MWPW-189894: relocate some of functions.
seanchoi-dev Apr 15, 2026
9ea4692
MWPW-189894: Using body.path instead hard-coded path for the fallback.
seanchoi-dev Apr 15, 2026
0cb7389
MWPW-189894: Unit tests updates.
seanchoi-dev Apr 15, 2026
cbeccca
Merge branch 'main' into MWPW-189894
npeltier Apr 15, 2026
5820296
Merge branch 'main' into MWPW-189894
seanchoi-dev Apr 15, 2026
702dcee
MWPW-189894: Rename defaultLanguageVariation -> defaultLanguage
seanchoi-dev Apr 15, 2026
b6552a9
MWPW-189894: Handling double computing
seanchoi-dev Apr 15, 2026
bf61073
Merge branch 'main' into MWPW-189894
Axelcureno Apr 15, 2026
b37a6ab
Merge branch 'main' into MWPW-189894
seanchoi-dev Apr 16, 2026
9edc248
Merge branch 'main' into MWPW-189894
afmicka Apr 16, 2026
2fe294a
Merge branch 'main' into MWPW-189894
seanchoi-dev Apr 16, 2026
5e3f989
MWPW-189894: unit test updates to reach out 100% code coverage
seanchoi-dev Apr 16, 2026
0d78468
Merge branch 'main' into MWPW-189894
afmicka Apr 20, 2026
c52ea30
Merge branch 'main' into MWPW-189894
afmicka Apr 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion io/www/src/fragment/pipeline.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import zlib from 'zlib';
import stateLib from '@adobe/aio-lib-state';

import { transformer as fetchFragment } from './transformers/fetchFragment.js';
import { transformer as defaultLanguageVariation } from './transformers/defaultLanguageVariation.js';
import { transformer as corrector } from './transformers/corrector.js';
import { transformer as replace } from './transformers/replace.js';
import { transformer as promotions } from './transformers/promotions.js';
Expand All @@ -21,7 +22,7 @@ function calculateHash(body) {
return crypto.createHash('sha256').update(JSON.stringify(body)).digest('hex');
}

const PIPELINE = [fetchFragment, promotions, customize, settings, replace, wcs, corrector];
const PIPELINE = [fetchFragment, defaultLanguageVariation, promotions, customize, settings, replace, wcs, corrector];

const RESPONSE_HEADERS = {
'Access-Control-Expose-Headers': 'X-Request-Id,Etag,Last-Modified,server-timing',
Expand Down
104 changes: 24 additions & 80 deletions io/www/src/fragment/transformers/customize.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { odinReferences, odinUrl } from '../utils/paths.js';
import { fetch, getFragmentId, getRequestInfos } from '../utils/common.js';
import { log, logDebug } from '../utils/log.js';
import { getDefaultLocaleCode, getLocaleCode, getRegionLocales, parseLocaleCode } from '../locales.js';
import { getRequestInfos } from '../utils/common.js';
import { logDebug } from '../utils/log.js';
import { computeRegionLocale, getDefaultLanguageVariation } from './defaultLanguageVariation.js';

const PZN_FOLDER = '/pzn/';

Expand All @@ -14,52 +13,25 @@ function skimFragmentFromReferences(fragment) {
}

/**
* get fragment associated to default language, just returning the body
* @param {*} context
* - 'locale' comes from request parameter, so can be optional
* - 'parsedLocale' is the actual location of the fetched fragment
* @returns null if something wrong, [] if not found, body if found
* Pipeline: await `defaultLanguageVariation` init (after `fetchFragment` phase 1). Fallback: same helpers as that transformer.
* @param {object} [requestInfosCached] - Result of `getRequestInfos(context)` from `customize`; avoids a second call on the fallback path.
*/
async function getDefaultLanguageVariation(context) {
let { body } = context;
const { surface, locale, fragmentPath, preview, parsedLocale } = context;
// if no 'locale' request parameter, serve fragment as is
if (!locale) {
context.defaultLocale = parsedLocale;
return { body, parsedLocale, status: 200 };
async function resolveFragmentInit(context, requestInfosCached) {
if (context?.promises?.defaultLanguageVariation) {
return await context.promises.defaultLanguageVariation;
}
const defaultLocale = getDefaultLocaleCode(surface, locale);
if (!defaultLocale) {
return { status: 400, message: `Default locale not found for requested locale '${locale}'` };
}
if (defaultLocale !== parsedLocale) {
logDebug(() => `Looking for fragment id for ${surface}/${defaultLocale}/${fragmentPath}`, context);
const defaultLocaleIdUrl = odinUrl(surface, { locale: defaultLocale, fragmentPath, preview });
const { id: defaultLocaleId, status, message } = await getFragmentId(context, defaultLocaleIdUrl, 'default-locale-id');
if (status != 200) {
return { status, message };
}
const defaultLocaleUrl = odinReferences(defaultLocaleId, true, preview);
const response = await fetch(defaultLocaleUrl, context, 'default-locale-fragment');
if (response.status != 200 || !response.body) {
/* c8 ignore next */
const message = response.message || 'Error fetching default locale fragment';
/* c8 ignore next */
return { status: response.status || 503, message };
}
({ body } = response);
}
context.defaultLocale = defaultLocale;
return { body, defaultLocale, status: 200 };
}

async function init(context) {
const { body, surface, fragmentPath, parsedLocale } = await getRequestInfos(context);
context = { ...context, surface, fragmentPath, parsedLocale, body };
const { body, surface, fragmentPath, parsedLocale } = requestInfosCached ?? (await getRequestInfos(context));
const variationContext = { ...context, surface, fragmentPath, parsedLocale, body };
if (!surface || !fragmentPath) {
return { status: 400, message: 'Missing surface or fragmentPath' };
}
return await getDefaultLanguageVariation(context);
const result = await getDefaultLanguageVariation(variationContext);
Comment thread
seanchoi-dev marked this conversation as resolved.
Outdated
if (result.status !== 200) {
return result;
}
const defaultLocale = variationContext.defaultLocale;
const regionLocale = computeRegionLocale({ ...variationContext, defaultLocale });
Comment thread
seanchoi-dev marked this conversation as resolved.
Outdated
return { ...result, regionLocale };
}

function deepMerge(...objects) {
Expand Down Expand Up @@ -285,48 +257,19 @@ function customizeTree(root, referencesTree = [], customizeContext) {
return { fragment: customizedRoot, references: customizeContext.references, referencesTree: adaptedTree };
}

/**
* Returns the locale used for regional paths and personalization.
* If the request uses the default locale code but country differs from that locale's default country and maps to a
* known region for that language on the surface, returns that regional code (e.g. fr_FR + CA → fr_CA).
* If the requested locale is already a regional code, it is preserved when no country override applies.
* @param {*} context
* @returns {string}
*/
export function computeRegionLocale(context) {
const { locale, defaultLocale: defaultLocaleCode, surface } = context;
const country = context.country?.toUpperCase();
const [, defaultCountry] = parseLocaleCode(defaultLocaleCode);
const defaultCountryUpper = defaultCountry?.toUpperCase();
const effectiveCountry = country && defaultCountryUpper != null && country !== defaultCountryUpper ? country : null;

let regionLocale = locale;
if (locale !== defaultLocaleCode || effectiveCountry != null) {
const regionObjects = getRegionLocales(surface, defaultLocaleCode, true);
const regionLocaleObject =
effectiveCountry != null ? regionObjects.find((r) => r.country?.toUpperCase() === effectiveCountry) : null;
const mapped = regionLocaleObject ? getLocaleCode(regionLocaleObject) : null;
regionLocale = mapped || locale;
}
logDebug(
() =>
`Computed region locale '${regionLocale}' for requested locale '${locale}' with country '${country}' on surface '${surface}'`,
context,
);
return regionLocale;
}

async function customize(context) {
const { surface } = await getRequestInfos(context);
const { body, defaultLocale, status, message } = await context.promises?.customize;
const requestInfos = await getRequestInfos(context);
const { surface } = requestInfos;
const fragmentInit = await resolveFragmentInit(context, requestInfos);
const { body, defaultLocale, status, message, regionLocale: regionLocaleFromInit } = fragmentInit;
const promos = await context.promises?.promotions;

if (status != 200) {
return { ...context, status, message };
}
const baseFragment = skimFragmentFromReferences(body);
const { references, referencesTree } = body;
const regionLocale = computeRegionLocale({ ...context, defaultLocale, surface });
const regionLocale = context.regionLocale ?? regionLocaleFromInit;
const isRegionLocale = regionLocale !== defaultLocale;
const customizeContext = {
...context,
Expand All @@ -347,12 +290,13 @@ async function customize(context) {
...context,
status: 200,
body: customizedFragment,
locale: regionLocale,
defaultLocale,
Comment thread
seanchoi-dev marked this conversation as resolved.
};
}

export const transformer = {
name: 'customize',
process: customize,
init,
};
export { deepMerge };
137 changes: 137 additions & 0 deletions io/www/src/fragment/transformers/defaultLanguageVariation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { fetch, getFragmentId } from '../utils/common.js';
import { logDebug } from '../utils/log.js';
import { odinReferences, odinUrl } from '../utils/paths.js';
import { getDefaultLocaleCode, getLocaleCode, getRegionLocales, parseLocaleCode } from '../locales.js';

/**
* Resolves the fragment body for the default language of the requested locale.
* On success, sets `context.defaultLocale` (when `locale` is omitted, to `parsedLocale`).
*
* @param {*} context - Must include `surface`, `fragmentPath`, `body`, `parsedLocale`. `locale` is optional (request param).
* @returns {Promise<object>} Success and error shapes:
* - No `locale` param: `{ body, parsedLocale, status: 200 }`. `defaultLocale` is not on the return value; read `context.defaultLocale` (set to `parsedLocale`).
* - Normal success after resolving default language: `{ body, defaultLocale, status: 200 }` (also `context.defaultLocale`).
* - Failure: `{ status, message }` from unknown locale, fragment-id lookup, or default-locale fetch errors.
*/
export async function getDefaultLanguageVariation(context) {
let { body } = context;
const { surface, locale, fragmentPath, preview, parsedLocale } = context;
if (!locale) {
context.defaultLocale = parsedLocale;
return { body, parsedLocale, status: 200 };
}
const defaultLocale = getDefaultLocaleCode(surface, locale);
if (!defaultLocale) {
return { status: 400, message: `Default locale not found for requested locale '${locale}'` };
}
if (defaultLocale !== parsedLocale) {
logDebug(() => `Looking for fragment id for ${surface}/${defaultLocale}/${fragmentPath}`, context);
const defaultLocaleIdUrl = odinUrl(surface, { locale: defaultLocale, fragmentPath, preview });
const { id: defaultLocaleId, status, message } = await getFragmentId(context, defaultLocaleIdUrl, 'default-locale-id');
if (status != 200) {
return { status, message };
}
const defaultLocaleUrl = odinReferences(defaultLocaleId, true, preview);
const response = await fetch(defaultLocaleUrl, context, 'default-locale-fragment');
if (response.status != 200 || !response.body) {
/* c8 ignore next */
const message = response.message || 'Error fetching default locale fragment';
/* c8 ignore next */
return { status: response.status || 503, message };
}
({ body } = response);
}
context.defaultLocale = defaultLocale;
return { body, defaultLocale, status: 200 };
}

/**
* Returns the locale used for regional paths and personalization.
* If the request uses the default locale code but country differs from that locale's default country and maps to a
* known region for that language on the surface, returns that regional code (e.g. fr_FR + CA → fr_CA).
* If the requested locale is already a regional code, it is preserved when no country override applies.
* @param {*} context
* @returns {string}
*/
export function computeRegionLocale(context) {
const { locale, defaultLocale: defaultLocaleCode, surface } = context;
const country = context.country?.toUpperCase();
const [, defaultCountry] = parseLocaleCode(defaultLocaleCode);
const defaultCountryUpper = defaultCountry?.toUpperCase();
const effectiveCountry = country && defaultCountryUpper != null && country !== defaultCountryUpper ? country : null;

let regionLocale = locale;
if (locale !== defaultLocaleCode || effectiveCountry != null) {
const regionObjects = getRegionLocales(surface, defaultLocaleCode, true);
const regionLocaleObject =
effectiveCountry != null ? regionObjects.find((r) => r.country?.toUpperCase() === effectiveCountry) : null;
const mapped = regionLocaleObject ? getLocaleCode(regionLocaleObject) : null;
regionLocale = mapped || locale;
}
logDebug(
() =>
`Computed region locale '${regionLocale}' for requested locale '${locale}' with country '${country}' on surface '${surface}'`,
context,
);
return regionLocale;
}

const TRANSFORMER_NAME = 'defaultLanguageVariation';

/**
* Runs after `fetchFragment` init. Awaits `promises.fetchFragment` (phase 1), then default-language variation +
* `computeRegionLocale`. Result is `promises.defaultLanguageVariation`.
*/
async function init(initContext) {
const early = await initContext.promises?.fetchFragment;
if (!early) {
return { status: 400, message: 'fetchFragment init not available' };
}
if (early.status !== 200) {
return early;
}
const { body: earlyBody, parsedLocale, surface, fragmentPath } = early;
let context = { ...initContext, body: earlyBody, parsedLocale, surface, fragmentPath };
const variationResult = await getDefaultLanguageVariation(context);
/* c8 ignore next 3 — default-locale fetch errors covered via pipeline / customize tests */
if (variationResult.status != 200) {
return variationResult;
}
context = { ...context, body: variationResult.body };
const defaultLocale = context.defaultLocale;
const regionLocale = computeRegionLocale({ ...context, defaultLocale });
return {
...initContext,
status: 200,
body: variationResult.body,
parsedLocale,
surface,
fragmentPath,
defaultLocale,
locale: regionLocale,
regionLocale,
};
}

async function defaultLanguageVariation(context) {
const response = await context.promises?.[TRANSFORMER_NAME];
if (response?.status !== 200) {
return response;
}
return {
...context,
body: response.body,
parsedLocale: response.parsedLocale,
surface: response.surface,
fragmentPath: response.fragmentPath,
defaultLocale: response.defaultLocale,
locale: response.locale,
regionLocale: response.regionLocale,
};
}

export const transformer = {
name: TRANSFORMER_NAME,
init,
process: defaultLanguageVariation,
};
73 changes: 44 additions & 29 deletions io/www/src/fragment/transformers/fetchFragment.js
Original file line number Diff line number Diff line change
@@ -1,41 +1,53 @@
import { fetch } from '../utils/common.js';
import { getErrorContext } from '../utils/log.js';
import { PATH_TOKENS, odinReferences } from '../utils/paths.js';

const TRANSFORMER_NAME = 'fetchFragment';

async function init(initContext) {
/**
* First fragment fetch + path parse only. Resolves as soon as surface / parsedLocale / fragmentPath / body are known,
* without waiting on default-locale variation fetch. Shared via `promises.requestInfos` so dictionary/settings inits
* can proceed in parallel with that work.
*/
async function fetchRequestInfosPhase1(initContext) {
const { id, locale, fragmentsIds, preview } = initContext;
if (id && locale) {
//either we have a default locale id that we can fetch directly or we use the id
const toFetchId = fragmentsIds?.['default-locale-id'] || id;
const path = odinReferences(toFetchId, true, preview);
const response = await fetch(path, initContext, 'fragment');
if (response?.status != 200) {
return await getErrorContext(response);
}
const match = response?.body?.path?.match(PATH_TOKENS);
if (!match) {
return {
status: 400,
message: 'source path is either not here or invalid',
};
}

const { parsedLocale, surface, fragmentPath } = match.groups;
return {
...initContext,
status: 200,
body: response.body,
parsedLocale,
surface,
fragmentPath,
};
} else {
if (!(id && locale)) {
return { status: 400, message: 'requested parameters id & locale are not present' };
}
const toFetchId = fragmentsIds?.['default-locale-id'] || id;
const path = odinReferences(toFetchId, true, preview);
const response = await fetch(path, initContext, 'fragment');
if (response?.status != 200) {
return await getErrorContext(response);
}
const match = response?.body?.path?.match(PATH_TOKENS);
if (!match) {
return {
status: 400,
message: 'requested parameters id & locale are not present',
message: 'source path is either not here or invalid',
};
}
const { parsedLocale, surface, fragmentPath } = match.groups;
return {
status: 200,
body: response.body,
parsedLocale,
surface,
fragmentPath,
};
}

/**
* Phase 1 only: first fragment fetch + path parse. Result is `promises.fetchFragment` (and `promises.requestInfos`).
* Default-language variation + region locale run in the `defaultLanguageVariation` transformer (before promotions).
*/
function init(initContext) {
const { promises } = initContext;
const phase1Promise = fetchRequestInfosPhase1(initContext);
if (promises) {
promises.requestInfos = phase1Promise;
}
return phase1Promise;
}

async function fetchFragment(context) {
Expand All @@ -45,7 +57,10 @@ async function fetchFragment(context) {
}
return {
...context,
body: response?.body,
body: response.body,
parsedLocale: response.parsedLocale,
surface: response.surface,
fragmentPath: response.fragmentPath,
};
}

Expand Down
Loading
Loading