Skip to content

Commit 11d00da

Browse files
author
vmarta_sfemu
committed
Sync from monorepo
Template version: 0.4.0-alpha.2 Uses NPM packages @salesforce/storefront-next-* v0.4.0-alpha.2 Synced by: vmarta_sfemu Monorepo SHA: 1b51dbc9f8c9b68bc44b8fb5e2a801f51e858fb2 Latest change: 1b51dbc9f - fix: locale bundle bloat + branded error pages on direct navigation (@W-22261594@) (#1555)
1 parent 8bde996 commit 11d00da

10 files changed

Lines changed: 307 additions & 125 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
- Update Page Designer middleware and `vite.config.ts` to align with upstream `@salesforce/mrt-utilities` conditional export flow (no local Vite alias requirement) ([#1533](https://github.com/commerce-emu/storefront-next/pull/1533))
2626
- Standardize behaviors of API errors in Checkout (@W-22199926) ([#1521](https://github.com/commerce-emu/storefront-next/pull/1521))
2727
- Migrate i18n infrastructure to SDK: replace `src/lib/i18next.ts`, `src/lib/i18next.client.ts`, and `scripts/aggregate-extension-locales.js` with imports from `@salesforce/storefront-next-runtime/i18n` and `sfnext locales aggregate-extensions`
28+
- Fix locale bundle bloat, SSR 404 plain-text response, and homepage links ignoring active locale on error pages
2829

2930
## v0.4.0-dev (Apr 15, 2026)
3031

lighthouserc.cjs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ module.exports = {
6060
'categories:best-practices': ['error', { minScore: 0.96, aggregationMethod: 'median' }],
6161
'resource-summary:script:size': [
6262
'error',
63-
{ maxNumericValue: 440000, aggregationMethod: 'median' },
63+
{ maxNumericValue: 391000, aggregationMethod: 'median' },
6464
],
6565
'resource-summary:document:size': [
6666
'error',
@@ -100,7 +100,7 @@ module.exports = {
100100
'categories:best-practices': ['error', { minScore: 0.96, aggregationMethod: 'median' }],
101101
'resource-summary:script:size': [
102102
'error',
103-
{ maxNumericValue: 490000, aggregationMethod: 'median' },
103+
{ maxNumericValue: 438000, aggregationMethod: 'median' },
104104
],
105105
'resource-summary:document:size': [
106106
'error',
@@ -120,7 +120,7 @@ module.exports = {
120120
'categories:best-practices': ['error', { minScore: 0.96, aggregationMethod: 'median' }],
121121
'resource-summary:script:size': [
122122
'error',
123-
{ maxNumericValue: 460000, aggregationMethod: 'median' },
123+
{ maxNumericValue: 415000, aggregationMethod: 'median' },
124124
],
125125
'resource-summary:document:size': [
126126
'error',

src/locales/en-GB/translations.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1521,7 +1521,7 @@
15211521
"checkBack": "Check back in a few minutes, or follow us for updates.",
15221522
"tryAgain": "Try Again"
15231523
},
1524-
"error": {
1524+
"routeError": {
15251525
"defaultTitle": "Something went wrong",
15261526
"defaultDescription": "An error occurred",
15271527
"defaultDetails": "An unexpected error occurred.",

src/locales/en-US/translations.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1509,7 +1509,7 @@
15091509
"checkBack": "Check back in a few minutes, or follow us for updates.",
15101510
"tryAgain": "Try Again"
15111511
},
1512-
"error": {
1512+
"routeError": {
15131513
"defaultTitle": "Something went wrong",
15141514
"defaultDescription": "An error occurred",
15151515
"defaultDetails": "An unexpected error occurred.",

src/locales/it-IT/translations.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1472,7 +1472,7 @@
14721472
"checkBack": "Ricontrolla tra qualche minuto o seguici per aggiornamenti.",
14731473
"tryAgain": "Riprova"
14741474
},
1475-
"error": {
1475+
"routeError": {
14761476
"403": {
14771477
"title": "Accesso limitato",
14781478
"message": "Non hai il permesso di visualizzare questa pagina. Se ritieni che questo sia un errore, contatta il nostro team di supporto.",

src/locales/locale-imports.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* Copyright 2026 Salesforce, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
import { describe, it, expect } from 'vitest';
17+
import { readFileSync } from 'fs';
18+
import { resolve } from 'path';
19+
20+
describe('locale-imports regression guard', () => {
21+
it('root.tsx should not statically import the all-locales barrel @/locales', () => {
22+
const rootPath = resolve(__dirname, '../root.tsx');
23+
const content = readFileSync(rootPath, 'utf-8');
24+
25+
// Static import of @/locales pulls ALL locale JSON into the client bundle.
26+
// ErrorBoundary uses errorTranslations from the root loader instead.
27+
const hasStaticLocaleImport = /^\s*import\s+\S+\s+from\s+['"]@\/locales['"]/m.test(content);
28+
expect(hasStaticLocaleImport).toBe(false);
29+
});
30+
});

src/root.test.tsx

Lines changed: 80 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@ const defaultClientAuth: PublicSessionData = {
3232
userType: 'registered',
3333
};
3434
import { mockConfig, mockBuildConfig } from '@/test-utils/config';
35+
// ErrorBoundary creates its own isolated i18next instance from errorTranslations —
36+
// it does not use the global i18next singleton, so getTranslation() would return key strings here.
37+
// Importing the JSON directly is the correct source of truth for the translated path assertion.
38+
// The fallback path (no loader data) renders hardcoded inline English strings — no JSON import needed.
39+
import itITTranslations from '@/locales/it-IT/translations.json';
40+
const itITRouteError = itITTranslations.routeError;
41+
import enGBTranslations from '@/locales/en-GB/translations.json';
42+
const enGBRouteError = enGBTranslations.routeError;
3543

3644
const mockSite = {
3745
...mockBuildConfig.app.commerce.sites[0],
@@ -46,42 +54,11 @@ vi.mock('@salesforce/storefront-next-runtime/i18n/client', async () => {
4654
// (no resources pre-loaded, uses backend to fetch translations)
4755
const testInstance = i18next.default.createInstance();
4856

49-
// Mock the backend to return test translations
5057
const mockBackend = {
5158
type: 'backend' as const,
5259
init: vi.fn(),
53-
read: vi.fn((_language: string, namespace: string, callback: (error: any, data: any) => void) => {
54-
// Return test translations for error namespace
55-
if (namespace === 'error') {
56-
callback(null, {
57-
defaultTitle: 'Something went wrong',
58-
goToHomepage: 'Go to Homepage',
59-
allRightsReserved: 'All rights reserved.',
60-
'404': {
61-
title: 'Page not found',
62-
message:
63-
"We couldn't find the page you're looking for. It may have been moved or the link might be incorrect.",
64-
secondaryMessage: "Don't worry—you can still explore our collection or head back home.",
65-
details: 'The requested page could not be found.',
66-
},
67-
'403': {
68-
title: 'Access restricted',
69-
message:
70-
"You don't have permission to view this page. If you believe this is an error, please contact our support team.",
71-
secondaryMessage: 'In the meantime, feel free to browse our collection.',
72-
},
73-
'500': {
74-
title: 'Something went wrong',
75-
message:
76-
"We're sorry, but something unexpected happened on our end. Our team has been notified and is working to fix it.",
77-
secondaryMessage:
78-
'Please try again in a few moments, or browse our shop while we sort things out.',
79-
},
80-
});
81-
} else {
82-
// Return empty translations for other namespaces
83-
callback(null, {});
84-
}
60+
read: vi.fn((_language: string, _namespace: string, callback: (error: any, data: any) => void) => {
61+
callback(null, {});
8562
}),
8663
};
8764

@@ -162,6 +139,22 @@ vi.mock('@salesforce/storefront-next-runtime/design/react/core', async (importOr
162139
};
163140
});
164141

142+
vi.mock('react-router', async (importOriginal) => {
143+
const actual = await importOriginal<typeof import('react-router')>();
144+
const realUseRouteLoaderData = actual.useRouteLoaderData;
145+
return {
146+
...actual,
147+
// ErrorBoundary tests render outside a router context; swallow the invariant and return null.
148+
useRouteLoaderData: vi.fn((routeId: string) => {
149+
try {
150+
return realUseRouteLoaderData(routeId);
151+
} catch {
152+
return null;
153+
}
154+
}),
155+
};
156+
});
157+
165158
vi.mock('@/middlewares/basket.server', async () => ({
166159
...(await vi.importActual('@/middlewares/basket.server')),
167160
default: vi.fn(),
@@ -305,6 +298,16 @@ describe('root.tsx', () => {
305298
const stackText = 'Error: Test error with stack';
306299

307300
describe('development mode', () => {
301+
beforeEach(async () => {
302+
const reactRouter = await import('react-router');
303+
vi.mocked(reactRouter.useRouteLoaderData).mockReturnValue({
304+
errorTranslations: enGBRouteError,
305+
appConfig: { i18n: { fallbackLng: 'en-GB' } },
306+
locale: { id: 'en-GB' },
307+
site: mockSite,
308+
} as any);
309+
});
310+
308311
it('should render normal error with message', () => {
309312
const error = new Error('Test error');
310313
error.stack = stackText;
@@ -359,7 +362,7 @@ describe('root.tsx', () => {
359362

360363
expect(getByText('500')).toBeInTheDocument();
361364
expect(getByText('Something went wrong')).toBeInTheDocument();
362-
// 500 errors now show friendly translated message instead of statusText
365+
// 500 errors show friendly translated message instead of statusText
363366
expect(getByText(/We're sorry, but something unexpected happened on our end/)).toBeInTheDocument();
364367
expect(container.querySelector('pre')).not.toBeInTheDocument();
365368
expect(container.querySelector('code')).not.toBeInTheDocument();
@@ -369,9 +372,16 @@ describe('root.tsx', () => {
369372
describe('production mode', () => {
370373
let originalEnv = import.meta.env.DEV;
371374

372-
beforeEach(() => {
375+
beforeEach(async () => {
373376
originalEnv = import.meta.env.DEV;
374377
import.meta.env.DEV = false;
378+
const reactRouter = await import('react-router');
379+
vi.mocked(reactRouter.useRouteLoaderData).mockReturnValue({
380+
errorTranslations: enGBRouteError,
381+
appConfig: { i18n: { fallbackLng: 'en-GB' } },
382+
locale: { id: 'en-GB' },
383+
site: mockSite,
384+
} as any);
375385
});
376386

377387
afterEach(() => {
@@ -421,12 +431,46 @@ describe('root.tsx', () => {
421431

422432
expect(getByText('500')).toBeInTheDocument();
423433
expect(getByText('Something went wrong')).toBeInTheDocument();
424-
// 500 errors now show friendly translated message instead of statusText
434+
// 500 errors show friendly translated message instead of statusText
425435
expect(getByText(/We're sorry, but something unexpected happened on our end/)).toBeInTheDocument();
426436
expect(container.querySelector('pre')).not.toBeInTheDocument();
427437
expect(container.querySelector('code')).not.toBeInTheDocument();
428438
});
429439
});
440+
441+
describe('translation source', () => {
442+
afterEach(async () => {
443+
// Reset so the mock's try/catch default behaviour is restored for other tests.
444+
const reactRouter = await import('react-router');
445+
vi.mocked(reactRouter.useRouteLoaderData).mockRestore();
446+
});
447+
448+
it('should use errorTranslations from rootData when available', async () => {
449+
const reactRouter = await import('react-router');
450+
vi.mocked(reactRouter.useRouteLoaderData).mockReturnValue({
451+
errorTranslations: itITRouteError,
452+
appConfig: { i18n: { fallbackLng: 'en-GB' } },
453+
locale: { id: 'it-IT' },
454+
site: mockSite,
455+
} as any);
456+
457+
const error = { status: 404, statusText: 'Not Found', data: {}, internal: false };
458+
const { getByText } = render(<ErrorBoundary error={error} />);
459+
460+
expect(getByText('404')).toBeInTheDocument();
461+
expect(getByText(itITRouteError['404'].title)).toBeInTheDocument();
462+
});
463+
464+
it('should render hardcoded English fallback when no errorTranslations are available', () => {
465+
// useRouteLoaderData returns null (outside router context — default mock behaviour).
466+
// ErrorBoundary skips i18next entirely and renders hardcoded inline English strings.
467+
const error = { status: 404, statusText: 'Not Found', data: {}, internal: false };
468+
const { getByText } = render(<ErrorBoundary error={error} />);
469+
470+
expect(getByText('404')).toBeInTheDocument();
471+
expect(getByText('Page not found')).toBeInTheDocument();
472+
});
473+
});
430474
});
431475

432476
describe('App Component', () => {

0 commit comments

Comments
 (0)