# General Translation Integrations: 在多个市场中销售
URL: https://generaltranslation.com/zh/docs/integrations/shopify/guides/selling-in-markets.mdx
Docs index: https://generaltranslation.com/llms.txt
Description: 如何为每种语言匹配各自的 Shopify 市场，并在购物者切换语言时迁移购物车。

Shopify 会根据 `@inContext` 中 `country` 所选定的市场，返回相应的价格、Currency和库存情况。本地化指南让所有语言共用同一个市场，因此切换语言时只有文本会变化。本指南则参照[示例店面](https://gt-shopify-vercel.vercel.app)的做法，为每种语言匹配各自的市场，让每种语言同时显示对应市场的价格和Currency。

*注意：本文代码在[本地化 Next.js Shopify 店面](/docs/integrations/shopify/guides/localizing-nextjs)中的 `proxy.ts` 和 `lib/i18n` 模块基础上进行扩展。*

## 开始之前 [#before-start]

* 完成[本地化 Next.js Shopify 店面](/docs/integrations/shopify/guides/localizing-nextjs)。
* 为每个要与语言配对的国家/地区添加一个 Shopify 市场。如果你将目录译文存储在 Shopify 中，请在各自的市场中发布对应的语言。

语言与市场的配对并非必需。如果希望购物者能独立于语言选择国家/地区，可以改为添加一个单独的国家/地区选择器，并在其值变化时更新购物车的买家身份 (参见[语言和市场](/docs/integrations/shopify/reference/locale-handling#language-market)) 。

## 将每个区域设置映射到对应市场 [#market-map]

将仅支持单一市场的 `getCommerceLocale` 替换为一个映射，为每个区域设置指定对应的 Shopify 国家和语言：

```ts title="lib/i18n/index.ts"
import type { CommerceLocale } from "@/lib/config/types";

// 每种语言对应一个 Shopify 市场，因此 Shopify 会返回该市场的价格和货币。
// 货币始终取自 Shopify 的响应，而非此表。
const markets: Record<string, CommerceLocale> = {
  en: { country: "US", language: "EN" },
  fr: { country: "FR", language: "FR" },
  ja: { country: "JP", language: "JA" },
  ko: { country: "KR", language: "KO" },
  id: { country: "ID", language: "ID" },
};

export function getCommerceLocale(locale: string): CommerceLocale {
  const market = markets[locale];
  if (!market) throw new Error(`No Shopify market for locale: ${locale}`);
  return market;
}
```

映射中的每个国家都必须属于该店铺的某个 Shopify 市场，且每种语言都必须是 Storefront API 的 `LanguageCode` (参见[区域设置代码](/docs/integrations/shopify/reference/locale-handling#locale-codes)) 。

目录读取、Hydrogen 的处理函数以及 [`getRegion`](/docs/react/nextjs/reference/functions/get-region) 都无需其他改动：它们本来就读取 `getCommerceLocale`，因此现在每个区域设置都会查询各自的市场，而 `"use cache"` getter 也会为每个市场分别保留一个缓存条目。创建新购物车时，请将该市场的国家设为买家的 `countryCode`。

## 在不同市场之间迁移购物车 [#move-cart]

购物车会沿用其买家所在国家/地区的Currency。Hydrogen 的购物车处理函数不提供更新买家国家/地区的操作，因此当页面跳转可能导致市场变化时，代理会读取购物车实际的国家/地区，并在页面渲染前完成更新：

```ts title="lib/cart/request.ts"
const CART_COUNTRY_QUERY = gql(`#graphql
  query cartCountry($cartId: ID!) {
    cart(id: $cartId) {
      buyerIdentity { countryCode }
    }
  }
`);

const CART_COUNTRY_UPDATE_MUTATION = gql(`#graphql
  mutation cartCountryUpdate($cartId: ID!, $countryCode: CountryCode!) {
    cartBuyerIdentityUpdate(cartId: $cartId, buyerIdentity: { countryCode: $countryCode }) {
      cart { buyerIdentity { countryCode } }
      userErrors {
        message
      }
    }
  }
`);

export async function moveCartToMarket(
  request: Request,
  requestContext: ShopifyRequestContext,
): Promise<void> {
  const cartId = getCartId({ cookie: request.headers.get("cookie") ?? undefined });
  if (!cartId) return;
  const client = createRequestStorefrontClient(requestContext);
  const countryCode = requestContext.i18n.country;
  // 绕过缓存读取购物车；其他标签页或会话可能已将其移至其他市场。
  const current = await client.graphql(CART_COUNTRY_QUERY, { variables: { cartId } });
  if (current.errors) throw new Error("Unable to read the cart's country");
  const cart = current.data?.cart;
  // 已过期的购物车由模板现有的购物车创建流程处理。
  if (!cart || cart.buyerIdentity.countryCode === countryCode) return;

  const result = await client.graphql(CART_COUNTRY_UPDATE_MUTATION, {
    variables: { cartId, countryCode },
  });
  if (result.errors) throw new Error("Unable to update the cart's country");
  const updated = result.data.cartBuyerIdentityUpdate;
  if (
    !updated ||
    updated.userErrors.length > 0 ||
    updated.cart?.buyerIdentity.countryCode !== countryCode
  ) {
    throw new Error("The cart's country was not updated");
  }
}
```

在 `proxy.ts` 中，仅在市场可能发生变化时才执行迁移，并在迁移成功后再设置区域设置 cookie。以下代码将替换 Next.js 指南中更新 cookie 的逻辑：

```ts title="proxy.ts"
// 切换语言，或在没有区域设置 cookie 的情况下浏览页面，都可能将购物车移至另一个市场。
const marketMayChange = isPageNavigation && rememberedLocale !== locale;

// 在 handleShopifyRoutes 拒绝处理该请求之后、生成页面响应或设置区域设置 cookie 之前。
if (marketMayChange) {
  try {
    await moveCartToMarket(request, requestContext);
  } catch {
    // 不要在属于另一个市场的购物车旁渲染新市场的价格。
    const failure = new NextResponse("Unable to switch the cart's market. Please reload to retry.", {
      status: 503,
      headers: { "Cache-Control": "no-store" },
    });
    // 即使此处失败，更新也可能已提交到 Shopify，因此清除
    // 已记住的区域设置；下次页面导航时会重新读取购物车。
    failure.cookies.delete(LOCALE_COOKIE_NAME);
    return failure;
  }
}

// 在响应上设置。仅在 moveCartToMarket 成功后才会执行到这里。
if (marketMayChange) {
  response.cookies.set(LOCALE_COOKIE_NAME, locale, { path: "/", sameSite: "lax" });
}
```

该检查的行为如下：

* **开销。** 在同一语言内导航时会跳过该检查。切换语言或在没有 cookie 的情况下访问时，会额外产生一次不经缓存的购物车读取；仅当国家/地区不一致时，才会再执行一次变更 (mutation) 。不要缓存购物车国家/地区查询。
* **失败处理。** 遇到网络故障、GraphQL 错误或变更返回的 `userErrors` 时，会直接返回 503，而不是在旧市场的购物车旁渲染新市场的价格。由于只有市场变更时才会运行该检查，Shopify 购物车服务中断只会导致无法切换语言，而不会影响正常浏览。
* **恢复。** 区域设置 cookie 仅在迁移成功后才会写入。迁移失败时 (包括请求已到达 Shopify 但响应丢失的情况) 会删除该 cookie，这样无论使用哪种语言，下一次页面导航都会重新读取购物车。该 cookie 只是一个提示，并不能证明购物车所属的国家/地区：如果国家/地区在此代理之外被修改，会在下一次切换语言或无 cookie 访问时得到纠正。

国家/地区更新成功后，Shopify 会重新为购物车商品行定价。[语言切换器](/docs/integrations/shopify/guides/localizing-nextjs#routing)通过完整页面加载进行导航，因此每一次切换都会经过代理。

## Next steps

- /docs/integrations/shopify/guides/localizing-nextjs
- /docs/integrations/shopify/guides/translating-products
- /docs/integrations/shopify/guides/localizing-react-router

## Sitemap

See the full [sitemap](https://generaltranslation.com/sitemap.md) for all pages.
