@nebulr-group/bridge-svelte 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/components/sdk-auth/LoginForm.svelte +1 -1
- package/dist/client/components/sdk-auth/SsoButton.svelte +13 -5
- package/dist/client/components/sdk-auth/SsoButton.svelte.d.ts +3 -1
- package/dist/client/components/sdk-auth/TenantSelector.svelte +9 -3
- package/dist/client/components/sdk-auth/TenantSelector.svelte.d.ts +3 -1
- package/dist/client/components/sdk-auth/WorkspaceSelector.svelte +9 -3
- package/dist/client/components/sdk-auth/WorkspaceSelector.svelte.d.ts +3 -1
- package/dist/client/components/sdk-auth/selector-i18n.test.d.ts +1 -0
- package/dist/client/components/sdk-auth/selector-i18n.test.js +248 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +9 -0
- package/package.json +3 -3
|
@@ -237,7 +237,7 @@
|
|
|
237
237
|
<MfaSetup onError={onError} {messages} />
|
|
238
238
|
|
|
239
239
|
{:else if currentAuthState === 'tenant-selection'}
|
|
240
|
-
<TenantSelector onError={onError} />
|
|
240
|
+
<TenantSelector onError={onError} {messages} />
|
|
241
241
|
|
|
242
242
|
<!-- Inline forgot password -->
|
|
243
243
|
{:else if step === 'forgot-password'}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import type { HTMLButtonAttributes } from 'svelte/elements';
|
|
4
|
-
import type { FederationConnection } from '@nebulr-group/bridge-auth-core';
|
|
4
|
+
import type { FederationConnection, MessageOverrides } from '@nebulr-group/bridge-auth-core';
|
|
5
5
|
import { getBridgeAuth } from '../../../core/bridge-instance.js';
|
|
6
|
+
import { getTranslator } from '../../stores/i18n.js';
|
|
6
7
|
import Spinner from './shared/Spinner.svelte';
|
|
7
8
|
|
|
8
9
|
interface Props extends HTMLButtonAttributes {
|
|
@@ -19,6 +20,8 @@
|
|
|
19
20
|
onSuccess?: () => void;
|
|
20
21
|
onError?: (error: Error) => void;
|
|
21
22
|
icon?: Snippet;
|
|
23
|
+
/** Per-key copy overrides for this component only (TBP-630). */
|
|
24
|
+
messages?: MessageOverrides;
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
let {
|
|
@@ -28,13 +31,18 @@
|
|
|
28
31
|
onSuccess,
|
|
29
32
|
onError,
|
|
30
33
|
icon,
|
|
34
|
+
messages,
|
|
31
35
|
class: className,
|
|
32
36
|
style,
|
|
33
37
|
...rest
|
|
34
38
|
}: Props = $props();
|
|
35
39
|
|
|
40
|
+
const t = $derived(getTranslator(messages));
|
|
41
|
+
|
|
36
42
|
let loading = $state(false);
|
|
37
|
-
|
|
43
|
+
// `label` still wins: an app naming its own provider button is voice, not
|
|
44
|
+
// mechanics, and the catalogue only supplies the default (TBP-634).
|
|
45
|
+
let buttonLabel = $derived(label ?? t('sso.continueWith', { provider: connection.name }));
|
|
38
46
|
|
|
39
47
|
async function handleClick() {
|
|
40
48
|
if (loading) return;
|
|
@@ -44,13 +52,13 @@
|
|
|
44
52
|
if (result.type === 'auth_success') {
|
|
45
53
|
onSuccess?.();
|
|
46
54
|
} else if (result.type === 'auth_error') {
|
|
47
|
-
throw new Error(result.error || '
|
|
55
|
+
throw new Error(result.error || t('sso.error.login'));
|
|
48
56
|
}
|
|
49
57
|
// auth_mfa_required and auth_tenant_selection are handled by authState store
|
|
50
58
|
} catch (err: any) {
|
|
51
59
|
const message = err.message?.includes('popup')
|
|
52
|
-
? '
|
|
53
|
-
: err.message || '
|
|
60
|
+
? t('sso.error.popupBlocked')
|
|
61
|
+
: err.message || t('sso.error.login');
|
|
54
62
|
onError?.(new Error(message));
|
|
55
63
|
} finally {
|
|
56
64
|
loading = false;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
2
|
import type { HTMLButtonAttributes } from 'svelte/elements';
|
|
3
|
-
import type { FederationConnection } from '@nebulr-group/bridge-auth-core';
|
|
3
|
+
import type { FederationConnection, MessageOverrides } from '@nebulr-group/bridge-auth-core';
|
|
4
4
|
interface Props extends HTMLButtonAttributes {
|
|
5
5
|
connection: FederationConnection;
|
|
6
6
|
label?: string;
|
|
@@ -15,6 +15,8 @@ interface Props extends HTMLButtonAttributes {
|
|
|
15
15
|
onSuccess?: () => void;
|
|
16
16
|
onError?: (error: Error) => void;
|
|
17
17
|
icon?: Snippet;
|
|
18
|
+
/** Per-key copy overrides for this component only (TBP-630). */
|
|
19
|
+
messages?: MessageOverrides;
|
|
18
20
|
}
|
|
19
21
|
declare const SsoButton: import("svelte").Component<Props, {}, "">;
|
|
20
22
|
type SsoButton = ReturnType<typeof SsoButton>;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
|
-
import type { TenantUser } from '@nebulr-group/bridge-auth-core';
|
|
4
|
+
import type { MessageOverrides, TenantUser } from '@nebulr-group/bridge-auth-core';
|
|
5
5
|
import { getBridgeAuth, tenantUsersStore } from '../../../core/bridge-instance.js';
|
|
6
|
+
import { getTranslator } from '../../stores/i18n.js';
|
|
6
7
|
import AuthFormWrapper from './shared/AuthFormWrapper.svelte';
|
|
7
8
|
import Spinner from './shared/Spinner.svelte';
|
|
8
9
|
import Alert from './shared/Alert.svelte';
|
|
@@ -11,17 +12,22 @@
|
|
|
11
12
|
onSelect?: () => void;
|
|
12
13
|
onError?: (error: Error) => void;
|
|
13
14
|
tenantItem?: Snippet<[TenantUser]>;
|
|
15
|
+
/** Per-key copy overrides for this component only (TBP-630). */
|
|
16
|
+
messages?: MessageOverrides;
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
let {
|
|
17
20
|
onSelect,
|
|
18
21
|
onError,
|
|
19
22
|
tenantItem,
|
|
23
|
+
messages,
|
|
20
24
|
class: className,
|
|
21
25
|
style,
|
|
22
26
|
...rest
|
|
23
27
|
}: Props = $props();
|
|
24
28
|
|
|
29
|
+
const t = $derived(getTranslator(messages));
|
|
30
|
+
|
|
25
31
|
let loading = $state(false);
|
|
26
32
|
let selectedId = $state<string | null>(null);
|
|
27
33
|
let error = $state<string | null>(null);
|
|
@@ -35,7 +41,7 @@
|
|
|
35
41
|
await getBridgeAuth().selectTenant(tenantUser.id);
|
|
36
42
|
onSelect?.();
|
|
37
43
|
} catch (err: any) {
|
|
38
|
-
error = err.message || '
|
|
44
|
+
error = err.message || t('tenant.error.select');
|
|
39
45
|
onError?.(err);
|
|
40
46
|
} finally {
|
|
41
47
|
loading = false;
|
|
@@ -44,7 +50,7 @@
|
|
|
44
50
|
}
|
|
45
51
|
</script>
|
|
46
52
|
|
|
47
|
-
<AuthFormWrapper heading=
|
|
53
|
+
<AuthFormWrapper heading={t('tenant.chooseHeading')} class={className} {style} {...rest}>
|
|
48
54
|
{#if error}
|
|
49
55
|
<Alert variant="error">{error}</Alert>
|
|
50
56
|
{/if}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
2
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
-
import type { TenantUser } from '@nebulr-group/bridge-auth-core';
|
|
3
|
+
import type { MessageOverrides, TenantUser } from '@nebulr-group/bridge-auth-core';
|
|
4
4
|
interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
5
5
|
onSelect?: () => void;
|
|
6
6
|
onError?: (error: Error) => void;
|
|
7
7
|
tenantItem?: Snippet<[TenantUser]>;
|
|
8
|
+
/** Per-key copy overrides for this component only (TBP-630). */
|
|
9
|
+
messages?: MessageOverrides;
|
|
8
10
|
}
|
|
9
11
|
declare const TenantSelector: import("svelte").Component<Props, {}, "">;
|
|
10
12
|
type TenantSelector = ReturnType<typeof TenantSelector>;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
3
|
import type { Snippet } from 'svelte';
|
|
4
|
-
import type { Workspace } from '@nebulr-group/bridge-auth-core';
|
|
4
|
+
import type { MessageOverrides, Workspace } from '@nebulr-group/bridge-auth-core';
|
|
5
5
|
import { onMount } from 'svelte';
|
|
6
6
|
import { getBridgeAuth, profileStore } from '../../../core/bridge-instance.js';
|
|
7
|
+
import { getTranslator } from '../../stores/i18n.js';
|
|
7
8
|
import Spinner from './shared/Spinner.svelte';
|
|
8
9
|
import Alert from './shared/Alert.svelte';
|
|
9
10
|
|
|
@@ -11,17 +12,22 @@
|
|
|
11
12
|
onSwitch?: () => void;
|
|
12
13
|
onError?: (error: Error) => void;
|
|
13
14
|
workspaceItem?: Snippet<[{ workspace: Workspace; isActive: boolean; isLoading: boolean; onSelect: () => void }]>;
|
|
15
|
+
/** Per-key copy overrides for this component only (TBP-630). */
|
|
16
|
+
messages?: MessageOverrides;
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
let {
|
|
17
20
|
onSwitch,
|
|
18
21
|
onError,
|
|
19
22
|
workspaceItem,
|
|
23
|
+
messages,
|
|
20
24
|
class: className,
|
|
21
25
|
style,
|
|
22
26
|
...rest
|
|
23
27
|
}: Props = $props();
|
|
24
28
|
|
|
29
|
+
const t = $derived(getTranslator(messages));
|
|
30
|
+
|
|
25
31
|
let workspaces = $state<Workspace[]>([]);
|
|
26
32
|
let loadError = $state<string | null>(null);
|
|
27
33
|
let loadingList = $state(true);
|
|
@@ -34,7 +40,7 @@
|
|
|
34
40
|
try {
|
|
35
41
|
workspaces = await getBridgeAuth().getWorkspaces();
|
|
36
42
|
} catch (err: any) {
|
|
37
|
-
loadError = err.message || '
|
|
43
|
+
loadError = err.message || t('workspace.error.load');
|
|
38
44
|
} finally {
|
|
39
45
|
loadingList = false;
|
|
40
46
|
}
|
|
@@ -48,7 +54,7 @@
|
|
|
48
54
|
await getBridgeAuth().switchWorkspace(workspace.id);
|
|
49
55
|
onSwitch?.();
|
|
50
56
|
} catch (err: any) {
|
|
51
|
-
switchError = err.message || '
|
|
57
|
+
switchError = err.message || t('workspace.error.switch');
|
|
52
58
|
onError?.(err);
|
|
53
59
|
} finally {
|
|
54
60
|
switchingId = null;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
|
-
import type { Workspace } from '@nebulr-group/bridge-auth-core';
|
|
3
|
+
import type { MessageOverrides, Workspace } from '@nebulr-group/bridge-auth-core';
|
|
4
4
|
interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
5
5
|
onSwitch?: () => void;
|
|
6
6
|
onError?: (error: Error) => void;
|
|
@@ -10,6 +10,8 @@ interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
|
10
10
|
isLoading: boolean;
|
|
11
11
|
onSelect: () => void;
|
|
12
12
|
}]>;
|
|
13
|
+
/** Per-key copy overrides for this component only (TBP-630). */
|
|
14
|
+
messages?: MessageOverrides;
|
|
13
15
|
}
|
|
14
16
|
declare const WorkspaceSelector: import("svelte").Component<Props, {}, "">;
|
|
15
17
|
type WorkspaceSelector = ReturnType<typeof WorkspaceSelector>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
// TenantSelector, WorkspaceSelector and SsoButton were missed by TBP-630's
|
|
2
|
+
// original i18n sweep and by the TBP-633 port, so they still rendered English
|
|
3
|
+
// in all four UI packages after everything around them had been translated.
|
|
4
|
+
// TenantSelector is the one that stings: it renders MID-LOGIN on the
|
|
5
|
+
// multi-workspace path, putting one English screen between a translated login
|
|
6
|
+
// form and a translated app (TBP-634).
|
|
7
|
+
//
|
|
8
|
+
// HARNESS NOTE — see auth-form-description.test.ts for the full explanation of
|
|
9
|
+
// why these render through Svelte's server renderer rather than a DOM. Short
|
|
10
|
+
// version: this workspace has no DOM implementation, so `mount()` and therefore
|
|
11
|
+
// clicks are unavailable. Two consequences shape this file.
|
|
12
|
+
//
|
|
13
|
+
// 1. Anything reachable at initial render (TenantSelector's heading,
|
|
14
|
+
// SsoButton's label) is asserted on the REAL rendered markup.
|
|
15
|
+
//
|
|
16
|
+
// 2. The five error strings are only produced inside a catch block a click
|
|
17
|
+
// would have to reach. Those get two assertions that are weak alone and
|
|
18
|
+
// strong together:
|
|
19
|
+
// - a `seeded()` render proving the KEY resolves to real Swedish, and
|
|
20
|
+
// - a source sweep proving the English literal the handler used to carry
|
|
21
|
+
// is gone from the file.
|
|
22
|
+
// Reverting the handler to `'Failed to select workspace.'` puts that exact
|
|
23
|
+
// string back in the source and fails the sweep. The sweep reads its
|
|
24
|
+
// forbidden strings out of auth-core's `en` catalogue, so it cannot drift
|
|
25
|
+
// from what the keys actually say.
|
|
26
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
27
|
+
import { createServer } from 'vite';
|
|
28
|
+
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
|
29
|
+
import { render } from 'svelte/server';
|
|
30
|
+
import {} from 'svelte';
|
|
31
|
+
import { en, sv } from '@nebulr-group/bridge-auth-core';
|
|
32
|
+
import fs from 'node:fs';
|
|
33
|
+
import path from 'node:path';
|
|
34
|
+
const ROOT = process.cwd();
|
|
35
|
+
const COMPONENT_DIR = 'src/lib/client/components/sdk-auth';
|
|
36
|
+
const SEED_PREFIX = '__seeded-i18n-';
|
|
37
|
+
/** The keys TBP-634 added, and which component owns each. */
|
|
38
|
+
const OWNED = {
|
|
39
|
+
'TenantSelector.svelte': ['tenant.chooseHeading', 'tenant.error.select'],
|
|
40
|
+
'WorkspaceSelector.svelte': ['workspace.error.load', 'workspace.error.switch'],
|
|
41
|
+
'SsoButton.svelte': ['sso.continueWith', 'sso.error.popupBlocked', 'sso.error.login'],
|
|
42
|
+
};
|
|
43
|
+
let server;
|
|
44
|
+
const seededPaths = [];
|
|
45
|
+
beforeAll(async () => {
|
|
46
|
+
for (const f of fs.readdirSync(path.join(ROOT, COMPONENT_DIR))) {
|
|
47
|
+
if (f.startsWith(SEED_PREFIX))
|
|
48
|
+
fs.unlinkSync(path.join(ROOT, COMPONENT_DIR, f));
|
|
49
|
+
}
|
|
50
|
+
server = await createServer({
|
|
51
|
+
configFile: false,
|
|
52
|
+
root: ROOT,
|
|
53
|
+
logLevel: 'error',
|
|
54
|
+
server: { middlewareMode: true, hmr: false },
|
|
55
|
+
plugins: [svelte({ compilerOptions: { dev: false } })],
|
|
56
|
+
});
|
|
57
|
+
}, 120_000);
|
|
58
|
+
afterAll(async () => {
|
|
59
|
+
await server?.close();
|
|
60
|
+
for (const p of seededPaths) {
|
|
61
|
+
if (fs.existsSync(p))
|
|
62
|
+
fs.unlinkSync(p);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
function componentId(file) {
|
|
66
|
+
return `/${COMPONENT_DIR}/${file}`;
|
|
67
|
+
}
|
|
68
|
+
/** A copy of `file` with initial-state literals replaced, to reach a later view. */
|
|
69
|
+
function seeded(file, seeds) {
|
|
70
|
+
let source = fs.readFileSync(path.join(ROOT, COMPONENT_DIR, file), 'utf8');
|
|
71
|
+
for (const [from, to] of seeds) {
|
|
72
|
+
if (!source.includes(from)) {
|
|
73
|
+
throw new Error(`State seed target not found in ${file}:\n ${from}\n` +
|
|
74
|
+
'The component was refactored. Update the seed so the test keeps ' +
|
|
75
|
+
'rendering the intended view — do not delete the assertion.');
|
|
76
|
+
}
|
|
77
|
+
source = source.replace(from, to);
|
|
78
|
+
}
|
|
79
|
+
const name = `${SEED_PREFIX}${process.pid}-${seededPaths.length}-${file}`;
|
|
80
|
+
const full = path.join(ROOT, COMPONENT_DIR, name);
|
|
81
|
+
fs.writeFileSync(full, source);
|
|
82
|
+
seededPaths.push(full);
|
|
83
|
+
return `/${COMPONENT_DIR}/${name}`;
|
|
84
|
+
}
|
|
85
|
+
async function renderComponent(id, props = {}) {
|
|
86
|
+
const mod = await server.ssrLoadModule(id);
|
|
87
|
+
const Component = mod.default;
|
|
88
|
+
return render(Component, { props }).body;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Point the shared config store at a locale.
|
|
92
|
+
*
|
|
93
|
+
* The components read locale off BridgeConfig rather than taking it as a prop,
|
|
94
|
+
* so this is how a real app sets it. Loaded through the same Vite module graph
|
|
95
|
+
* the components resolve, otherwise they would read a different store instance.
|
|
96
|
+
*/
|
|
97
|
+
async function setLocale(locale) {
|
|
98
|
+
const mod = await server.ssrLoadModule('/src/lib/client/stores/config.store.ts');
|
|
99
|
+
mod.bridgeConfig.initConfig({ appId: 'tbp-634-test', locale });
|
|
100
|
+
}
|
|
101
|
+
const CONNECTION = { type: 'google', name: 'Google' };
|
|
102
|
+
function text(html) {
|
|
103
|
+
return html
|
|
104
|
+
.replace(/<!--[\s\S]*?-->/g, '')
|
|
105
|
+
.replace(/<[^>]*>/g, '')
|
|
106
|
+
.replace(/\s+/g, ' ')
|
|
107
|
+
.trim();
|
|
108
|
+
}
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// 1. What the server renderer can reach directly
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
describe('TenantSelector heading (TBP-634)', () => {
|
|
113
|
+
it('renders the English heading when no locale is set', async () => {
|
|
114
|
+
await setLocale(undefined);
|
|
115
|
+
const html = await renderComponent(componentId('TenantSelector.svelte'));
|
|
116
|
+
expect(text(html)).toContain(en['tenant.chooseHeading']);
|
|
117
|
+
});
|
|
118
|
+
it('renders the Swedish heading at locale sv', async () => {
|
|
119
|
+
await setLocale('sv');
|
|
120
|
+
const html = await renderComponent(componentId('TenantSelector.svelte'));
|
|
121
|
+
const rendered = text(html);
|
|
122
|
+
expect(rendered).toContain(sv['tenant.chooseHeading']);
|
|
123
|
+
// The English must be GONE, not merely joined by the Swedish. A component
|
|
124
|
+
// that rendered both would satisfy a `toContain` on the Swedish alone.
|
|
125
|
+
expect(rendered).not.toContain(en['tenant.chooseHeading']);
|
|
126
|
+
});
|
|
127
|
+
it('lets a per-component override beat the locale', async () => {
|
|
128
|
+
await setLocale('sv');
|
|
129
|
+
const html = await renderComponent(componentId('TenantSelector.svelte'), {
|
|
130
|
+
messages: { 'tenant.chooseHeading': 'Välj kund' },
|
|
131
|
+
});
|
|
132
|
+
const rendered = text(html);
|
|
133
|
+
expect(rendered).toContain('Välj kund');
|
|
134
|
+
expect(rendered).not.toContain(sv['tenant.chooseHeading']);
|
|
135
|
+
});
|
|
136
|
+
it('never renders the raw key', async () => {
|
|
137
|
+
for (const locale of ['sv', 'de', 'klingon', undefined]) {
|
|
138
|
+
await setLocale(locale);
|
|
139
|
+
const html = await renderComponent(componentId('TenantSelector.svelte'));
|
|
140
|
+
expect(text(html)).not.toContain('tenant.chooseHeading');
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
describe('SsoButton label (TBP-634)', () => {
|
|
145
|
+
it('interpolates the provider name into the localised label', async () => {
|
|
146
|
+
await setLocale('sv');
|
|
147
|
+
const html = await renderComponent(componentId('SsoButton.svelte'), {
|
|
148
|
+
connection: CONNECTION,
|
|
149
|
+
});
|
|
150
|
+
const rendered = text(html);
|
|
151
|
+
expect(rendered).toBe(sv['sso.continueWith'].replace('{provider}', 'Google'));
|
|
152
|
+
// Placeholder consumed, not printed.
|
|
153
|
+
expect(rendered).not.toContain('{provider}');
|
|
154
|
+
expect(rendered).not.toContain(en['sso.continueWith'].replace('{provider}', 'Google'));
|
|
155
|
+
});
|
|
156
|
+
it('still lets the app supply its own label — that is voice, not mechanics', async () => {
|
|
157
|
+
await setLocale('sv');
|
|
158
|
+
const html = await renderComponent(componentId('SsoButton.svelte'), {
|
|
159
|
+
connection: CONNECTION,
|
|
160
|
+
label: 'Logga in med jobbkontot',
|
|
161
|
+
});
|
|
162
|
+
expect(text(html)).toBe('Logga in med jobbkontot');
|
|
163
|
+
});
|
|
164
|
+
it('falls back to English for an unsupported locale, never to the key', async () => {
|
|
165
|
+
// Invented, not a real-but-unshipped code: TBP-632 turned 'de' and 'fr'
|
|
166
|
+
// into shipped locales and broke every test that had borrowed one as its
|
|
167
|
+
// stand-in for 'unknown'.
|
|
168
|
+
await setLocale('klingon');
|
|
169
|
+
const html = await renderComponent(componentId('SsoButton.svelte'), {
|
|
170
|
+
connection: CONNECTION,
|
|
171
|
+
});
|
|
172
|
+
expect(text(html)).toBe(en['sso.continueWith'].replace('{provider}', 'Google'));
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
// 2. The error strings, reached by seeding initial state
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
describe('selector error copy resolves through the catalogue (TBP-634)', () => {
|
|
179
|
+
const CASES = [
|
|
180
|
+
{
|
|
181
|
+
file: 'TenantSelector.svelte',
|
|
182
|
+
from: 'let error = $state<string | null>(null);',
|
|
183
|
+
key: 'tenant.error.select',
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
file: 'WorkspaceSelector.svelte',
|
|
187
|
+
from: 'let loadError = $state<string | null>(null);',
|
|
188
|
+
key: 'workspace.error.load',
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
file: 'WorkspaceSelector.svelte',
|
|
192
|
+
from: 'let switchError = $state<string | null>(null);',
|
|
193
|
+
key: 'workspace.error.switch',
|
|
194
|
+
},
|
|
195
|
+
];
|
|
196
|
+
for (const { file, from, key } of CASES) {
|
|
197
|
+
it(`${file} renders ${key} in Swedish`, async () => {
|
|
198
|
+
await setLocale('sv');
|
|
199
|
+
const to = from.replace('(null)', `(t('${key}'))`);
|
|
200
|
+
const html = await renderComponent(seeded(file, [[from, to]]));
|
|
201
|
+
const rendered = text(html);
|
|
202
|
+
expect(rendered).toContain(sv[key]);
|
|
203
|
+
expect(rendered).not.toContain(en[key]);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
// 3. Source sweep — the half that survives a reverted handler
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
describe('no English literal remains in the three components (TBP-634)', () => {
|
|
211
|
+
for (const [file, keys] of Object.entries(OWNED)) {
|
|
212
|
+
it(`${file} carries none of its English strings as a literal`, () => {
|
|
213
|
+
const source = fs.readFileSync(path.join(ROOT, COMPONENT_DIR, file), 'utf8');
|
|
214
|
+
for (const key of keys) {
|
|
215
|
+
// Interpolated copy is compared on its literal prefix: the source used
|
|
216
|
+
// to hold `Continue with ${connection.name}`, which is the catalogue
|
|
217
|
+
// string with the placeholder swapped for an expression.
|
|
218
|
+
const needle = en[key].split('{')[0].trim();
|
|
219
|
+
expect(needle.length, `${key} has no comparable literal prefix`).toBeGreaterThan(4);
|
|
220
|
+
expect(source, `${file} still contains "${needle}"`).not.toContain(needle);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
it('the sweep is looking at real files with real content', () => {
|
|
225
|
+
// Guard against the whole suite passing because a rename made every
|
|
226
|
+
// readFileSync return something empty or unrelated.
|
|
227
|
+
for (const file of Object.keys(OWNED)) {
|
|
228
|
+
const source = fs.readFileSync(path.join(ROOT, COMPONENT_DIR, file), 'utf8');
|
|
229
|
+
expect(source.length).toBeGreaterThan(500);
|
|
230
|
+
expect(source).toContain('getTranslator');
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
// 4. LoginForm fans `messages` down to TenantSelector
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
describe('LoginForm → TenantSelector fan-out (TBP-634)', () => {
|
|
238
|
+
it('passes messages down, the way it already does to MfaChallenge', () => {
|
|
239
|
+
// Only reachable on the multi-workspace path, which needs auth state a
|
|
240
|
+
// server render cannot produce — so this is asserted on the template. It is
|
|
241
|
+
// the easiest of the fan-outs to forget precisely because it so rarely
|
|
242
|
+
// renders.
|
|
243
|
+
const source = fs.readFileSync(path.join(ROOT, COMPONENT_DIR, 'LoginForm.svelte'), 'utf8');
|
|
244
|
+
const tag = source.match(/<TenantSelector[^>]*\/>/);
|
|
245
|
+
expect(tag, 'LoginForm no longer renders <TenantSelector>').not.toBeNull();
|
|
246
|
+
expect(tag[0]).toContain('{messages}');
|
|
247
|
+
});
|
|
248
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -51,3 +51,5 @@ export type { Operator, AttributeType, Condition, ConditionValue, ValidationErro
|
|
|
51
51
|
export type { FlagResponse, CreateFlagInput, UpdateFlagInput, FlagSchedule, } from '@nebulr-group/bridge-auth-core';
|
|
52
52
|
export { DEFAULT_RETURN_TO_PARAM, readReturnTo, sanitizeReturnTo, withReturnTo, } from '@nebulr-group/bridge-auth-core';
|
|
53
53
|
export type { ReturnToConfig } from '@nebulr-group/bridge-auth-core';
|
|
54
|
+
export { createTranslator, en, hasLocale, interpolate, LOCALES, normalizeLocale, sv, } from '@nebulr-group/bridge-auth-core';
|
|
55
|
+
export type { MessageKey, MessageOverrides, Messages, Translator, } from '@nebulr-group/bridge-auth-core';
|
package/dist/index.js
CHANGED
|
@@ -71,3 +71,12 @@ export { BridgeFlags, BridgeIdentity, MemoryIdentityStorage, attachIdentity, gen
|
|
|
71
71
|
// login page can import the reader (and its open-redirect validation) from the
|
|
72
72
|
// same package it already depends on, instead of reaching past bridge-svelte.
|
|
73
73
|
export { DEFAULT_RETURN_TO_PARAM, readReturnTo, sanitizeReturnTo, withReturnTo, } from '@nebulr-group/bridge-auth-core';
|
|
74
|
+
// TBP-630 — SDK auth copy / translations. Re-exported from auth-core for the
|
|
75
|
+
// same reason as the return-to helpers above: a consumer building its own
|
|
76
|
+
// locale catalogue should import from the package it already depends on rather
|
|
77
|
+
// than reaching past bridge-svelte to a peer dependency it never declared —
|
|
78
|
+
// which pnpm's default layout and Yarn PnP refuse outright.
|
|
79
|
+
//
|
|
80
|
+
// bridge-react, -angular and -nextjs already export this surface; svelte was
|
|
81
|
+
// the odd one out.
|
|
82
|
+
export { createTranslator, en, hasLocale, interpolate, LOCALES, normalizeLocale, sv, } from '@nebulr-group/bridge-auth-core';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nebulr-group/bridge-svelte",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Bridge Svelte library, This library helps you to add bridge authentication and feature flags, and payments to your svelte application.",
|
|
5
5
|
"author": "Iman Pouya",
|
|
6
6
|
"license": "MIT",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"svelte": "^5.0.0",
|
|
57
57
|
"@sveltejs/kit": "^2.0.0",
|
|
58
58
|
"@stripe/stripe-js": ">=4.0.0",
|
|
59
|
-
"@nebulr-group/bridge-auth-core": ">=0.
|
|
59
|
+
"@nebulr-group/bridge-auth-core": ">=0.6.0 <1.0.0"
|
|
60
60
|
},
|
|
61
61
|
"peerDependenciesMeta": {
|
|
62
62
|
"@stripe/stripe-js": {
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
"typescript": "^6.0.0",
|
|
78
78
|
"vite": "^6.2.6",
|
|
79
79
|
"vitest": "^4.1.4",
|
|
80
|
-
"@nebulr-group/bridge-auth-core": "^0.
|
|
80
|
+
"@nebulr-group/bridge-auth-core": "^0.6.0"
|
|
81
81
|
},
|
|
82
82
|
"keywords": [
|
|
83
83
|
"svelte",
|