Skip to content

Commit b6b8f44

Browse files
authored
Merge pull request #19320 from mozilla/fxa-12273
feat(l10n): Implement dynamic localization context and language switcher
2 parents ba37533 + 818cef1 commit b6b8f44

24 files changed

Lines changed: 685 additions & 35 deletions

File tree

packages/fxa-content-server/server/config/local.json-dist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
"recoveryPhonePasswordReset2fa": true,
7272
"updatedInlineTotpSetupFlow": true,
7373
"updatedInlineRecoverySetupFlow": true
74+
"showLocaleToggle": true
7475
},
7576
"rolloutRates": {
7677
"keyStretchV2": 1,

packages/fxa-content-server/server/lib/beta-settings.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ const settingsConfig = {
120120
updatedInlineRecoverySetupFlow: config.get(
121121
'featureFlags.updatedInlineRecoverySetupFlow'
122122
),
123+
showLocaleToggle: config.get('featureFlags.showLocaleToggle'),
123124
},
124125
nimbusPreview: config.get('nimbusPreview'),
125126
cms: {

packages/fxa-content-server/server/lib/configuration.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,12 @@ const conf = (module.exports = convict({
265265
format: Boolean,
266266
env: 'FEATURE_FLAGS_UPDATED_INLINE_RECOVERY_SETUP_FLOW',
267267
},
268+
showLocaleToggle: {
269+
default: false,
270+
doc: 'Enables the locale toggle in the footer',
271+
format: Boolean,
272+
env: 'FEATURE_FLAGS_SHOW_LOCALE_TOGGLE',
273+
},
268274
},
269275
cms: {
270276
enabled: {

packages/fxa-content-server/server/lib/routes/react-app/route-definition-index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ function getIndexRouteDefinition(config) {
6464
const FEATURE_FLAGS_UPDATED_INLINE_RECOVERY_SETUP_FLOW = config.get(
6565
'featureFlags.updatedInlineRecoverySetupFlow'
6666
);
67+
const FEATURE_FLAGS_SHOW_LOCALE_TOGGLE = config.get(
68+
'featureFlags.showLocaleToggle'
69+
);
6770
const NIMBUS_PREVIEW = config.get('nimbusPreview');
6871
const GLEAN_ENABLED = config.get('glean.enabled');
6972
const GLEAN_APPLICATION_ID = config.get('glean.applicationId');
@@ -131,6 +134,7 @@ function getIndexRouteDefinition(config) {
131134
updated2faSetupFlow: FEATURE_FLAGS_UPDATED_2FA_SETUP_FLOW,
132135
updatedInlineRecoverySetupFlow:
133136
FEATURE_FLAGS_UPDATED_INLINE_RECOVERY_SETUP_FLOW,
137+
showLocaleToggle: FEATURE_FLAGS_SHOW_LOCALE_TOGGLE,
134138
},
135139
cms: {
136140
enabled: CMS_ENABLED,

packages/fxa-react/components/Footer/index.test.tsx

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import Footer from '.';
88
import { renderWithLocalizationProvider } from '../../lib/test-utils/localizationProvider';
99

1010
describe('Footer', () => {
11-
it('renders as expected', () => {
12-
renderWithLocalizationProvider(<Footer />);
11+
it('renders as expected without LocaleToggle', () => {
12+
renderWithLocalizationProvider(<Footer showLocaleToggle={false} />);
1313

1414
const linkMozilla = screen.getByTestId('link-mozilla');
1515

@@ -29,5 +29,39 @@ describe('Footer', () => {
2929
'href',
3030
'https://www.mozilla.org/about/legal/terms/services/'
3131
);
32+
33+
// Check that LocaleToggle placeholder is NOT rendered when showLocaleToggle is false
34+
expect(screen.queryByTestId('locale-toggle-placeholder')).not.toBeInTheDocument();
35+
});
36+
37+
it('renders LocaleToggle when showLocaleToggle is true and localeToggleComponent is provided', () => {
38+
const MockLocaleToggle = ({ placement }: { placement?: 'footer' | 'header' }) => (
39+
<div data-testid="locale-toggle" data-placement={placement}>
40+
Mock LocaleToggle
41+
</div>
42+
);
43+
44+
renderWithLocalizationProvider(
45+
<Footer showLocaleToggle={true} localeToggleComponent={MockLocaleToggle} />
46+
);
47+
48+
// Check that LocaleToggle is rendered in the footer
49+
const localeToggle = screen.getByTestId('locale-toggle');
50+
expect(localeToggle).toBeInTheDocument();
51+
expect(localeToggle).toHaveAttribute('data-placement', 'footer');
52+
});
53+
54+
it('does not render LocaleToggle when showLocaleToggle is true but no localeToggleComponent is provided', () => {
55+
renderWithLocalizationProvider(<Footer showLocaleToggle={true} />);
56+
57+
// Check that nothing is rendered when no component is provided
58+
expect(screen.queryByTestId('locale-toggle')).not.toBeInTheDocument();
59+
});
60+
61+
it('renders without LocaleToggle by default', () => {
62+
renderWithLocalizationProvider(<Footer />);
63+
64+
// Check that LocaleToggle placeholder is NOT rendered by default
65+
expect(screen.queryByTestId('locale-toggle-placeholder')).not.toBeInTheDocument();
3266
});
3367
});

packages/fxa-react/components/Footer/index.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,13 @@ import { Localized, useLocalization } from '@fluent/react';
77
import LinkExternal from '../LinkExternal';
88
import mozLogo from '@fxa/shared/assets/images/moz-logo-bw-rgb.svg';
99

10-
export const Footer = () => {
10+
export const Footer = ({
11+
showLocaleToggle = false,
12+
localeToggleComponent
13+
}: {
14+
showLocaleToggle?: boolean;
15+
localeToggleComponent?: React.ComponentType<{ placement?: 'footer' | 'header' }>;
16+
}) => {
1117
const { l10n } = useLocalization();
1218
return (
1319
<footer
@@ -51,6 +57,11 @@ export const Footer = () => {
5157
</LinkExternal>
5258
</Localized>
5359
</div>
60+
{showLocaleToggle && localeToggleComponent && (
61+
<div className="w-full mobileLandscape:w-auto flex items-center mt-3 mobileLandscape:mt-0 mobileLandscape:ml-10">
62+
{React.createElement(localeToggleComponent, { placement: 'footer' })}
63+
</div>
64+
)}
5465
</footer>
5566
);
5667
};

packages/fxa-settings/src/components/AppLayout/index.test.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,18 @@ import {
1919
} from './mocks';
2020
import { RelierCmsInfo } from '../../models';
2121

22+
// Mock the useConfig hook
23+
jest.mock('../../models/hooks', () => ({
24+
useConfig: jest.fn(() => ({
25+
featureFlags: {
26+
showLocaleToggle: false
27+
}
28+
})),
29+
useFtlMsgResolver: () => ({
30+
getMsg: (id: string, fallback: string) => fallback
31+
})
32+
}));
33+
2234
describe('<AppLayout />', () => {
2335
it('renders as expected with children', async () => {
2436
renderWithLocalizationProvider(
@@ -275,6 +287,26 @@ describe('<AppLayout />', () => {
275287
expect(screen.getAllByRole('img')).toHaveLength(1);
276288
});
277289

290+
describe('LocaleToggle visibility', () => {
291+
it('shows LocaleToggle when feature flag enabled', () => {
292+
// Tried to not use require here but it was not working
293+
const { useConfig } = require('../../models/hooks');
294+
useConfig.mockReturnValue({
295+
featureFlags: {
296+
showLocaleToggle: true
297+
}
298+
});
299+
300+
renderWithLocalizationProvider(
301+
<AppLayout>
302+
<p>Hello, world!</p>
303+
</AppLayout>
304+
);
305+
306+
expect(screen.getByTestId('locale-select')).toBeInTheDocument();
307+
});
308+
});
309+
278310
describe('snapshots', () => {
279311
it('renders correctly with CMS', () => {
280312
const mockCmsInfo: RelierCmsInfo = {

packages/fxa-settings/src/components/AppLayout/index.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import { useLocalization } from '@fluent/react';
99
import Head from 'fxa-react/components/Head';
1010
import classNames from 'classnames';
1111
import { RelierCmsInfo } from '../../models/integrations';
12+
import { LocaleToggle } from '../LocaleToggle';
13+
import { useConfig } from '../../models/hooks';
1214

1315
type AppLayoutProps = {
1416
// TODO: FXA-6803 - the title prop should be made mandatory
@@ -25,6 +27,8 @@ type AppLayoutProps = {
2527
* `FlowContainer`.
2628
*/
2729
wrapInCard?: boolean;
30+
/** Whether to show the locale toggle in the footer */
31+
showLocaleToggle?: boolean;
2832
};
2933

3034
const looseValidBgCheck = (value: string | undefined) => {
@@ -45,6 +49,7 @@ export const AppLayout = ({
4549
wrapInCard = true,
4650
}: AppLayoutProps) => {
4751
const { l10n } = useLocalization();
52+
const config = useConfig();
4853
const cmsBackgroundColor = cmsInfo?.shared?.backgroundColor;
4954
const cmsHeaderBackground = cmsInfo?.shared?.headerBackground;
5055
const cmsPageTitle = cmsInfo?.shared?.pageTitle;
@@ -58,6 +63,8 @@ export const AppLayout = ({
5863

5964
const favicon = cmsInfo?.shared?.favicon;
6065

66+
const showLocaleToggle =config.featureFlags?.showLocaleToggle;
67+
6168
return (
6269
<>
6370
<Head {...{ title: overrideTitle, favicon }} />
@@ -116,7 +123,7 @@ export const AppLayout = ({
116123
</LinkExternal>
117124
</header>
118125
<main className="mobileLandscape:flex mobileLandscape:items-center mobileLandscape:flex-1">
119-
<section>
126+
<section className="relative">
120127
{wrapInCard ? (
121128
<div className={classNames('card', widthClass)}>{children}</div>
122129
) : (
@@ -125,6 +132,14 @@ export const AppLayout = ({
125132
</section>
126133
</main>
127134
</div>
135+
<footer>
136+
{/* LocaleToggle positioned in lower left corner of page */}
137+
{showLocaleToggle && (
138+
<div className="fixed bottom-6 left-6 z-10">
139+
<LocaleToggle placement="footer" />
140+
</div>
141+
)}
142+
</footer>
128143
<div id="body-bottom" className="w-full block mobileLandscape:hidden" />
129144
</>
130145
);

packages/fxa-settings/src/components/FormVerifyTotp/index.test.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@ describe('FormVerifyTotp component', () => {
1313
renderWithLocalizationProvider(<Subject />);
1414
expect(screen.getByText('Enter 6-digit code')).toBeVisible();
1515
expect(screen.getAllByRole('textbox')).toHaveLength(1);
16-
const button = screen.getByRole('button');
16+
const button = screen.getByRole('button', { name: 'Submit' });
1717
expect(button).toHaveTextContent('Submit');
1818
});
1919

2020
describe('submit button', () => {
2121
it('is disabled on render', () => {
2222
renderWithLocalizationProvider(<Subject />);
23-
const button = screen.getByRole('button');
23+
const button = screen.getByRole('button', { name: 'Submit' });
2424
expect(button).toHaveTextContent('Submit');
2525
expect(button).toBeDisabled();
2626
});
@@ -29,7 +29,7 @@ describe('FormVerifyTotp component', () => {
2929
const user = userEvent.setup();
3030
renderWithLocalizationProvider(<Subject />);
3131
const input = screen.getByRole('textbox');
32-
const button = screen.getByRole('button');
32+
const button = screen.getByRole('button', { name: 'Submit' });
3333
expect(button).toHaveTextContent('Submit');
3434
expect(button).toBeDisabled();
3535

@@ -42,7 +42,7 @@ describe('FormVerifyTotp component', () => {
4242
const user = userEvent.setup();
4343
renderWithLocalizationProvider(<Subject />);
4444
const input = screen.getByRole('textbox');
45-
const button = screen.getByRole('button');
45+
const button = screen.getByRole('button', { name: 'Submit' });
4646
expect(button).toHaveTextContent('Submit');
4747
expect(button).toBeDisabled();
4848

packages/fxa-settings/src/components/InputPhoneNumber/index.test.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ describe('InputPhoneNumber', () => {
1616
expect(usOption).toBeInTheDocument();
1717
expect(canadaOption).toBeInTheDocument();
1818

19-
const options = screen.getAllByRole('option');
19+
const phoneSelect = screen.getByLabelText('Select country');
20+
const options = phoneSelect.querySelectorAll('option');
2021
expect(options.length).toBe(2);
2122

2223
// expect list to be sorted alphabetically
@@ -34,14 +35,15 @@ describe('InputPhoneNumber', () => {
3435
renderWithLocalizationProvider(
3536
<Subject countries={extendedCountryOptions} />
3637
);
37-
let options = screen.getAllByRole('option');
38+
const phoneSelect = screen.getByLabelText('Select country');
39+
let options = phoneSelect.querySelectorAll('option');
3840
expect(options.length).toBe(extendedCountryOptions.length);
3941

4042
// There should be an option for each country listed
4143
expect(extendedCountryOptions.length).toEqual(options.length);
4244
extendedCountryOptions.forEach((countryOption) => {
4345
expect(
44-
options.some((option) =>
46+
Array.from(options).some((option) =>
4547
option.textContent?.includes(countryOption.name)
4648
)
4749
).toBe(true);

0 commit comments

Comments
 (0)