>;
diff --git a/packages/swingset/src/components/StoryEmbed.tsx b/packages/swingset/src/components/StoryEmbed.tsx
index 1457332727c..d3e91240d0b 100644
--- a/packages/swingset/src/components/StoryEmbed.tsx
+++ b/packages/swingset/src/components/StoryEmbed.tsx
@@ -1,6 +1,6 @@
'use client';
-import { MosaicProvider } from '@clerk/ui/mosaic/MosaicProvider';
+import { MosaicProvider } from '@clerk/ui/mosaic/providers/mosaic-provider';
import { Layers2Icon } from 'lucide-react';
import type React from 'react';
import { useState } from 'react';
diff --git a/packages/swingset/src/components/StoryPreview.tsx b/packages/swingset/src/components/StoryPreview.tsx
index ac202903489..841e3990fc6 100644
--- a/packages/swingset/src/components/StoryPreview.tsx
+++ b/packages/swingset/src/components/StoryPreview.tsx
@@ -1,6 +1,6 @@
'use client';
-import { MosaicProvider } from '@clerk/ui/mosaic/MosaicProvider';
+import { MosaicProvider } from '@clerk/ui/mosaic/providers/mosaic-provider';
import { RotateCcwIcon, SlidersHorizontalIcon } from 'lucide-react';
import type React from 'react';
import { useEffect, useState } from 'react';
@@ -61,8 +61,8 @@ export function StoryPreview({ name, storyModule }: StoryPreviewProps) {
{mounted && (
diff --git a/packages/ui/package.json b/packages/ui/package.json
index b885f3a8ff2..dd48cf45413 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -94,6 +94,7 @@
"type-check": "tsc --noEmit"
},
"dependencies": {
+ "@clerk/i18n": "workspace:^",
"@clerk/localizations": "workspace:^",
"@clerk/shared": "workspace:^",
"@emotion/cache": "11.11.0",
diff --git a/packages/ui/src/mosaic/__tests__/MosaicProvider.test.tsx b/packages/ui/src/mosaic/__tests__/MosaicProvider.test.tsx
index 1e8958c7718..f87fcc147fe 100644
--- a/packages/ui/src/mosaic/__tests__/MosaicProvider.test.tsx
+++ b/packages/ui/src/mosaic/__tests__/MosaicProvider.test.tsx
@@ -4,7 +4,7 @@ import { describe, expect, it } from 'vitest';
import type { MosaicAppearance } from '../appearance';
import { parseMosaicAppearance, useMosaicAppearance } from '../appearance';
-import { MosaicProvider, useMosaicTheme } from '../MosaicProvider';
+import { MosaicProvider, useMosaicTheme } from '../providers/mosaic-provider';
const appearance: MosaicAppearance = {
elements: {
@@ -42,7 +42,12 @@ describe('parseMosaicAppearance', () => {
describe('MosaicProvider appearance context', () => {
it('exposes [global, scoped] layers via useMosaicAppearance', () => {
const { result } = renderHook(() => useMosaicAppearance(), {
- wrapper: ({ children }) => React.createElement(MosaicProvider, { appearance, scope: 'signIn' }, children),
+ wrapper: ({ children }) =>
+ React.createElement(
+ MosaicProvider,
+ { appearance: { elements: appearance.elements, scope: 'signIn' } },
+ children,
+ ),
});
expect(result.current).toEqual([{ button: { color: 'green' } }, { button: { color: 'red' } }]);
});
diff --git a/packages/ui/src/mosaic/__tests__/i18n-server.test.ts b/packages/ui/src/mosaic/__tests__/i18n-server.test.ts
new file mode 100644
index 00000000000..e2c11aa2e1a
--- /dev/null
+++ b/packages/ui/src/mosaic/__tests__/i18n-server.test.ts
@@ -0,0 +1,37 @@
+import { describe, expect, it } from 'vitest';
+
+import { getLocalization } from '../i18n/server';
+
+describe('getLocalization', () => {
+ it('returns the default locale with no messages when acceptLanguage is null', async () => {
+ const result = await getLocalization(null);
+ expect(result).toEqual({ locale: 'en', initialMessages: {} });
+ });
+
+ it('matches the default locale via base tag (en-US → en)', async () => {
+ const result = await getLocalization('en-US');
+ expect(result).toEqual({ locale: 'en', initialMessages: {} });
+ });
+
+ it('picks the highest-priority match (en-US,fr → en, not fr)', async () => {
+ const result = await getLocalization('en-US,fr;q=0.9');
+ expect(result.locale).toBe('en');
+ });
+
+ it('loads french messages when locale is fr', async () => {
+ const result = await getLocalization('fr');
+ expect(result.locale).toBe('fr');
+ expect(result.initialMessages).toHaveProperty('fr');
+ expect(result.initialMessages.fr).toBeTruthy();
+ });
+
+ it('matches a supported locale via base tag (fr-FR → fr)', async () => {
+ const result = await getLocalization('fr-FR');
+ expect(result.locale).toBe('fr');
+ });
+
+ it('falls back to the default locale for an unsupported language', async () => {
+ const result = await getLocalization('de');
+ expect(result).toEqual({ locale: 'en', initialMessages: {} });
+ });
+});
diff --git a/packages/ui/src/mosaic/__tests__/localization-provider.test.tsx b/packages/ui/src/mosaic/__tests__/localization-provider.test.tsx
new file mode 100644
index 00000000000..07fa72e9840
--- /dev/null
+++ b/packages/ui/src/mosaic/__tests__/localization-provider.test.tsx
@@ -0,0 +1,47 @@
+import { renderHook } from '@testing-library/react';
+import React from 'react';
+import { describe, expect, it, vi } from 'vitest';
+
+import frMessages from '../locales/fr.json';
+import { MosaicProvider } from '../providers/mosaic-provider';
+import { formatBillingAmount, useMessages } from '../providers/localization-provider';
+
+const base = { title: 'Organization Profile', tab: { general: 'General', members: 'Members' } };
+
+describe('LocalizationProvider overrides', () => {
+ it('override wins over French translation when both locale and overrides are set', () => {
+ const { result } = renderHook(() => useMessages('organizationProfile', base), {
+ wrapper: ({ children }) =>
+ React.createElement(
+ MosaicProvider,
+ {
+ localization: {
+ locale: 'fr',
+ initialMessages: { fr: frMessages },
+ overrides: { 'organizationProfile.title': 'test' },
+ },
+ },
+ children,
+ ),
+ });
+ expect(result.current.title).toBe('test');
+ // Non-overridden key still resolves from the French bundle
+ expect((result.current.tab as Record).general).toBe('Général');
+ });
+});
+
+describe('LocalizationProvider currency formatting', () => {
+ const amount = {
+ amount: 1000,
+ amountFormatted: '10.00',
+ currency: 'USD',
+ currencySymbol: '$',
+ };
+
+ it('adapts Billing money amounts to a resolved currency message', () => {
+ const formatCurrency = vi.fn(() => '$10');
+
+ expect(formatBillingAmount(formatCurrency, amount, { style: 'short' })).toBe('$10');
+ expect(formatCurrency).toHaveBeenCalledWith(1000, 'USD', { style: 'short' });
+ });
+});
diff --git a/packages/ui/src/mosaic/__tests__/slot-recipe.test.ts b/packages/ui/src/mosaic/__tests__/slot-recipe.test.ts
index f50b76e7046..24b8a5084f5 100644
--- a/packages/ui/src/mosaic/__tests__/slot-recipe.test.ts
+++ b/packages/ui/src/mosaic/__tests__/slot-recipe.test.ts
@@ -3,13 +3,13 @@ import React from 'react';
import { describe, expect, it } from 'vitest';
import type { MosaicAppearance } from '../appearance';
-import { MosaicProvider } from '../MosaicProvider';
+import { MosaicProvider } from '../providers/mosaic-provider';
import { defineSlotRecipe, useRecipe } from '../slot-recipe';
import { slot, useSlot } from '../useSlot';
function wrapper(appearance?: MosaicAppearance, scope?: string) {
return function Wrapper({ children }: { children: React.ReactNode }) {
- return React.createElement(MosaicProvider, { appearance, scope }, children);
+ return React.createElement(MosaicProvider, { appearance: { ...appearance, scope } }, children);
};
}
diff --git a/packages/ui/src/mosaic/aio/organization-profile.messages.ts b/packages/ui/src/mosaic/aio/organization-profile.messages.ts
new file mode 100644
index 00000000000..41796315da5
--- /dev/null
+++ b/packages/ui/src/mosaic/aio/organization-profile.messages.ts
@@ -0,0 +1,8 @@
+export const orgProfileBase = {
+ title: 'Organization Profile',
+ tab: {
+ general: 'General',
+ members: 'Members',
+ },
+ membersContent: 'Members content',
+};
diff --git a/packages/ui/src/mosaic/aio/organization-profile.tsx b/packages/ui/src/mosaic/aio/organization-profile.tsx
index 743da8386b5..43b277a6c03 100644
--- a/packages/ui/src/mosaic/aio/organization-profile.tsx
+++ b/packages/ui/src/mosaic/aio/organization-profile.tsx
@@ -1,8 +1,11 @@
import { Box } from '../components/box';
import { Tabs } from '../components/tabs';
import { OrganizationProfileGeneral } from '../panels/organization-profile-general';
+import { useMessages } from '../providers/localization-provider';
+import { orgProfileBase } from './organization-profile.messages';
export function OrganizationProfile() {
+ const m = useMessages('organizationProfile', orgProfileBase);
return (
({
@@ -17,12 +20,12 @@ export function OrganizationProfile() {
marginBlockEnd: t.spacing(8),
})}
>
- Organization Profile
+ {m.title}
- General
- Members
+ {m.tab.general}
+ {m.tab.members}
@@ -36,7 +39,7 @@ export function OrganizationProfile() {
textAlign: 'center',
})}
>
- Members content
+ {m.membersContent}
diff --git a/packages/ui/src/mosaic/components/__tests__/button.test.tsx b/packages/ui/src/mosaic/components/__tests__/button.test.tsx
index beada857bc8..9860e2fa8fc 100644
--- a/packages/ui/src/mosaic/components/__tests__/button.test.tsx
+++ b/packages/ui/src/mosaic/components/__tests__/button.test.tsx
@@ -3,7 +3,7 @@ import React from 'react';
import { describe, expect, it } from 'vitest';
import type { MosaicAppearance } from '../../appearance';
-import { MosaicProvider } from '../../MosaicProvider';
+import { MosaicProvider } from '../../providers/mosaic-provider';
import { Button } from '../button';
/** Concatenates every inserted Emotion `