@usequeek/theme-kit 0.1.20 → 0.1.21
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/README.md +117 -0
- package/components/auth/auth-email-login-step.tsx +9 -7
- package/components/auth/auth-email-otp-step.tsx +9 -7
- package/components/auth/auth-email-otp-verify-step.tsx +8 -6
- package/components/auth/auth-email-register-step.tsx +10 -8
- package/components/auth/auth-signup-step.tsx +8 -6
- package/components/blog/category-filter.tsx +6 -5
- package/components/blog/pagination.tsx +8 -7
- package/components/blog/post-meta.tsx +15 -8
- package/components/blog/related-posts.tsx +5 -3
- package/components/blog/share-buttons.tsx +10 -8
- package/components/checkout/checkout-contact.tsx +6 -4
- package/components/checkout/default-checkout-shell.tsx +17 -15
- package/components/checkout/policy-links.tsx +5 -3
- package/hooks/use-auth-flow.tsx +18 -17
- package/index.ts +4 -0
- package/locales/en.default.json +45 -7
- package/package.json +1 -1
- package/strings/pseudo-locale.ts +264 -0
- package/strings/theme-strings.ts +178 -15
- package/tests/auth-email-errors.test.tsx +55 -0
- package/tests/display-format.test.ts +364 -2
- package/tests/shopper-chrome-strings.test.tsx +272 -0
- package/tests/theme-hook-placement.test.tsx +179 -0
- package/tests/theme-strings-pseudo.test.tsx +171 -0
- package/tests/theme-strings-typed.test.tsx +97 -0
- package/tests/theme-strings.test.tsx +8 -2
- package/tests/theme-typed-keys.fixture.ts +76 -0
- package/utils/format.ts +97 -9
- package/utils/locale.ts +52 -0
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import type { JSX } from 'react';
|
|
4
4
|
import { useEffect, useRef, useState } from 'react';
|
|
5
5
|
import { useAuth } from '../../hooks/use-auth';
|
|
6
|
+
import { useThemeStrings } from '../../provider';
|
|
6
7
|
import { Image } from '../image';
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -13,6 +14,7 @@ import { Image } from '../image';
|
|
|
13
14
|
*/
|
|
14
15
|
export function CheckoutContactSection(): JSX.Element {
|
|
15
16
|
const { user, isAuthenticated, openAuthModal, logout } = useAuth();
|
|
17
|
+
const t = useThemeStrings();
|
|
16
18
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
17
19
|
const menuRef = useRef<HTMLDivElement>(null);
|
|
18
20
|
|
|
@@ -31,13 +33,13 @@ export function CheckoutContactSection(): JSX.Element {
|
|
|
31
33
|
return (
|
|
32
34
|
<section className="core-checkout__section core-checkout-contact">
|
|
33
35
|
<div className="core-checkout-contact__row">
|
|
34
|
-
<span className="core-checkout-contact__label">
|
|
36
|
+
<span className="core-checkout-contact__label">{t('checkout.contact.label')}</span>
|
|
35
37
|
<button
|
|
36
38
|
type="button"
|
|
37
39
|
className="core-checkout-contact__signin"
|
|
38
40
|
onClick={() => openAuthModal('login')}
|
|
39
41
|
>
|
|
40
|
-
|
|
42
|
+
{t('checkout.contact.signin')}
|
|
41
43
|
</button>
|
|
42
44
|
</div>
|
|
43
45
|
</section>
|
|
@@ -62,7 +64,7 @@ export function CheckoutContactSection(): JSX.Element {
|
|
|
62
64
|
<button
|
|
63
65
|
type="button"
|
|
64
66
|
className="core-checkout-contact__menu-btn"
|
|
65
|
-
aria-label=
|
|
67
|
+
aria-label={t('checkout.contact.options')}
|
|
66
68
|
onClick={() => setMenuOpen((v) => !v)}
|
|
67
69
|
>
|
|
68
70
|
<svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
|
@@ -74,7 +76,7 @@ export function CheckoutContactSection(): JSX.Element {
|
|
|
74
76
|
{menuOpen ? (
|
|
75
77
|
<div className="core-checkout-contact__menu">
|
|
76
78
|
<button type="button" onClick={() => { setMenuOpen(false); logout(); }}>
|
|
77
|
-
|
|
79
|
+
{t('checkout.contact.signout')}
|
|
78
80
|
</button>
|
|
79
81
|
</div>
|
|
80
82
|
) : null}
|
|
@@ -5,6 +5,7 @@ import type { CheckoutShellProps } from '../../types/theme';
|
|
|
5
5
|
import { formatMoney } from '../../utils/format';
|
|
6
6
|
import { useHref } from '../../hooks/use-href';
|
|
7
7
|
import { CheckoutContactSection } from './checkout-contact';
|
|
8
|
+
import { useThemeStrings } from '../../provider';
|
|
8
9
|
import { PolicyLinks } from './policy-links';
|
|
9
10
|
|
|
10
11
|
export function DefaultCheckoutShell({
|
|
@@ -29,14 +30,15 @@ export function DefaultCheckoutShell({
|
|
|
29
30
|
policies,
|
|
30
31
|
}: CheckoutShellProps): JSX.Element {
|
|
31
32
|
const href = useHref();
|
|
33
|
+
const t = useThemeStrings();
|
|
32
34
|
|
|
33
35
|
if (mode === 'page') {
|
|
34
36
|
if (totalCount === 0) {
|
|
35
37
|
return (
|
|
36
38
|
<div className="core-checkout core-checkout--page">
|
|
37
39
|
<div className="core-checkout__empty">
|
|
38
|
-
<p>
|
|
39
|
-
<Link href={href('/')} className="core-checkout__back-link">
|
|
40
|
+
<p>{t('checkout.empty.message')}</p>
|
|
41
|
+
<Link href={href('/')} className="core-checkout__back-link">{t('checkout.empty.browse')}</Link>
|
|
40
42
|
</div>
|
|
41
43
|
</div>
|
|
42
44
|
);
|
|
@@ -52,23 +54,23 @@ export function DefaultCheckoutShell({
|
|
|
52
54
|
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true" width="14" height="14">
|
|
53
55
|
<path d="M10 13L5 8l5-5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
54
56
|
</svg>
|
|
55
|
-
|
|
57
|
+
{t('checkout.page.back')}
|
|
56
58
|
</Link>
|
|
57
59
|
</div>
|
|
58
60
|
<CheckoutContactSection />
|
|
59
61
|
<section className="core-checkout__section">
|
|
60
|
-
<h3 className="core-checkout__section-title">
|
|
62
|
+
<h3 className="core-checkout__section-title">{t('checkout.delivery.title')}</h3>
|
|
61
63
|
{deliveryModes}
|
|
62
64
|
</section>
|
|
63
65
|
{shippingZonePicker ? (
|
|
64
66
|
<section className="core-checkout__section">
|
|
65
|
-
<h3 className="core-checkout__section-title">
|
|
67
|
+
<h3 className="core-checkout__section-title">{t('checkout.zone.title')}</h3>
|
|
66
68
|
{shippingZonePicker}
|
|
67
69
|
</section>
|
|
68
70
|
) : null}
|
|
69
71
|
{addressPicker ? (
|
|
70
72
|
<section className="core-checkout__section">
|
|
71
|
-
<h3 className="core-checkout__section-title">
|
|
73
|
+
<h3 className="core-checkout__section-title">{t('checkout.address.title')}</h3>
|
|
72
74
|
{addressPicker}
|
|
73
75
|
</section>
|
|
74
76
|
) : null}
|
|
@@ -79,13 +81,13 @@ export function DefaultCheckoutShell({
|
|
|
79
81
|
) : null}
|
|
80
82
|
{schedulePicker ? (
|
|
81
83
|
<section className="core-checkout__section">
|
|
82
|
-
<h3 className="core-checkout__section-title">
|
|
84
|
+
<h3 className="core-checkout__section-title">{t('checkout.schedule.title')}</h3>
|
|
83
85
|
{schedulePicker}
|
|
84
86
|
</section>
|
|
85
87
|
) : null}
|
|
86
88
|
<section className="core-checkout__section">
|
|
87
|
-
<h3 className="core-checkout__payment-title">
|
|
88
|
-
<p className="core-checkout__payment-subtitle">
|
|
89
|
+
<h3 className="core-checkout__payment-title">{t('checkout.payment.title')}</h3>
|
|
90
|
+
<p className="core-checkout__payment-subtitle">{t('checkout.payment.secure')}</p>
|
|
89
91
|
{paymentPicker}
|
|
90
92
|
</section>
|
|
91
93
|
<section className="core-checkout__section">
|
|
@@ -97,7 +99,7 @@ export function DefaultCheckoutShell({
|
|
|
97
99
|
{/* Right pane: off-white — order summary */}
|
|
98
100
|
<div className="core-checkout__page-summary">
|
|
99
101
|
<div className="core-checkout__summary-card">
|
|
100
|
-
<h3 className="core-checkout__section-title">
|
|
102
|
+
<h3 className="core-checkout__section-title">{t('checkout.summary.title')}</h3>
|
|
101
103
|
{cartItems}
|
|
102
104
|
<div className="core-checkout__summary-promo">{promoInput}</div>
|
|
103
105
|
<div className="core-checkout__summary-fees">{feesBreakdown}</div>
|
|
@@ -123,7 +125,7 @@ export function DefaultCheckoutShell({
|
|
|
123
125
|
className={`core-checkout__tab${activeTab === 'summary' ? ' is-active' : ''}`}
|
|
124
126
|
onClick={() => onTabChange('summary')}
|
|
125
127
|
>
|
|
126
|
-
|
|
128
|
+
{t('checkout.tabs.summary')}{totalCount > 0 ? ` (${totalCount})` : ' '}
|
|
127
129
|
</button>
|
|
128
130
|
<button
|
|
129
131
|
type="button"
|
|
@@ -131,24 +133,24 @@ export function DefaultCheckoutShell({
|
|
|
131
133
|
onClick={() => onTabChange('checkout')}
|
|
132
134
|
disabled={totalCount === 0}
|
|
133
135
|
>
|
|
134
|
-
|
|
136
|
+
{t('checkout.tabs.checkout')}
|
|
135
137
|
</button>
|
|
136
138
|
</div>
|
|
137
139
|
|
|
138
140
|
{activeTab === 'summary' ? (
|
|
139
141
|
<div className="core-checkout__summary">
|
|
140
142
|
{totalCount === 0 ? (
|
|
141
|
-
<div className="core-checkout__empty"><p>
|
|
143
|
+
<div className="core-checkout__empty"><p>{t('checkout.summary.empty')}</p></div>
|
|
142
144
|
) : (
|
|
143
145
|
<>
|
|
144
146
|
{cartItems}
|
|
145
147
|
<div className="core-checkout__summary-footer">
|
|
146
148
|
<div className="core-checkout__subtotal">
|
|
147
|
-
<span>
|
|
149
|
+
<span>{t('checkout.summary.subtotal')}</span>
|
|
148
150
|
<span>{formatMoney(subtotal, currency)}</span>
|
|
149
151
|
</div>
|
|
150
152
|
<button type="button" className="core-submit-btn" onClick={() => onTabChange('checkout')}>
|
|
151
|
-
|
|
153
|
+
{t('checkout.summary.proceed')}
|
|
152
154
|
</button>
|
|
153
155
|
</div>
|
|
154
156
|
</>
|
|
@@ -6,6 +6,7 @@ import { createPortal } from 'react-dom';
|
|
|
6
6
|
import Link from 'next/link';
|
|
7
7
|
import type { Page } from '../../types/page';
|
|
8
8
|
import { useHref } from '../../hooks/use-href';
|
|
9
|
+
import { useThemeStrings } from '../../provider';
|
|
9
10
|
import { usePolicyModalStore } from '../../stores/policy-modal-store';
|
|
10
11
|
import { PageRenderer } from '../../page-renderer';
|
|
11
12
|
|
|
@@ -23,6 +24,7 @@ function subscribeNoop(): () => void {
|
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
export function PolicyLinks({ policies }: { policies: Page[] }): JSX.Element | null {
|
|
27
|
+
const t = useThemeStrings();
|
|
26
28
|
const activeSlug = usePolicyModalStore((s) => s.activeSlug);
|
|
27
29
|
const open = usePolicyModalStore((s) => s.open);
|
|
28
30
|
const close = usePolicyModalStore((s) => s.close);
|
|
@@ -59,7 +61,7 @@ export function PolicyLinks({ policies }: { policies: Page[] }): JSX.Element | n
|
|
|
59
61
|
>
|
|
60
62
|
<div className="core-policy-modal__head">
|
|
61
63
|
<h2 className="core-policy-modal__title">{active.title}</h2>
|
|
62
|
-
<button type="button" className="core-policy-modal__close" aria-label=
|
|
64
|
+
<button type="button" className="core-policy-modal__close" aria-label={t('checkout.policy.close')} onClick={close}>
|
|
63
65
|
<svg width="18" height="18" viewBox="0 0 20 20" fill="none" aria-hidden="true">
|
|
64
66
|
<path d="M5 5l10 10M15 5L5 15" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" />
|
|
65
67
|
</svg>
|
|
@@ -70,7 +72,7 @@ export function PolicyLinks({ policies }: { policies: Page[] }): JSX.Element | n
|
|
|
70
72
|
</div>
|
|
71
73
|
<div className="core-policy-modal__foot">
|
|
72
74
|
<Link href={href(`/${active.slug}`)} className="core-policy-modal__full-link" onClick={close}>
|
|
73
|
-
|
|
75
|
+
{t('checkout.policy.view')}
|
|
74
76
|
</Link>
|
|
75
77
|
</div>
|
|
76
78
|
</div>
|
|
@@ -81,7 +83,7 @@ export function PolicyLinks({ policies }: { policies: Page[] }): JSX.Element | n
|
|
|
81
83
|
|
|
82
84
|
return (
|
|
83
85
|
<>
|
|
84
|
-
<nav className="core-checkout-policies" aria-label=
|
|
86
|
+
<nav className="core-checkout-policies" aria-label={t('checkout.policy.list')}>
|
|
85
87
|
{policies.map((policy) => (
|
|
86
88
|
<button
|
|
87
89
|
key={policy.id}
|
package/hooks/use-auth-flow.tsx
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import type { JSX } from 'react';
|
|
4
4
|
import { createContext, useContext, useState, type ReactNode } from 'react';
|
|
5
5
|
import { QueekSdkError, type EmailVerifyOtpResponse } from '@queekai/client-sdk';
|
|
6
|
-
import { useStorefront } from '../provider';
|
|
6
|
+
import { useStorefront, useThemeStrings } from '../provider';
|
|
7
7
|
import { getQueekClient, setAuthTokens, QUEEK_AUTH_URL } from '../sdk/queek-client';
|
|
8
8
|
import { useAuthModalStore } from '../stores/auth-modal-store';
|
|
9
9
|
import { useUserStore } from '../stores/user-store';
|
|
@@ -63,32 +63,33 @@ interface PasswordAuthData {
|
|
|
63
63
|
};
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
function mapEmailError(err: unknown): string {
|
|
66
|
+
export function mapEmailError(err: unknown, t: (key: string) => string): string {
|
|
67
67
|
if (err instanceof QueekSdkError) {
|
|
68
68
|
switch (err.code) {
|
|
69
69
|
case 'invalid_credentials':
|
|
70
|
-
return '
|
|
70
|
+
return t('auth.error.credentials');
|
|
71
71
|
case 'login_mode_mismatch':
|
|
72
|
-
return '
|
|
72
|
+
return t('auth.error.method');
|
|
73
73
|
case 'account_already_exists':
|
|
74
|
-
return '
|
|
74
|
+
return t('auth.error.exists');
|
|
75
75
|
case 'phone_already_exists':
|
|
76
|
-
return '
|
|
76
|
+
return t('auth.error.phone');
|
|
77
77
|
case 'otp_resend_too_soon':
|
|
78
|
-
return '
|
|
78
|
+
return t('auth.error.resend');
|
|
79
79
|
case 'invalid_otp':
|
|
80
|
-
return '
|
|
80
|
+
return t('auth.error.code');
|
|
81
81
|
default:
|
|
82
|
-
return err.message || '
|
|
82
|
+
return err.message || t('auth.error.generic');
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
|
-
return '
|
|
85
|
+
return t('auth.error.generic');
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
const AuthFlowContext = createContext<AuthFlowValue | null>(null);
|
|
89
89
|
|
|
90
90
|
export function AuthFlowProvider({ children }: { children: ReactNode }): JSX.Element {
|
|
91
91
|
const { vendor } = useStorefront();
|
|
92
|
+
const t = useThemeStrings();
|
|
92
93
|
const closeModal = useAuthModalStore((s) => s.close);
|
|
93
94
|
const setUser = useUserStore((s) => s.setUser);
|
|
94
95
|
|
|
@@ -131,7 +132,7 @@ export function AuthFlowProvider({ children }: { children: ReactNode }): JSX.Ele
|
|
|
131
132
|
setOtp('');
|
|
132
133
|
setStep('email-otp-verify');
|
|
133
134
|
} catch (err) {
|
|
134
|
-
setError(mapEmailError(err));
|
|
135
|
+
setError(mapEmailError(err, t));
|
|
135
136
|
} finally {
|
|
136
137
|
setLoading(false);
|
|
137
138
|
}
|
|
@@ -156,7 +157,7 @@ export function AuthFlowProvider({ children }: { children: ReactNode }): JSX.Ele
|
|
|
156
157
|
applyUser(data.user);
|
|
157
158
|
closeModal();
|
|
158
159
|
} catch (err) {
|
|
159
|
-
setError(mapEmailError(err));
|
|
160
|
+
setError(mapEmailError(err, t));
|
|
160
161
|
setOtp('');
|
|
161
162
|
} finally {
|
|
162
163
|
setLoading(false);
|
|
@@ -167,7 +168,7 @@ export function AuthFlowProvider({ children }: { children: ReactNode }): JSX.Ele
|
|
|
167
168
|
|
|
168
169
|
const submitRegister = async (): Promise<void> => {
|
|
169
170
|
if (!firstName.trim()) {
|
|
170
|
-
setError('
|
|
171
|
+
setError(t('auth.error.firstname'));
|
|
171
172
|
return;
|
|
172
173
|
}
|
|
173
174
|
setError(null);
|
|
@@ -183,7 +184,7 @@ export function AuthFlowProvider({ children }: { children: ReactNode }): JSX.Ele
|
|
|
183
184
|
applyUser(res.user);
|
|
184
185
|
closeModal();
|
|
185
186
|
} catch (err) {
|
|
186
|
-
setError(err instanceof QueekSdkError ? err.message : '
|
|
187
|
+
setError(err instanceof QueekSdkError ? err.message : t('auth.error.register'));
|
|
187
188
|
} finally {
|
|
188
189
|
setLoading(false);
|
|
189
190
|
}
|
|
@@ -206,7 +207,7 @@ export function AuthFlowProvider({ children }: { children: ReactNode }): JSX.Ele
|
|
|
206
207
|
applyUser(res.data.user);
|
|
207
208
|
closeModal();
|
|
208
209
|
} catch (err) {
|
|
209
|
-
setError(mapEmailError(err));
|
|
210
|
+
setError(mapEmailError(err, t));
|
|
210
211
|
} finally {
|
|
211
212
|
setLoading(false);
|
|
212
213
|
}
|
|
@@ -230,7 +231,7 @@ export function AuthFlowProvider({ children }: { children: ReactNode }): JSX.Ele
|
|
|
230
231
|
applyUser(res.data.user);
|
|
231
232
|
closeModal();
|
|
232
233
|
} catch (err) {
|
|
233
|
-
setError(mapEmailError(err));
|
|
234
|
+
setError(mapEmailError(err, t));
|
|
234
235
|
} finally {
|
|
235
236
|
setLoading(false);
|
|
236
237
|
}
|
|
@@ -272,7 +273,7 @@ export function AuthFlowProvider({ children }: { children: ReactNode }): JSX.Ele
|
|
|
272
273
|
} catch (err) {
|
|
273
274
|
// eslint-disable-next-line no-console
|
|
274
275
|
console.error('[google-auth] initiate failed', err);
|
|
275
|
-
setError('
|
|
276
|
+
setError(t('auth.error.google'));
|
|
276
277
|
} finally {
|
|
277
278
|
setLoading(false);
|
|
278
279
|
}
|
package/index.ts
CHANGED
|
@@ -48,9 +48,13 @@ export * from './utils/benefit-icon';
|
|
|
48
48
|
export * from './utils/video-embed';
|
|
49
49
|
export * from './utils/match-variant';
|
|
50
50
|
export * from './utils/announcement';
|
|
51
|
+
export * from './utils/locale';
|
|
51
52
|
|
|
52
53
|
// Theme strings — the pure/server-safe i18n core (no next/*, no React).
|
|
53
54
|
export * from './strings/theme-strings';
|
|
55
|
+
// Pseudo-locale QA — dev/test-only dictionary transform + English-leak scan
|
|
56
|
+
// (pure/server-safe: no next/*, no React, so theme-check can import it too).
|
|
57
|
+
export * from './strings/pseudo-locale';
|
|
54
58
|
|
|
55
59
|
// Rendering a theme's own demo.json — what makes local preview possible.
|
|
56
60
|
export * from './demo';
|
package/locales/en.default.json
CHANGED
|
@@ -87,18 +87,29 @@
|
|
|
87
87
|
"title": "Sign in",
|
|
88
88
|
"subtitle": "Enter your email and password",
|
|
89
89
|
"email": "Email address",
|
|
90
|
-
"password": "Password"
|
|
90
|
+
"password": "Password",
|
|
91
|
+
"signing": "Signing in…",
|
|
92
|
+
"create": "Don't have an account? Create one",
|
|
93
|
+
"code": "Use an email code instead"
|
|
91
94
|
},
|
|
92
95
|
"email": {
|
|
93
96
|
"title": "Sign in",
|
|
94
97
|
"subtitle": "Enter your email to continue",
|
|
98
|
+
"placeholder": "your@email.com",
|
|
95
99
|
"or": "or",
|
|
96
|
-
"
|
|
100
|
+
"google": "Continue with Google",
|
|
101
|
+
"sending": "Sending code…",
|
|
102
|
+
"continue": "Continue",
|
|
103
|
+
"password": "Have a password? Sign in with it instead"
|
|
97
104
|
},
|
|
98
105
|
"code": {
|
|
99
106
|
"title": "Enter code",
|
|
100
|
-
"
|
|
101
|
-
"digit": "Digit {index}"
|
|
107
|
+
"lead": "{length}-digit code sent to",
|
|
108
|
+
"digit": "Digit {index}",
|
|
109
|
+
"verifying": "Verifying…",
|
|
110
|
+
"verify": "Verify",
|
|
111
|
+
"resend": "Resend code",
|
|
112
|
+
"change": "← Change email"
|
|
102
113
|
},
|
|
103
114
|
"register": {
|
|
104
115
|
"title": "Create account",
|
|
@@ -106,13 +117,30 @@
|
|
|
106
117
|
"first": "First name",
|
|
107
118
|
"last": "Last name (optional)",
|
|
108
119
|
"email": "Email address",
|
|
109
|
-
"password": "Password (min 8 characters)"
|
|
120
|
+
"password": "Password (min 8 characters)",
|
|
121
|
+
"creating": "Creating account…",
|
|
122
|
+
"signin": "Already have an account? Sign in"
|
|
110
123
|
},
|
|
111
124
|
"signup": {
|
|
112
125
|
"title": "Create your account",
|
|
113
126
|
"subtitle": "Just a few details to get you started",
|
|
114
127
|
"first": "First name",
|
|
115
|
-
"last": "Last name (optional)"
|
|
128
|
+
"last": "Last name (optional)",
|
|
129
|
+
"create": "Create account",
|
|
130
|
+
"creating": "Creating account…",
|
|
131
|
+
"different": "← Use a different email"
|
|
132
|
+
},
|
|
133
|
+
"error": {
|
|
134
|
+
"credentials": "Wrong email or password. Please try again.",
|
|
135
|
+
"method": "This account uses a different sign-in method (Google or phone).",
|
|
136
|
+
"exists": "Email already registered. Please sign in instead.",
|
|
137
|
+
"phone": "Phone number already in use.",
|
|
138
|
+
"resend": "Please wait before requesting another code.",
|
|
139
|
+
"code": "Incorrect code. Please try again.",
|
|
140
|
+
"generic": "Something went wrong. Please try again.",
|
|
141
|
+
"firstname": "Please enter your first name.",
|
|
142
|
+
"register": "Registration failed. Try again.",
|
|
143
|
+
"google": "Could not connect to Google sign-in. Please try again."
|
|
116
144
|
}
|
|
117
145
|
},
|
|
118
146
|
"blog": {
|
|
@@ -132,7 +160,17 @@
|
|
|
132
160
|
"x": "Share on X",
|
|
133
161
|
"facebook": "Share on Facebook",
|
|
134
162
|
"whatsapp": "Share on WhatsApp",
|
|
135
|
-
"copy": "Copy link"
|
|
163
|
+
"copy": "Copy link",
|
|
164
|
+
"copied": "Copied"
|
|
165
|
+
},
|
|
166
|
+
"related": {
|
|
167
|
+
"title": "Related posts"
|
|
168
|
+
},
|
|
169
|
+
"meta": {
|
|
170
|
+
"reading": {
|
|
171
|
+
"one": "{count} min read",
|
|
172
|
+
"other": "{count} min read"
|
|
173
|
+
}
|
|
136
174
|
}
|
|
137
175
|
}
|
|
138
176
|
}
|