@akinon/pz-theme 2.0.117-rc.0 → 2.0.118-beta.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/CHANGELOG.md +15 -35
- package/package.json +4 -4
- package/src/blocks/__tests__/locale-links.test.tsx +429 -0
- package/src/blocks/account-button-block.tsx +1 -1
- package/src/blocks/account-menu-block.tsx +9 -3
- package/src/blocks/applied-filters-block.tsx +7 -1
- package/src/blocks/auth-form-block.tsx +7 -4
- package/src/blocks/basket-items-block.tsx +1 -1
- package/src/blocks/basket-summary-block.tsx +1 -1
- package/src/blocks/button-block.tsx +5 -1
- package/src/blocks/carousel-item-block.tsx +1 -1
- package/src/blocks/coupon-wallet-block.tsx +3 -1
- package/src/blocks/facet-filters-block.tsx +14 -10
- package/src/blocks/favorite-button-block.tsx +1 -1
- package/src/blocks/favourites-grid-block.tsx +1 -1
- package/src/blocks/group-block.tsx +19 -6
- package/src/blocks/hamburger-menu-block.tsx +1 -1
- package/src/blocks/hotspot-block.tsx +3 -1
- package/src/blocks/hover-showcase-item-block.tsx +3 -1
- package/src/blocks/link-block.tsx +5 -22
- package/src/blocks/listing-breadcrumb-block.tsx +1 -1
- package/src/blocks/listing-pagination-block.tsx +11 -3
- package/src/blocks/mega-menu-block.tsx +9 -3
- package/src/blocks/mini-basket-block.tsx +1 -1
- package/src/blocks/nav-menu-block.tsx +9 -3
- package/src/blocks/oauth-buttons-block.tsx +4 -0
- package/src/blocks/order-detail-block.tsx +1 -1
- package/src/blocks/order-list-block.tsx +1 -1
- package/src/blocks/order-success-block.tsx +1 -1
- package/src/blocks/otp-form-block.tsx +4 -1
- package/src/blocks/payment-badges-block.tsx +1 -1
- package/src/blocks/product-breadcrumb-block.tsx +1 -1
- package/src/blocks/product-grid-block.tsx +1 -1
- package/src/blocks/product-recommendations-block.tsx +1 -1
- package/src/blocks/register-form-block.tsx +6 -3
- package/src/blocks/search-box-block.tsx +7 -1
- package/src/blocks/social-icons-list-block.tsx +1 -1
- package/src/blocks/social-links-block.tsx +1 -1
- package/src/blocks/sort-select-block.tsx +8 -2
- package/src/blocks/store-locator-block.tsx +3 -1
- package/src/blocks/text-block.tsx +8 -1
- package/src/blocks/variant-selector-block.tsx +6 -2
- package/src/blocks/wishlist-button-block.tsx +1 -1
- package/src/components/__tests__/theme-link.test.tsx +169 -0
- package/src/components/theme-link.tsx +36 -0
- package/src/custom/README-sections.md +6 -0
- package/src/custom/README.md +71 -0
- package/src/custom/__tests__/custom-entry-exports.test.tsx +13 -0
- package/src/custom/__tests__/theme-editable.test.tsx +179 -0
- package/src/custom/__tests__/use-theme-setting.test.tsx +101 -0
- package/src/custom/index.ts +16 -0
- package/src/custom/theme-editable.tsx +57 -4
- package/src/custom/use-theme-setting.ts +77 -10
- package/src/hooks/use-localize-href.ts +34 -0
- package/src/index.ts +12 -0
- package/src/sections/find-in-store-section.tsx +3 -1
- package/src/sections/hover-showcase-section.tsx +6 -2
- package/src/sections/installment-options-section.tsx +3 -1
- package/src/sections/slider-quick-view.tsx +1 -1
- package/src/utils/__tests__/localize-href.test.ts +328 -0
- package/src/utils/localize-href.ts +192 -0
|
@@ -231,6 +231,107 @@ describe('useThemeSetting — used outside a custom block', () => {
|
|
|
231
231
|
});
|
|
232
232
|
});
|
|
233
233
|
|
|
234
|
+
/**
|
|
235
|
+
* A `text` default written per language: the author's own copy follows the
|
|
236
|
+
* page's language, while the editor keeps seeing one plain string. Turkish is
|
|
237
|
+
* the hidden default and English lives under /en, which is zsa-zsa-zsu's setup.
|
|
238
|
+
*/
|
|
239
|
+
describe('useThemeSetting — a text default written per language', () => {
|
|
240
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
241
|
+
const hooks = require('@akinon/next/hooks');
|
|
242
|
+
let localizationSpy: jest.SpyInstance;
|
|
243
|
+
|
|
244
|
+
const onPage = (locale: string) => {
|
|
245
|
+
const original = hooks.useLocalization;
|
|
246
|
+
localizationSpy = jest
|
|
247
|
+
.spyOn(hooks, 'useLocalization')
|
|
248
|
+
.mockImplementation(() => ({
|
|
249
|
+
...original(),
|
|
250
|
+
locale,
|
|
251
|
+
defaultLocaleValue: 'tr',
|
|
252
|
+
locales: [{ value: 'tr' }, { value: 'en' }]
|
|
253
|
+
}));
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
afterEach(() => localizationSpy?.mockRestore());
|
|
257
|
+
|
|
258
|
+
const LABEL = { tr: 'MENÜ', en: 'MENU' };
|
|
259
|
+
|
|
260
|
+
const read = (
|
|
261
|
+
value: CustomBlockContextValue | null,
|
|
262
|
+
fallback: unknown = LABEL
|
|
263
|
+
) =>
|
|
264
|
+
renderHook(
|
|
265
|
+
() =>
|
|
266
|
+
useThemeSetting('menuLabel', {
|
|
267
|
+
type: 'text',
|
|
268
|
+
label: 'Menu label',
|
|
269
|
+
default: fallback as Record<string, string>
|
|
270
|
+
}),
|
|
271
|
+
{ wrapper: wrapperFor(value) }
|
|
272
|
+
).result.current;
|
|
273
|
+
|
|
274
|
+
it('reads the entry for the page’s language when nothing is stored', () => {
|
|
275
|
+
onPage('en');
|
|
276
|
+
expect(read(contextValue())).toBe('MENU');
|
|
277
|
+
|
|
278
|
+
localizationSpy.mockRestore();
|
|
279
|
+
onPage('tr');
|
|
280
|
+
expect(read(contextValue())).toBe('MENÜ');
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('translates the default the editor seeded when the block was placed', () => {
|
|
284
|
+
onPage('en');
|
|
285
|
+
const seeded = contextValue({
|
|
286
|
+
properties: { menuLabel: { desktop: 'MENÜ' } }
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
expect(read(seeded)).toBe('MENU');
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('shows the merchant’s own text as typed, in every language', () => {
|
|
293
|
+
onPage('en');
|
|
294
|
+
const typed = contextValue({
|
|
295
|
+
properties: { menuLabel: { desktop: 'Kategoriler' } }
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
expect(read(typed)).toBe('Kategoriler');
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('tells the editor the default language’s text, as one plain string', () => {
|
|
302
|
+
onPage('en');
|
|
303
|
+
const sink = jest.fn();
|
|
304
|
+
read(contextValue({ sink }));
|
|
305
|
+
|
|
306
|
+
expect(sink.mock.calls[0][0]).toMatchObject({
|
|
307
|
+
key: 'menuLabel',
|
|
308
|
+
type: 'text',
|
|
309
|
+
defaultValue: { desktop: 'MENÜ' }
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('falls back to the default language for a language the author skipped', () => {
|
|
314
|
+
onPage('en');
|
|
315
|
+
expect(read(contextValue(), { tr: 'Tümü' })).toBe('Tümü');
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('reads the page’s entry outside a custom block too', () => {
|
|
319
|
+
onPage('en');
|
|
320
|
+
const warn = jest
|
|
321
|
+
.spyOn(console, 'warn')
|
|
322
|
+
.mockImplementation(() => undefined);
|
|
323
|
+
|
|
324
|
+
expect(read(null)).toBe('MENU');
|
|
325
|
+
|
|
326
|
+
warn.mockRestore();
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('leaves a one-string default exactly as it was', () => {
|
|
330
|
+
onPage('en');
|
|
331
|
+
expect(read(contextValue(), 'MENÜ')).toBe('MENÜ');
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
|
|
234
335
|
describe('useThemeSetting — invalid declarations throw in development', () => {
|
|
235
336
|
const renderWith = (key: string, options: Record<string, unknown>) =>
|
|
236
337
|
renderHook(
|
package/src/custom/index.ts
CHANGED
|
@@ -137,6 +137,22 @@ export type {
|
|
|
137
137
|
ThemeSettingOptions
|
|
138
138
|
} from './use-theme-setting';
|
|
139
139
|
|
|
140
|
+
/**
|
|
141
|
+
* LINKS AND THE PAGE'S LOCALE. Storefront paths are stored and authored
|
|
142
|
+
* without one ('/yatak-odasi/'), which on an English page is the Turkish site.
|
|
143
|
+
* The `<a>` that `ThemeEditable` wraps gets the locale on its own. For any
|
|
144
|
+
* other link use `ThemeLink` in place of `next/link`, and for a bare string — a
|
|
145
|
+
* plain `<a>`, `router.push`, an address read off commerce data — run it
|
|
146
|
+
* through the function `useLocalizeHref()` returns. Nothing else in a block has
|
|
147
|
+
* to know which language the page is in.
|
|
148
|
+
*
|
|
149
|
+
* This entry, not the package root: the root re-exports `ThemePlaceholder`,
|
|
150
|
+
* which is server-only, so a client component importing from there fails the
|
|
151
|
+
* build.
|
|
152
|
+
*/
|
|
153
|
+
export { ThemeLink } from '../components/theme-link';
|
|
154
|
+
export { useLocalizeHref } from '../hooks/use-localize-href';
|
|
155
|
+
|
|
140
156
|
/**
|
|
141
157
|
* The context every custom unit renders inside, for the one question the
|
|
142
158
|
* components above cannot answer for a developer: is this the editor canvas?
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import React from 'react';
|
|
4
4
|
|
|
5
|
+
import { useLocalizeHref } from '../hooks/use-localize-href';
|
|
5
6
|
import { getResponsiveValue } from '../utils';
|
|
6
7
|
import { devWarn } from '../utils/dev-warn';
|
|
8
|
+
import { useValueLocale } from '../utils/localized-value';
|
|
7
9
|
import { partAttrs } from '../utils/part-styles';
|
|
8
10
|
import { useCustomBlockContext } from './block-context';
|
|
9
11
|
import type { CustomBlockContextValue } from './block-context';
|
|
@@ -113,6 +115,15 @@ export interface ThemeEditableProps {
|
|
|
113
115
|
name: string;
|
|
114
116
|
/** Overrides tag inference. The escape hatch for a container tag. */
|
|
115
117
|
type?: EditableKind;
|
|
118
|
+
/**
|
|
119
|
+
* The authored text in the store's other languages, keyed by locale:
|
|
120
|
+
* `<ThemeEditable name="label" translations={{ en: 'MENU' }}><span>MENÜ</span>`.
|
|
121
|
+
* The child stays the default language's text, which is all the editor is
|
|
122
|
+
* told. Shown for as long as the field still says that text, including the
|
|
123
|
+
* copy the editor seeds when the block is placed. Text the merchant typed is
|
|
124
|
+
* theirs and shows as typed. Text and link labels only.
|
|
125
|
+
*/
|
|
126
|
+
translations?: Readonly<Record<string, string>>;
|
|
116
127
|
children?: React.ReactNode;
|
|
117
128
|
}
|
|
118
129
|
|
|
@@ -148,13 +159,20 @@ export interface ThemeEditableProps {
|
|
|
148
159
|
export function ThemeEditable({
|
|
149
160
|
name,
|
|
150
161
|
type,
|
|
162
|
+
translations,
|
|
151
163
|
children
|
|
152
164
|
}: ThemeEditableProps): React.ReactElement {
|
|
153
165
|
const context = useCustomBlockContext();
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
//
|
|
166
|
+
const localizeHref = useLocalizeHref();
|
|
167
|
+
const valueLocale = useValueLocale();
|
|
168
|
+
|
|
169
|
+
// Untouched rather than empty, so a `<ThemeEditable>` copy-pasted outside a
|
|
170
|
+
// custom block keeps rendering what the developer wrote while the warning
|
|
171
|
+
// tells them why nothing is editable. What it does need is a Next app router
|
|
172
|
+
// page and the `LocalizationProvider` around it: the two hooks above reach
|
|
173
|
+
// `@akinon/next`'s `useLocalization`, which calls `next/navigation`'s
|
|
174
|
+
// `useRouter`. A test gets there with a mocked `@akinon/next/hooks`, the way
|
|
175
|
+
// this package's jest config does.
|
|
158
176
|
if (!context) {
|
|
159
177
|
devWarn(
|
|
160
178
|
'ThemeEditable',
|
|
@@ -314,6 +332,25 @@ export function ThemeEditable({
|
|
|
314
332
|
)
|
|
315
333
|
};
|
|
316
334
|
|
|
335
|
+
// The author's own text, still untouched, in the page's language. Compared
|
|
336
|
+
// with the authored child rather than "nothing stored" because the editor
|
|
337
|
+
// stores that child as the value when the block is placed: a trigger placed
|
|
338
|
+
// on a Turkish store carries `label: { desktop: 'MENÜ' }` from day one.
|
|
339
|
+
const translated =
|
|
340
|
+
valueProp === 'children' &&
|
|
341
|
+
valueLocale.locale &&
|
|
342
|
+
valueLocale.locale !== valueLocale.defaultLocale
|
|
343
|
+
? translations?.[valueLocale.locale]
|
|
344
|
+
: undefined;
|
|
345
|
+
|
|
346
|
+
if (
|
|
347
|
+
typeof translated === 'string' &&
|
|
348
|
+
translated !== '' &&
|
|
349
|
+
nextProps[valueProp] === authoredValue
|
|
350
|
+
) {
|
|
351
|
+
nextProps[valueProp] = translated;
|
|
352
|
+
}
|
|
353
|
+
|
|
317
354
|
if (hrefKey) {
|
|
318
355
|
// A destination is a plain string in every control that writes one, so it
|
|
319
356
|
// never carries the picker's `{ url }` wrapper.
|
|
@@ -325,6 +362,22 @@ export function ThemeEditable({
|
|
|
325
362
|
);
|
|
326
363
|
}
|
|
327
364
|
|
|
365
|
+
// The page's locale goes on the destination here, once, for every custom
|
|
366
|
+
// block and section. The stored address and the authored JSX both say
|
|
367
|
+
// '/yatak-odasi/', which on an English page is the Turkish site, and a
|
|
368
|
+
// developer who had to remember `/en` on each link would forget one. Only a
|
|
369
|
+
// root-relative href changes (see `localizeHref`); the stored value is left
|
|
370
|
+
// alone, because the editor reads properties, never the rendered anchor.
|
|
371
|
+
const destination = hrefKey
|
|
372
|
+
? nextProps.href
|
|
373
|
+
: tag === 'a'
|
|
374
|
+
? childProps.href
|
|
375
|
+
: undefined;
|
|
376
|
+
|
|
377
|
+
if (typeof destination === 'string') {
|
|
378
|
+
nextProps.href = localizeHref(destination);
|
|
379
|
+
}
|
|
380
|
+
|
|
328
381
|
if (stampsFieldPart) {
|
|
329
382
|
// `use-designer-features.tsx` resolves the nearest `[data-part]` under the
|
|
330
383
|
// click and ships its value as `partName` on SELECT_BLOCK, so this one
|
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
import { getResponsiveValue } from '../utils';
|
|
4
4
|
import { devWarn } from '../utils/dev-warn';
|
|
5
|
+
import {
|
|
6
|
+
isLocalizedValue,
|
|
7
|
+
resolveLocalizedValue,
|
|
8
|
+
useValueLocale
|
|
9
|
+
} from '../utils/localized-value';
|
|
10
|
+
import type { ValueLocale } from '../utils/localized-value';
|
|
5
11
|
import { useCustomBlockContext } from './block-context';
|
|
6
12
|
import {
|
|
7
13
|
normalizeSettingChoices,
|
|
@@ -52,13 +58,22 @@ interface SettingValueByType {
|
|
|
52
58
|
*/
|
|
53
59
|
type NoInferred<T> = [T][T extends unknown ? 0 : never];
|
|
54
60
|
|
|
61
|
+
/**
|
|
62
|
+
* A `text` default may be written per language — `{ tr: 'MENÜ', en: 'MENU' }`
|
|
63
|
+
* — and the hook still returns one string: the entry for the page's locale.
|
|
64
|
+
* See `splitDefault`.
|
|
65
|
+
*/
|
|
66
|
+
type DefaultFor<TType, TValue> = TType extends 'text'
|
|
67
|
+
? NoInferred<TValue> | Readonly<Record<string, string>>
|
|
68
|
+
: NoInferred<TValue>;
|
|
69
|
+
|
|
55
70
|
export interface ThemeSettingOptions<
|
|
56
71
|
TType extends PropertyType = PropertyType,
|
|
57
72
|
TValue = unknown
|
|
58
73
|
> {
|
|
59
74
|
type: TType;
|
|
60
75
|
label: string;
|
|
61
|
-
default?:
|
|
76
|
+
default?: DefaultFor<TType, TValue>;
|
|
62
77
|
options?: ReadonlyArray<ThemeSettingChoice>;
|
|
63
78
|
min?: number;
|
|
64
79
|
max?: number;
|
|
@@ -156,6 +171,41 @@ const resolveAuthoredDefault = (
|
|
|
156
171
|
return resolved === NOT_AUTHORED ? value : resolved;
|
|
157
172
|
};
|
|
158
173
|
|
|
174
|
+
/**
|
|
175
|
+
* A `text` default written per language, split into its two readings.
|
|
176
|
+
*
|
|
177
|
+
* `base` is the default language's entry. It is what the editor is told the
|
|
178
|
+
* default is, so the panel, the seed a newly placed block is stored with and
|
|
179
|
+
* the static `settings` declaration all keep the plain string they always had,
|
|
180
|
+
* and a catalog declaring one is accepted by an editor that has never heard of
|
|
181
|
+
* languages. `page` is the entry for the locale being shown.
|
|
182
|
+
*
|
|
183
|
+
* Any other default, and a `text` default written as one string, reads the
|
|
184
|
+
* same both ways.
|
|
185
|
+
*/
|
|
186
|
+
const splitDefault = (
|
|
187
|
+
declaration: ThemeSettingOptions<PropertyType, unknown>,
|
|
188
|
+
valueLocale: ValueLocale
|
|
189
|
+
): { base: unknown; page: unknown; perLocale: boolean } => {
|
|
190
|
+
const value = declaration.default;
|
|
191
|
+
|
|
192
|
+
if (
|
|
193
|
+
declaration.type !== 'text' ||
|
|
194
|
+
!isLocalizedValue(value, valueLocale.locales)
|
|
195
|
+
) {
|
|
196
|
+
return { base: value, page: value, perLocale: false };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
base: resolveLocalizedValue(value, {
|
|
201
|
+
...valueLocale,
|
|
202
|
+
locale: valueLocale.defaultLocale
|
|
203
|
+
}),
|
|
204
|
+
page: resolveLocalizedValue(value, valueLocale),
|
|
205
|
+
perLocale: true
|
|
206
|
+
};
|
|
207
|
+
};
|
|
208
|
+
|
|
159
209
|
/**
|
|
160
210
|
* Declares one merchant-editable value that has no on-screen representation —
|
|
161
211
|
* how many products a grid shows, which category it pulls from, whether a
|
|
@@ -181,13 +231,21 @@ export function useThemeSetting<
|
|
|
181
231
|
TValue = SettingValueByType[TType]
|
|
182
232
|
>(key: string, options: ThemeSettingOptions<TType, TValue>): TValue {
|
|
183
233
|
const context = useCustomBlockContext();
|
|
234
|
+
const valueLocale = useValueLocale();
|
|
184
235
|
const declaration = options as ThemeSettingOptions<PropertyType, unknown>;
|
|
185
236
|
|
|
186
237
|
assertDeclarationIsValid(key, declaration);
|
|
187
238
|
|
|
239
|
+
const defaults = splitDefault(declaration, valueLocale);
|
|
240
|
+
|
|
188
241
|
if (!context) {
|
|
189
|
-
//
|
|
190
|
-
// a storybook or a unit test should see the
|
|
242
|
+
// A missing block context is never an error on its own: a developer
|
|
243
|
+
// previewing the component in a storybook or a unit test should see the
|
|
244
|
+
// declared defaults, not a crash. The tree around it is not free of
|
|
245
|
+
// requirements, though — `useValueLocale` above reads `@akinon/next`'s
|
|
246
|
+
// `useLocalization`, which calls `next/navigation`'s `useRouter` — so the
|
|
247
|
+
// render still needs a Next app router and the `LocalizationProvider`, or
|
|
248
|
+
// a mocked `@akinon/next/hooks` the way this package's jest config does.
|
|
191
249
|
// The key rides in the message so distinct misplacements dedupe
|
|
192
250
|
// independently inside `devWarn`.
|
|
193
251
|
devWarn(
|
|
@@ -196,13 +254,15 @@ export function useThemeSetting<
|
|
|
196
254
|
`unreachable and the declared default is used instead. Render this ` +
|
|
197
255
|
`component through a block registered with registerCustomBlocks().`
|
|
198
256
|
);
|
|
199
|
-
return
|
|
257
|
+
return defaults.page as TValue;
|
|
200
258
|
}
|
|
201
259
|
|
|
202
260
|
// Reported on every render rather than once from an effect: the collection
|
|
203
261
|
// pass renders the component and reads the sink synchronously, and on the
|
|
204
262
|
// server effects never run at all.
|
|
205
|
-
context.collect?.setting(
|
|
263
|
+
context.collect?.setting(
|
|
264
|
+
buildDescriptor(key, { ...declaration, default: defaults.base })
|
|
265
|
+
);
|
|
206
266
|
|
|
207
267
|
const breakpoint = context.currentBreakpoint;
|
|
208
268
|
|
|
@@ -219,9 +279,16 @@ export function useThemeSetting<
|
|
|
219
279
|
// undefined.
|
|
220
280
|
const resolved = getResponsiveValue(stored, breakpoint, NOT_AUTHORED);
|
|
221
281
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
282
|
+
if (resolved === NOT_AUTHORED) {
|
|
283
|
+
return resolveAuthoredDefault(defaults.page, breakpoint) as TValue;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// The default-language text as the editor seeded it when the block was
|
|
287
|
+
// placed, never changed since: still the author's copy, so it follows the
|
|
288
|
+
// page's language. Anything the merchant typed is theirs and shows as typed.
|
|
289
|
+
if (defaults.perLocale && resolved === defaults.base) {
|
|
290
|
+
return defaults.page as TValue;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return resolved as TValue;
|
|
227
294
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback } from 'react';
|
|
4
|
+
import { useLocalization } from '@akinon/next/hooks';
|
|
5
|
+
import { urlLocaleMatcherRegex } from '@akinon/next/utils';
|
|
6
|
+
import { localizeHref } from '../utils/localize-href';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* `localizeHref` bound to the active locale, for anything that navigates
|
|
10
|
+
* without a `ThemeLink`: `router.push`, a plain `<a>`, a fetched page URL.
|
|
11
|
+
*
|
|
12
|
+
* Everything that localizes goes through this hook rather than calling
|
|
13
|
+
* `localizeHref` itself, because this is the only place that knows the
|
|
14
|
+
* storefront's own address — and without it a merchant's full URL to this same
|
|
15
|
+
* site would not be recognized as a path. `NEXT_PUBLIC_URL` is a public env
|
|
16
|
+
* var, so Next inlines the same literal into the server render and the client
|
|
17
|
+
* bundle, and the two cannot disagree about which links are internal.
|
|
18
|
+
*/
|
|
19
|
+
export const useLocalizeHref = (): ((href: string) => string) => {
|
|
20
|
+
const { locale, defaultLocaleValue, localeUrlStrategy } = useLocalization();
|
|
21
|
+
const siteUrl = process.env.NEXT_PUBLIC_URL;
|
|
22
|
+
|
|
23
|
+
return useCallback(
|
|
24
|
+
(href: string) =>
|
|
25
|
+
localizeHref(href, {
|
|
26
|
+
locale,
|
|
27
|
+
defaultLocaleValue,
|
|
28
|
+
localeUrlStrategy,
|
|
29
|
+
localeMatcher: urlLocaleMatcherRegex,
|
|
30
|
+
siteUrl
|
|
31
|
+
}),
|
|
32
|
+
[locale, defaultLocaleValue, localeUrlStrategy, siteUrl]
|
|
33
|
+
);
|
|
34
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -27,6 +27,18 @@ export { isBrowseMode, useBrowseMode } from './browse-mode';
|
|
|
27
27
|
|
|
28
28
|
export { WithDesignerFeatures } from './components/with-designer-features';
|
|
29
29
|
|
|
30
|
+
// The locale rules as plain functions, for code that already has the store's
|
|
31
|
+
// localization settings in hand. `ThemeLink` and `useLocalizeHref` are NOT
|
|
32
|
+
// here: this entry re-exports `ThemePlaceholder`, which imports 'server-only',
|
|
33
|
+
// so importing anything from it inside a client component fails the build. A
|
|
34
|
+
// brand's custom section takes both from '@akinon/pz-theme/src/custom'.
|
|
35
|
+
export {
|
|
36
|
+
isRootRelativeHref,
|
|
37
|
+
localizeHref,
|
|
38
|
+
localizeHtmlLinks
|
|
39
|
+
} from './utils/localize-href';
|
|
40
|
+
export type { HrefLocale } from './utils/localize-href';
|
|
41
|
+
|
|
30
42
|
export type { Section } from './theme-section';
|
|
31
43
|
export type { Block } from './theme-block';
|
|
32
44
|
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
resolveThemeCssVariables
|
|
29
29
|
} from '../utils';
|
|
30
30
|
import { isNarrowBreakpoint } from '../utils/breakpoints';
|
|
31
|
+
import { useLocalizeHref } from '../hooks/use-localize-href';
|
|
31
32
|
|
|
32
33
|
import { postToEditor } from '../editor-bridge';
|
|
33
34
|
import { PreviewToEditorMessageType } from '@akinon/theme-contract';
|
|
@@ -534,6 +535,7 @@ export default function FindInStoreSection({
|
|
|
534
535
|
selectedBlockId = null
|
|
535
536
|
}: FindInStoreSectionProps) {
|
|
536
537
|
const { t } = useLocalization();
|
|
538
|
+
const localize = useLocalizeHref();
|
|
537
539
|
const themeSettings = useThemeSettingsContext();
|
|
538
540
|
|
|
539
541
|
const [productUrlInput, setProductUrlInput] = useState('');
|
|
@@ -1462,7 +1464,7 @@ export default function FindInStoreSection({
|
|
|
1462
1464
|
)
|
|
1463
1465
|
) : productUrl ? (
|
|
1464
1466
|
<a
|
|
1465
|
-
href={productUrl}
|
|
1467
|
+
href={localize(productUrl)}
|
|
1466
1468
|
className="text-[13px] font-semibold text-slate-800 underline underline-offset-4"
|
|
1467
1469
|
>
|
|
1468
1470
|
View product
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { useState, useEffect } from 'react';
|
|
4
4
|
import { useLocalization } from '@akinon/next/hooks';
|
|
5
|
+
import { useLocalizeHref } from '../hooks/use-localize-href';
|
|
5
6
|
import ThemeBlock, { Block } from '../theme-block';
|
|
6
7
|
import { useThemeSettingsContext } from '../theme-settings-context';
|
|
7
8
|
import {
|
|
@@ -32,6 +33,9 @@ const HoverShowcaseSection: React.FC<HoverShowcaseSectionProps> = ({
|
|
|
32
33
|
selectedBlockId = null
|
|
33
34
|
}) => {
|
|
34
35
|
const { locale } = useLocalization();
|
|
36
|
+
// The section draws its items itself (never through the item block), so
|
|
37
|
+
// their plain anchors need the active locale added here.
|
|
38
|
+
const localize = useLocalizeHref();
|
|
35
39
|
const defaultLocale = process.env.NEXT_PUBLIC_DEFAULT_LOCALE || 'en';
|
|
36
40
|
const themeSettings = useThemeSettingsContext();
|
|
37
41
|
|
|
@@ -229,7 +233,7 @@ const HoverShowcaseSection: React.FC<HoverShowcaseSectionProps> = ({
|
|
|
229
233
|
onClick={() => setActiveIndex(index)}
|
|
230
234
|
>
|
|
231
235
|
{link ? (
|
|
232
|
-
<a href={link} className="decoration-none">
|
|
236
|
+
<a href={localize(link)} className="decoration-none">
|
|
233
237
|
<h3
|
|
234
238
|
style={{
|
|
235
239
|
fontSize: String(titleFontSize),
|
|
@@ -378,7 +382,7 @@ const HoverShowcaseSection: React.FC<HoverShowcaseSectionProps> = ({
|
|
|
378
382
|
onMouseEnter={() => setActiveIndex(index)}
|
|
379
383
|
>
|
|
380
384
|
{link ? (
|
|
381
|
-
<a href={link} className="decoration-none">
|
|
385
|
+
<a href={localize(link)} className="decoration-none">
|
|
382
386
|
<h3
|
|
383
387
|
style={{
|
|
384
388
|
fontSize: String(titleFontSize),
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
resolveThemeCssVariables
|
|
22
22
|
} from '../utils';
|
|
23
23
|
import { isNarrowBreakpoint } from '../utils/breakpoints';
|
|
24
|
+
import { useLocalizeHref } from '../hooks/use-localize-href';
|
|
24
25
|
|
|
25
26
|
import { postToEditor } from '../editor-bridge';
|
|
26
27
|
import { PreviewToEditorMessageType } from '@akinon/theme-contract';
|
|
@@ -416,6 +417,7 @@ export default function InstallmentOptionsSection({
|
|
|
416
417
|
}: InstallmentOptionsSectionProps) {
|
|
417
418
|
const themeSettings = useThemeSettingsContext();
|
|
418
419
|
const { t } = useLocalization();
|
|
420
|
+
const localize = useLocalizeHref();
|
|
419
421
|
const isMobile = isNarrowBreakpoint(currentBreakpoint);
|
|
420
422
|
|
|
421
423
|
const maxWidth = getResponsiveValue(
|
|
@@ -833,7 +835,7 @@ export default function InstallmentOptionsSection({
|
|
|
833
835
|
</p>
|
|
834
836
|
{productUrl && productUrl !== '#' && (
|
|
835
837
|
<a
|
|
836
|
-
href={productUrl}
|
|
838
|
+
href={localize(productUrl)}
|
|
837
839
|
className="text-xs font-semibold text-[#0f172a] underline underline-offset-4"
|
|
838
840
|
>
|
|
839
841
|
View product
|