Skip to content

Commit 7985e42

Browse files
committed
fix(locale): Only use the users preferred locale to check for cms customization
1 parent b6b8f44 commit 7985e42

3 files changed

Lines changed: 146 additions & 66 deletions

File tree

packages/fxa-settings/src/lib/config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ export interface Config {
101101
nimbusPreview: boolean;
102102
cms: {
103103
enabled: boolean;
104+
l10nEnabled: boolean;
104105
};
105106
}
106107

@@ -185,8 +186,8 @@ export function getDefault() {
185186
showLocaleToggle: false,
186187
},
187188
cms: {
188-
// Note: Even if this flag is true, the user must be an `en` language
189189
enabled: false,
190+
l10nEnabled: false
190191
},
191192
nimbusPreview: false,
192193
} as Config;

packages/fxa-settings/src/models/hooks.test.ts

Lines changed: 127 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import { renderHook, waitFor } from '@testing-library/react';
77
import { ReactNode, createElement } from 'react';
88
import * as Sentry from '@sentry/browser';
99

10-
import { useCmsInfoState } from './hooks';
10+
import {
11+
useCmsInfoState
12+
} from './hooks';
1113
import { AppContext } from './contexts/AppContext';
1214

1315
// Mock all external dependencies before importing the hook
@@ -45,6 +47,10 @@ jest.mock('../lib/integrations', () => ({
4547

4648
jest.mock('../lib/nimbus', () => ({}));
4749

50+
jest.mock('../contexts/DynamicLocalizationContext', () => ({
51+
useDynamicLocalization: jest.fn().mockReturnValue({ currentLocale: 'en-US' }),
52+
}));
53+
4854
// Mock fetch
4955
const mockFetch = jest.fn();
5056
global.fetch = mockFetch;
@@ -93,6 +99,17 @@ describe('useCmsInfoState', () => {
9399
ok: true,
94100
json: jest.fn().mockResolvedValue({ customization: 'test' }),
95101
});
102+
103+
// Reset navigator.language and useDynamicLocalization to English defaults
104+
Object.defineProperty(navigator, 'language', {
105+
writable: true,
106+
value: 'en-US',
107+
});
108+
Object.defineProperty(navigator, 'languages', {
109+
writable: true,
110+
value: ['en-US', 'en'],
111+
});
112+
require('../contexts/DynamicLocalizationContext').useDynamicLocalization.mockReturnValue({ currentLocale: 'en-US' });
96113
});
97114

98115
it('should use default entrypoint when entrypoint is not provided in URL', async () => {
@@ -239,6 +256,9 @@ describe('useCmsInfoState', () => {
239256
value: ['fr-FR', 'fr'],
240257
});
241258

259+
// Mock useDynamicLocalization to return non-English locale
260+
require('../contexts/DynamicLocalizationContext').useDynamicLocalization.mockReturnValue({ currentLocale: 'fr-FR' });
261+
242262
mockUrlQueryData.get.mockImplementation((key: string) => {
243263
if (key === 'client_id') return '1234567890abcdef';
244264
if (key === 'entrypoint') return 'preferences';
@@ -249,21 +269,15 @@ describe('useCmsInfoState', () => {
249269
wrapper: MockAppProvider,
250270
});
251271

272+
// Wait for the effect to complete
273+
await waitFor(() => {
274+
expect(result.current.loading).toBe(false);
275+
});
276+
252277
// Should not fetch due to non-English locale
253278
expect(mockFetch).not.toHaveBeenCalled();
254-
expect(result.current.loading).toBe(false);
255279
expect(result.current.data).toBeUndefined();
256280
expect(result.current.error).toBeUndefined();
257-
258-
// Reset to English for other tests
259-
Object.defineProperty(navigator, 'language', {
260-
writable: true,
261-
value: 'en-US',
262-
});
263-
Object.defineProperty(navigator, 'languages', {
264-
writable: true,
265-
value: ['en-US', 'en'],
266-
});
267281
});
268282

269283
it('should handle fetch errors gracefully', async () => {
@@ -354,47 +368,106 @@ describe('useCmsInfoState', () => {
354368
);
355369
});
356370

357-
it('should handle various custom entrypoint values', async () => {
358-
const testCases = [
359-
'firefox-toolbar',
360-
'fxa_app_menu',
361-
'ios_settings_manage',
362-
'synced-tabs',
363-
'custom.entrypoint-123'
364-
];
365-
366-
for (const entrypoint of testCases) {
367-
jest.clearAllMocks();
368-
369-
mockUrlQueryData.get.mockImplementation((key: string) => {
370-
if (key === 'client_id') return '1234567890abcdef';
371-
if (key === 'entrypoint') return entrypoint;
372-
return null;
373-
});
374-
375-
mockFetch.mockResolvedValue({
376-
ok: true,
377-
json: jest.fn().mockResolvedValue({ customization: `test-${entrypoint}` }),
378-
});
379-
380-
const { result } = renderHook(() => useCmsInfoState(), {
381-
wrapper: MockAppProvider,
382-
});
383-
384-
// Wait for the fetch to complete
385-
await waitFor(() => {
386-
expect(result.current.loading).toBe(false);
387-
});
388-
389-
// Verify that fetch was called with the correct entrypoint
390-
expect(mockFetch).toHaveBeenCalledWith(
391-
expect.objectContaining({
392-
href: `http://localhost:9000/v1/cms/config?clientId=1234567890abcdef&entrypoint=${entrypoint}`,
393-
}),
394-
expect.any(Object)
395-
);
396-
397-
expect(result.current.data?.cmsInfo).toEqual({ customization: `test-${entrypoint}` });
398-
}
371+
it('should fetch when l10nEnabled is true for any locale', async () => {
372+
const l10nEnabledConfig = {
373+
...mockConfig,
374+
cms: { enabled: true, l10nEnabled: true },
375+
};
376+
377+
const L10nEnabledProvider = ({ children }: { children: ReactNode }) =>
378+
createElement(AppContext.Provider, {
379+
value: { config: l10nEnabledConfig as any, account: undefined, apolloClient: undefined }
380+
}, children);
381+
382+
// Mock non-English locale
383+
require('../contexts/DynamicLocalizationContext').useDynamicLocalization.mockReturnValue({ currentLocale: 'fr-FR' });
384+
Object.defineProperty(navigator, 'language', {
385+
writable: true,
386+
value: 'fr-FR',
387+
});
388+
389+
mockUrlQueryData.get.mockImplementation((key: string) => {
390+
if (key === 'client_id') return '1234567890abcdef';
391+
if (key === 'entrypoint') return 'preferences';
392+
return null;
393+
});
394+
395+
mockFetch.mockResolvedValue({
396+
ok: true,
397+
json: jest.fn().mockResolvedValue({ customization: 'french-test' }),
398+
});
399+
400+
const { result } = renderHook(() => useCmsInfoState(), {
401+
wrapper: L10nEnabledProvider,
402+
});
403+
404+
await waitFor(() => {
405+
expect(result.current.loading).toBe(false);
406+
});
407+
408+
expect(mockFetch).toHaveBeenCalled();
409+
expect(result.current.data?.cmsInfo).toEqual({ customization: 'french-test' });
410+
});
411+
412+
it('should fetch when l10nEnabled is false but browser language is English', async () => {
413+
// Mock English browser language but non-English selected locale
414+
Object.defineProperty(navigator, 'language', {
415+
writable: true,
416+
value: 'en-GB',
417+
});
418+
require('../contexts/DynamicLocalizationContext').useDynamicLocalization.mockReturnValue({ currentLocale: 'fr-FR' });
419+
420+
mockUrlQueryData.get.mockImplementation((key: string) => {
421+
if (key === 'client_id') return '1234567890abcdef';
422+
if (key === 'entrypoint') return 'preferences';
423+
return null;
424+
});
425+
426+
mockFetch.mockResolvedValue({
427+
ok: true,
428+
json: jest.fn().mockResolvedValue({ customization: 'english-browser-test' }),
429+
});
430+
431+
const { result } = renderHook(() => useCmsInfoState(), {
432+
wrapper: MockAppProvider,
433+
});
434+
435+
await waitFor(() => {
436+
expect(result.current.loading).toBe(false);
437+
});
438+
439+
expect(mockFetch).toHaveBeenCalled();
440+
expect(result.current.data?.cmsInfo).toEqual({ customization: 'english-browser-test' });
441+
});
442+
443+
it('should fetch when l10nEnabled is false but selected locale is English', async () => {
444+
// Mock non-English browser language but English selected locale
445+
Object.defineProperty(navigator, 'language', {
446+
writable: true,
447+
value: 'fr-FR',
448+
});
449+
require('../contexts/DynamicLocalizationContext').useDynamicLocalization.mockReturnValue({ currentLocale: 'en-US' });
450+
451+
mockUrlQueryData.get.mockImplementation((key: string) => {
452+
if (key === 'client_id') return '1234567890abcdef';
453+
if (key === 'entrypoint') return 'preferences';
454+
return null;
455+
});
456+
457+
mockFetch.mockResolvedValue({
458+
ok: true,
459+
json: jest.fn().mockResolvedValue({ customization: 'english-selected-test' }),
460+
});
461+
462+
const { result } = renderHook(() => useCmsInfoState(), {
463+
wrapper: MockAppProvider,
464+
});
465+
466+
await waitFor(() => {
467+
expect(result.current.loading).toBe(false);
468+
});
469+
470+
expect(mockFetch).toHaveBeenCalled();
471+
expect(result.current.data?.cmsInfo).toEqual({ customization: 'english-selected-test' });
399472
});
400473
});

packages/fxa-settings/src/models/hooks.ts

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -230,23 +230,29 @@ export function useCmsInfoState() {
230230
useEffect(() => {
231231
// We disable the CMS if:
232232
// 1. CMS is not enabled in the config
233-
// 2. The user's locale is not English
234-
// 3. The clientId is not provided or is not a valid 16 digit hex
235-
236-
function isEnglishLocale() {
237-
// Check primary language
238-
const primaryLanguage = navigator.language;
239-
if (primaryLanguage.startsWith('en')) {
233+
// 2. l10nEnabled is false AND user's locale is not English
234+
// 3. User language toggle is not English
235+
// 4. The clientId is not provided or is not a valid 16 digit hex
236+
//
237+
// These steps are a best effort to prevent users from seeing a page with both English and non-English text.
238+
239+
function shouldFetchCms() {
240+
// If l10nEnabled is true, fetch cms config for any locale
241+
if (config?.cms?.l10nEnabled === true) {
240242
return true;
241243
}
242244

243-
// Check preferred languages
244-
return navigator.languages.some(lang => lang.startsWith('en'));
245+
// If l10nEnabled is false, only fetch for English locales
246+
// Check both browser language and user's selected locale
247+
const isEnglishBrowser = navigator.language.startsWith('en');
248+
const isEnglishSelected = currentLocale.startsWith('en');
249+
250+
return isEnglishBrowser || isEnglishSelected;
245251
}
246252

247253
if (
248254
!config.cms.enabled ||
249-
!isEnglishLocale() ||
255+
!shouldFetchCms() ||
250256
!clientId ||
251257
!isHexadecimal(clientId) ||
252258
!length(clientId, 16) ||
@@ -306,7 +312,7 @@ export function useCmsInfoState() {
306312
return () => {
307313
mounted = false;
308314
};
309-
}, [authUrl, clientId, entrypoint, config.cms.enabled, currentLocale]);
315+
}, [authUrl, clientId, entrypoint, config.cms.enabled, config.cms?.l10nEnabled, currentLocale]);
310316

311317
return state;
312318
}

0 commit comments

Comments
 (0)