Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 15 additions & 17 deletions packages/api/src/platforms/vtex/resolvers/product.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import type { EnhancedCommercialOffer } from '../utils/enhanceCommercialOffer'
import { enhanceCommercialOffer } from '../utils/enhanceCommercialOffer'
import {
getConfiguredLocales,
getDefaultLocale,
isLocalizationEnabled,
} from '../utils/localization'
import { bestOfferFirst } from '../utils/productStock'
Expand Down Expand Up @@ -312,7 +311,6 @@ export const StoreProduct: Record<string, GraphqlResolver<Root>> & {
const productId = root.isVariantOf.productId
const itemId = root.itemId
const locale = ctx.storage.locale
const defaultLocale = getDefaultLocale(ctx)

// availableLinkIds returns localized slug for every locale,
// we fetch for the current locale (reusing the request-scoped cache shared with the slug and
Expand All @@ -326,21 +324,21 @@ export const StoreProduct: Record<string, GraphqlResolver<Root>> & {

return configuredLocales
.map((configuredLocale) => {
// The default locale always uses the canonical IS linkText: it is always
// present and matches the Query.product `slug.startsWith(linkText)` fast
// path, so the fallback URL resolves cleanly even when the catalog has no
// default-locale entry in availableLinkIds.
if (configuredLocale === defaultLocale) {
return { locale: configuredLocale, slug: getSlug(linkText, itemId) }
}

// Non-default locales only appear when they have a registered localized slug
// in availableLinkIds. Untranslated locales are omitted so they are never
// advertised as hreflang alternates — this keeps the hreflang cluster
// symmetric across all locale variants of the product (every variant emits
// the same set: default + translated locales). The LocalizationSelector
// falls back to the default slug under the target prefix for omitted locales.
const linkId = availableLinkIds[configuredLocale]
// Intelligent Search localizes linkText to the locale being browsed, so it
// can only stand in for that locale. Using it for the default locale while
// browsing another one produces the browsed locale's slug under the default
// locale's prefix, which 404s.
const linkId =
availableLinkIds[configuredLocale] ??
(configuredLocale === locale ? linkText : undefined)

// Locales with no registered localized slug are omitted rather than
// guessed, so an alternate is only ever advertised for a slug the catalog
// actually resolves. A product with no translations at all therefore
// advertises only the locale being browsed; emitting the untranslated slug
// for every locale would advertise URLs for locales that may not sell the
// product. The LocalizationSelector falls back to the default slug under
// the target prefix for omitted locales.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
return linkId
? { locale: configuredLocale, slug: getSlug(linkId, itemId) }
: null
Expand Down
79 changes: 79 additions & 0 deletions packages/api/test/unit/platforms/vtex/resolvers/product.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,85 @@ describe('StoreProduct', () => {
expect(result?.find((e: any) => e.locale === 'pt-BR')).toBeUndefined()
})

it('resolves the default locale from availableLinkIds while browsing another locale', async () => {
// Intelligent Search localizes linkText to the locale being browsed, so a
// pt-BR request reports the pt-BR slug as linkText. The en-US alternate must
// come from availableLinkIds, otherwise it points at a URL that 404s.
const getLocalizedProduct = vi.fn().mockResolvedValueOnce({
linkId: 'camisa-azul',
categories: [],
availableLinkIds: { 'en-US': 'blue-shirt', 'pt-BR': 'camisa-azul' },
})

const root = makeRoot({ linkText: 'camisa-azul' })
const ctx = makeCtx({
localizationEnabled: true,
locale: 'pt-BR',
locales: { 'en-US': {}, 'pt-BR': {} },
defaultLocale: 'en-US',
getLocalizedProduct,
})

const result = await (StoreProduct.otherLocales as any)(root, {}, ctx)

expect(result).toContainEqual({ locale: 'en-US', slug: 'blue-shirt-100' })
expect(result).toContainEqual({
locale: 'pt-BR',
slug: 'camisa-azul-100',
})
// The whole map comes from a single response — no per-locale fan-out.
expect(getLocalizedProduct).toHaveBeenCalledTimes(1)
})

it('omits the default locale rather than guessing when it is absent from availableLinkIds', async () => {
const getLocalizedProduct = vi.fn().mockResolvedValueOnce({
linkId: 'camisa-azul',
categories: [],
availableLinkIds: { 'pt-BR': 'camisa-azul' },
})

const root = makeRoot({ linkText: 'camisa-azul' })
const ctx = makeCtx({
localizationEnabled: true,
locale: 'pt-BR',
locales: { 'en-US': {}, 'pt-BR': {} },
defaultLocale: 'en-US',
getLocalizedProduct,
})

const result = await (StoreProduct.otherLocales as any)(root, {}, ctx)

expect(result?.find((e: any) => e.locale === 'en-US')).toBeUndefined()
expect(result).toContainEqual({
locale: 'pt-BR',
slug: 'camisa-azul-100',
})
})

it('falls back to linkText for the locale being browsed', async () => {
const getLocalizedProduct = vi.fn().mockResolvedValueOnce({
linkId: 'camisa-azul',
categories: [],
availableLinkIds: { 'en-US': 'blue-shirt' },
})

const root = makeRoot({ linkText: 'camisa-azul' })
const ctx = makeCtx({
localizationEnabled: true,
locale: 'pt-BR',
locales: { 'en-US': {}, 'pt-BR': {} },
defaultLocale: 'en-US',
getLocalizedProduct,
})

const result = await (StoreProduct.otherLocales as any)(root, {}, ctx)

expect(result).toContainEqual({
locale: 'pt-BR',
slug: 'camisa-azul-100',
})
})

it('returns null when the Dataplane API throws', async () => {
const getLocalizedProduct = vi
.fn()
Expand Down
Loading