# General Translation Integrations: 本地化 Next.js Shopify 店面
URL: https://generaltranslation.com/zh/docs/integrations/shopify/guides/localizing-nextjs.mdx
Docs index: https://generaltranslation.com/llms.txt
Description: 如何使用 gt-next、带区域设置前缀的路由和缓存组件来本地化 Vercel Shop 店面。

[Vercel Shop](https://github.com/vercel/shop) 是一个 Next.js App Router 模板，基于 Shopify 的框架无关店面 SDK Hydrogen 构建。借助 `gt-next`，默认区域设置的页面不带前缀，其他区域设置的页面则位于 `/fr/...` 等前缀下，且每个页面仍可按区域设置各预渲染一次。所有语言共用同一个 Shopify 市场进行销售；如需为每种语言配置独立的市场和货币，请在完成本指南后参阅[在多个市场销售](/docs/integrations/shopify/guides/selling-in-markets)。

*注意：本指南基于 Next.js 16、`cacheComponents: true`、`@shopify/hydrogen` 2026.10 和 `gt-next` 11.4 版本的模板编写，与[示例店面](https://gt-shopify-vercel.vercel.app)所用版本一致。该店面现已上线英语、法语、日语、韩语和印度尼西亚语版本。*

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

请参照 [Next.js 快速入门](/docs/react/nextjs-quickstart)，安装 `gt-next`，并将 `gt` CLI 安装为开发依赖。本指南将使用模板中已有的 `proxy.ts` 替代快速入门中的中间件。

## 配置 gt-next [#configure]

### 1. 添加配置插件

在模板传给 `withShopConfig` 的插件列表中添加 [`withGTConfig`](/docs/react/nextjs/config)：

```ts title="next.config.ts"
import { withGTConfig } from "gt-next/config";

export default withShopConfig(nextConfig, [
  (config: NextConfig) => withGTConfig(config), // [!code ++]
  shopConfig.botid.isEnabled && withBotId,
  shopConfig.agent.isEnabled && withEve,
]);
```

### 2. 添加请求函数

启用缓存组件时，区域设置必须来自 `[locale]` 根参数，而不能来自请求头或 cookie。请在项目根目录下创建 [`getLocale`](/docs/react/nextjs/reference/functions/get-locale) 和 [`getRegion`](/docs/react/nextjs/reference/functions/get-region) 文件，`withGTConfig` 会自动检测到它们。 (参见[使用缓存组件](/docs/react/nextjs/cache-components#request-values)。) 

```ts title="getLocale.ts"
import { locale } from "next/root-params";

import { defaultLocale } from "@/lib/i18n";

// `[locale]` 根参数属于静态缓存键的一部分，因此读取它不会影响页面的预渲染。
export default async function getLocale(): Promise<string> {
  return (await locale()) ?? defaultLocale;
}
```

```ts title="getRegion.ts"
import { locale } from "next/root-params";

import { defaultLocale, getCommerceLocale } from "@/lib/i18n";

// 地区即页面区域设置所面向的 Shopify 销售市场所在的国家/地区。
export default async function getRegion(): Promise<string> {
  return getCommerceLocale((await locale()) ?? defaultLocale).country;
}
```

`getCommerceLocale` 将区域设置映射到对应的 Shopify 国家/地区和语言 (参见[按语言查询 Shopify](#shopify-language)) 。

### 3. 在本地生成并加载译文

缓存组件需要使用自定义的 [`loadTranslations`](/docs/react/reference/functions/load-translations) 函数，因为在预渲染期间远程加载译文并不安全。请使用 CLI 将译文写入源代码，再从应用包中导入。 (参见[在本地存储译文](/docs/react/guides/storing-translations)。) 

```json title="gt.config.json"
{
  "defaultLocale": "en",
  "locales": ["fr", "ja", "ko", "id"],
  "src": ["app/**/*.{ts,tsx}", "components/**/*.{ts,tsx}", "lib/**/*.{ts,tsx}"],
  "files": {
    "gt": { "output": "public/_gt/[locale].json" }
  }
}
```

* `src` 中添加了 `lib/`。CLI 默认只扫描 `src/`、`app/`、`pages/` 和 `components/`，而该模板将元数据和标签存放在 `lib/` 中。

```ts title="loadTranslations.ts"
// `pnpm translate` 会在构建前生成这些文件；将其打包进来可保证页面能够预渲染。
const translations: Record<string, () => Promise<{ default: object }>> = {
  fr: () => import("./public/_gt/fr.json"),
  ja: () => import("./public/_gt/ja.json"),
  ko: () => import("./public/_gt/ko.json"),
  id: () => import("./public/_gt/id.json"),
};

export default async function loadTranslations(locale: string): Promise<object> {
  const loaded = await translations[locale]?.();
  return loaded?.default ?? {};
}
```

修改文案后，运行 [`npx gt translate`](/docs/cli/reference/commands/translate) 并提交生成的文件，或者在 `next build` 之前运行该命令。提交这些文件后，即使没有 General Translation API 密钥，店面应用也能正常构建。

## 按区域设置路由请求 [#routing]

### 将路由移至 `[locale]` 下

1. 将所有店面路由移至 `app/[locale]/`，并删除 `app/layout.tsx`，使 `[locale]` 成为根参数。`api/`、`md/`、`agent/`、`sitemap*`、`robots.ts`、`manifest.ts`、`llms.txt`、`global-error.tsx` 以及静态图标仍直接保留在 `app/` 下。

2. 将 `PageProps<"/...">` 和 `LayoutProps<"/...">` 的类型参数更新为 `"/[locale]/..."`。

3. 在 `app/[locale]/layout.tsx` 中，通过 `generateStaticParams` 返回所有区域设置，拒绝不受支持的区域设置，设置 `<html lang>`，并用 [`GTProvider`](/docs/react/reference/components/gt-provider) 包裹 body：

   ```tsx title="app/[locale]/layout.tsx"
   import { GTProvider } from "gt-next";
   import { getLocale } from "gt-next/server";
   import { notFound } from "next/navigation";

   import { isSupportedLocale, locales } from "@/lib/i18n";

   export const ensureStatic = "prefetch";

   export function generateStaticParams() {
     return locales.map((locale) => ({ locale }));
   }

   export default async function RootLayout({ children }: LayoutProps<"/[locale]">) {
     const locale = await getLocale();
     if (!isSupportedLocale(locale)) notFound();
     return (
       <html lang={locale}>
         <body>
           <GTProvider>{/* 跳转链接、购物车提供者、导航、主体、页脚 */}{children}</GTProvider>
         </body>
       </html>
     );
   }
   ```

4. 添加一个调用 `notFound()` 的兜底 (catch-all) 路由，使未匹配的路径在本地化布局中渲染 `app/[locale]/not-found.tsx`，而不是显示 Next.js 默认的 404 页面：

   ```tsx title="app/[locale]/[...rest]/page.tsx"
   import { notFound } from "next/navigation";

   import { PLACEHOLDER_HANDLE } from "@/lib/static-params";

   export function generateStaticParams() {
     return [{ rest: [PLACEHOLDER_HANDLE] }];
   }

   export default function CatchAllPage(): never {
     notFound();
   }
   ```

<Callout type="warn">
  **警告：**&#x5728; `gt-next` 11.4 中，[`GTProvider`](/docs/react/reference/components/gt-provider) 会在客户端读取 `usePathname()`。启用缓存组件后，没有静态参数的动态路由将因此无法预渲染。对于没有静态参数的路由 (例如兜底路由和 `account/orders/[id]`) ，请在 `generateStaticParams` 中返回一个占位符。
</Callout>

### 在代理中路由请求

从 `gt.config.json` 中一次性读取区域设置列表，并将前缀从每个路径中拆分出来：

```ts title="lib/i18n/index.ts"
import gtConfig from "@/gt.config.json";

export const defaultLocale = gtConfig.defaultLocale;
export const locales = [gtConfig.defaultLocale, ...gtConfig.locales];

export function isSupportedLocale(value: string | undefined): value is string {
  return value !== undefined && locales.includes(value);
}

/** 将 `/fr/products/x` 拆分为 `fr` 和 `/products/x`；不带前缀的路径属于默认区域设置。 */
export function splitLocalePath(pathname: string): { locale: string; pathname: string } {
  const [, segment, ...rest] = pathname.split("/");
  if (segment !== defaultLocale && isSupportedLocale(segment)) {
    return { locale: segment, pathname: `/${rest.join("/")}` };
  }
  return { locale: defaultLocale, pathname };
}

/** 默认区域设置的路径不带前缀，确保每个页面只有一个规范网址。 */
export function localizePath(pathname: string, locale: string): string {
  if (locale === defaultLocale) return pathname;
  return pathname === "/" ? `/${locale}` : `/${locale}${pathname}`;
}
```

在 `proxy.ts` 中，保留模板原有的 `handleShopifyRoutes` 分发逻辑，让 Shopify 优先处理购物车、预测搜索和客户账户相关请求。然后将其余所有页面请求重写到对应的 `/[locale]` 路由。API 及其他非页面路径则无需处理：

```ts title="proxy.ts"
// 沿用模板原有的分发逻辑：启用客户账户后，感知会话的
// 分支会额外添加账户处理函数和会话管理器。
const shopifyRoute = customerSession
  ? handleShopifyRoutes({
      ...routeOptions,
      handlers: customerHandlers(customerSession, isAuthPath),
      sessionManager: createCustomerSessionManager(request),
    })
  : handleShopifyRoutes({ ...routeOptions, handlers: [cartHandlers, predictiveSearchHandlers] });
if (shopifyRoute) return shopifyRoute;

const { locale, pathname: unprefixedPathname } = splitLocalePath(pathname);
const refinedPath = getRefinedCollectionPath(unprefixedPathname, request.nextUrl.search);
const localizedPath = isUnlocalizedPath(pathname)
  ? null
  : `/${locale}${refinedPath ?? (unprefixedPathname === "/" ? "" : unprefixedPathname)}`;
// 页面位于 app/[locale] 下；默认区域设置的 URL 不带前缀，由内部重写到对应路由。
const rewritePath =
  getMarkdownRewritePath(request) ?? (localizedPath === pathname ? null : localizedPath);
```

```ts title="proxy.ts"
const UNLOCALIZED_PATH = /^\/(?:api|md|agent|eve|_eve_internal|\.well-known|__shopify)(?:\/|$)/;

function isUnlocalizedPath(pathname: string): boolean {
  return UNLOCALIZED_PATH.test(pathname);
}

// 店面页面，不包括 API 路由以及 Shopify 带点号的端点（如 `/cart.js`）。
function isPagePath(pathname: string): boolean {
  return !isUnlocalizedPath(pathname) && !/\.[a-z]+$/i.test(pathname);
}
```

由于 `splitLocalePath` 从不将默认区域设置视为前缀，`/en/products/x` 会被重写为 `/en/en/products/x`，进而返回 404。这样，每个页面都只有一个规范网址。

### 本地化链接

在模板的 `Link` 组件中，将字符串类型的 href 传给 [`useLocalizeHref`](/docs/react/nextjs/reference/hooks/use-localize-href) 处理，确保内部链接保持当前区域设置。`router.push`、`router.replace` 和搜索导航也需要同样处理。

```tsx title="components/ui/link.tsx"
import { useLocalizeHref } from "gt-next/link";

// 导航时保留当前区域设置；默认区域设置的路径不带前缀。
const localizeHref = useLocalizeHref();
const localizedHref = typeof href === "string" ? localizeHref(href) : href;
```

实现语言切换器时，替换当前 URL 的前缀，并用每种语言的本族语名称来显示该语言：

```tsx title="components/nav/language-switcher.tsx"
"use client";

import { T, useLocale, useLocales } from "gt-next";
import { useId } from "react";

import { localizePath, splitLocalePath } from "@/lib/i18n";

/** 用每种语言的本族语名称显示该语言（"Français"、"日本語"），方便购物者一眼认出自己的语言。 */
function getEndonym(locale: string): string {
  const name = new Intl.DisplayNames([locale], { type: "language" }).of(locale) ?? locale;
  return name.charAt(0).toLocaleUpperCase(locale) + name.slice(1);
}

export function LanguageSwitcher() {
  const id = useId();
  const locale = useLocale();
  const locales = useLocales();
  return (
    <>
      <label htmlFor={id} className="sr-only">
        <T>Language</T>
      </label>
      <select
        id={id}
        value={locale}
        onChange={(event) => {
          // 切换区域设置会改变根布局，因此需整页重新加载来完成导航。
          const { pathname } = splitLocalePath(window.location.pathname);
          window.location.assign(
            `${localizePath(pathname, event.target.value)}${window.location.search}`,
          );
        }}
      >
        {locales.map((option) => (
          <option key={option} lang={option} value={option}>
            {getEndonym(option)}
          </option>
        ))}
      </select>
    </>
  );
}
```

### 添加 SEO 元数据

为每个页面设置其自身区域设置下的规范网址，并为每个区域设置提供一个 `hreflang` 替代项。模板中 `lib/seo/index.ts` 里的 `buildAlternates` 和 `buildOpenGraph` 是同步函数，无法读取区域设置，因此请将它们改写为异步函数并移至 `lib/seo/server.ts`：

1. 在 `lib/seo/index.ts` 中导出 `buildCanonicalPath`，并删除 `buildAlternates` 和 `buildOpenGraph`。
2. 将异步版本添加到 `lib/seo/server.ts`：

   ```ts title="lib/seo/server.ts"
   import { getLocale } from "gt-next/server";
   import type { Metadata } from "next";

   import { localizePath, locales } from "@/lib/i18n";
   import { buildCanonicalPath } from "@/lib/seo";
   import type { SearchParamsInput } from "@/lib/seo/types";

   /** 当前请求区域设置的规范网址，以及每个区域设置的 hreflang 替代项。 */
   export async function buildAlternates({
     pathname,
     searchParams,
   }: {
     pathname: string;
     searchParams?: SearchParamsInput;
   }): Promise<Metadata["alternates"]> {
     const canonicalPath = buildCanonicalPath(pathname, searchParams);
     const locale = await getLocale();
     return {
       canonical: localizePath(canonicalPath, locale),
       languages: {
         ...Object.fromEntries(locales.map((code) => [code, localizePath(canonicalPath, code)])),
         "x-default": canonicalPath,
       },
     };
   }
   ```

   用同样的方式将 `buildOpenGraph` 改为异步，并对其 URL 和区域设置进行本地化。Open Graph 要求使用 `language_TERRITORY` 格式的值 (例如 `fr_FR`) ，因此请在这里转换区域设置，而 `hreflang` 和 `<html lang>` 仍使用原始标签：

   ```ts title="lib/seo/server.ts"
   /** Open Graph 区域设置使用 language_TERRITORY 格式，例如 fr_FR；"fr" 会转换为其最可能的地区。 */
   function toOpenGraphLocale(locale: string): string {
     const { language, region } = new Intl.Locale(locale).maximize();
     return `${language}_${region}`;
   }

   // 在 buildOpenGraph 中，与现有字段一起返回：
   const locale = await getLocale();
   return {
     url: localizePath(url, locale),
     locale: toOpenGraphLocale(locale),
     alternateLocale: locales.filter((code) => code !== locale).map(toOpenGraphLocale),
   };
   ```

   `maximize()` 会选择最可能的地区，例如将 `fr` 转换为 `fr_FR`，将 `zh-Hant` 转换为 `zh_TW`。如果某个区域设置最可能的地区并不是你实际面向的地区，请改用显式映射来完成转换。
3. 在每个页面的 `generateMetadata` 中，从 `@/lib/seo/server` 导入这两个辅助函数并 await 它们：

   ```tsx title="app/[locale]/page.tsx"
   import { buildAlternates, buildOpenGraph } from "@/lib/seo/server"; // [!code highlight]

   export async function generateMetadata(): Promise<Metadata> {
     const gt = await getGT();
     const title = gt("Home");
     const description = gt("Explore featured products, curated collections, and seasonal campaigns.");
     return {
       title: `${title} | ${shopConfig.site.name}`,
       description,
       alternates: await buildAlternates({ pathname: "/" }), // [!code highlight]
       openGraph: await buildOpenGraph({ title, description, url: "/", type: "website" }), // [!code highlight]
     };
   }
   ```

   删除旧的辅助函数后，仍从 `@/lib/seo` 导入的页面将无法通过类型检查，借此即可找出所有遗漏的调用位置。

此外，还需在站点地图中列出每个区域设置的 URL，并为每个 URL 添加指向所有区域设置的 `<xhtml:link rel="alternate" hreflang="...">` 条目。

## 以每种语言查询 Shopify [#shopify-language]

将每个区域设置映射到模板中 Storefront API 调用所需的 Shopify 国家/地区和语言参数。所有区域设置共用该商店唯一的市场，因此只需切换语言：

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

// 所有语言共用的销售市场。货币始终以 Shopify 返回的响应为准。
const STORE_COUNTRY: CommerceLocale["country"] = "US";

// gt.config.json 中每个区域设置对应的 Storefront API 语言。
const SHOPIFY_LANGUAGES: Record<string, CommerceLocale["language"]> = {
  en: "EN",
  fr: "FR",
  ja: "JA",
  ko: "KO",
  id: "ID",
};

export function getCommerceLocale(locale: string): CommerceLocale {
  const language = SHOPIFY_LANGUAGES[locale];
  if (!language) throw new Error(`No Shopify language for locale: ${locale}`);
  return { country: STORE_COUNTRY, language };
}

/** 代理会将页面区域设置保存在此 cookie 中，确保 Hydrogen 处理函数调用（`/api/cart`）使用相同的语言。 */
export const LOCALE_COOKIE_NAME = "shop_locale";
/** 代理通过此请求头将解析后的区域设置转发给服务器操作和购物车初始数据。 */
export const LOCALE_HEADER_NAME = "x-shop-locale";
```

每种语言都必须是有效的 Storefront API `LanguageCode` (参见[区域设置代码](/docs/integrations/shopify/reference/locale-handling#locale-codes)) 。如果你[将目录译文存储在 Shopify 中](/docs/integrations/shopify/guides/translating-products#store-in-shopify)，还需要在商店中发布每种语言；对于尚未发布的语言，Shopify 会返回源文本。如果译文随店面一起打包，则无需发布对应语言。

### 在读取目录时传入语言

模板中的目录 getter 已支持类型为 `CommerceLocale` 的 `locale` 选项，并会将其传给 `@inContext`。请在每个页面和组件中传入当前区域设置的值。使用 `"use cache"` 的 getter 会将其作为参数接收，这样每种语言都会有各自独立的缓存条目。

```ts title="lib/i18n/server.ts"
import { getLocale } from "gt-next/server";
import { headers } from "next/headers";

import type { CommerceLocale } from "@/lib/config/types";
import { defaultLocale, getCommerceLocale, isSupportedLocale, LOCALE_HEADER_NAME } from "@/lib/i18n";

/** 页面 `[locale]` 对应的 Shopify 国家和语言；每次读取目录时都要传入。 */
export async function getRequestCommerceLocale(): Promise<CommerceLocale> {
  return getCommerceLocale(await getLocale());
}

/** 同一个值，供无法获取路由参数的动态代码（服务器操作、购物车初始数据）使用，从代理设置的请求头中读取。 */
export async function getForwardedCommerceLocale(): Promise<CommerceLocale> {
  const locale = (await headers()).get(LOCALE_HEADER_NAME) ?? undefined;
  return getCommerceLocale(isSupportedLocale(locale) ? locale : defaultLocale);
}
```

```tsx title="app/[locale]/products/[handle]/page.tsx"
const product = await getProduct({
  handle,
  locale: await getRequestCommerceLocale(),
});
```

快速购买和**加载更多**等服务器操作无法读取路由参数，请改用 `getForwardedCommerceLocale()` 获取该值。

### 让 Hydrogen 的处理函数使用同一语言

Hydrogen 的购物车和预测搜索处理函数会从请求上下文的 `i18n` 中读取语言和国家/地区。`/api/cart` 这类请求不带区域设置前缀，因此代理会将页面的区域设置保存在 cookie 中，再通过请求头转发给服务端组件和服务器操作：

```ts title="proxy.ts"
import {
  defaultLocale,
  getCommerceLocale,
  isSupportedLocale,
  LOCALE_COOKIE_NAME,
  LOCALE_HEADER_NAME,
  splitLocalePath,
} from "@/lib/i18n";

const rememberedLocale = request.cookies.get(LOCALE_COOKIE_NAME)?.value;
const requestLocale = resolveRequestLocale(request);
// 推测性预取不得更改购物者的购物车或已记住的语言。
const isPageNavigation =
  request.method === "GET" &&
  isPagePath(pathname) &&
  !request.headers.has("next-router-prefetch") &&
  request.headers.get("purpose") !== "prefetch" &&
  !request.headers.get("sec-purpose")?.includes("prefetch");

// Hydrogen 的购物车和搜索处理函数以购物者的语言运行。
const requestContext = createCustomerRequestContext(
  request,
  getCommerceLocale(requestLocale),
  requestLocale,
);

// handleShopifyRoutes 不处理该请求之后：
const headers = forwardCustomerRefreshAttempt(
  request.nextUrl,
  requestContext.getForwardedRequestHeaders(),
);
headers.set(LOCALE_HEADER_NAME, requestLocale);

const response = rewritePath
  ? NextResponse.rewrite(new URL(`${rewritePath}${request.nextUrl.search}`, request.url), {
      request: { headers },
    })
  : NextResponse.next({ request: { headers } });

// 处理响应时：
if (isPageNavigation && rememberedLocale !== locale) {
  response.cookies.set(LOCALE_COOKIE_NAME, locale, { path: "/", sameSite: "lax" });
}

/** 页面使用 URL 中的区域设置；无前缀的 Shopify 端点（如 `/api/cart`）使用已记住的页面区域设置。 */
function resolveRequestLocale(request: NextRequest): string {
  const { pathname } = request.nextUrl;
  const { locale } = splitLocalePath(pathname);
  if (locale !== defaultLocale || isPagePath(pathname)) return locale;
  const remembered = request.cookies.get(LOCALE_COOKIE_NAME)?.value;
  return isSupportedLocale(remembered) ? remembered : defaultLocale;
}
```

保留模板中从 `@/lib/auth/request` 导入的 `forwardCustomerRefreshAttempt`。请在决定使用 `NextResponse.rewrite` 还是 `NextResponse.next` 之前调用它，这样经过重写的账户页面也能收到刷新尝试请求头，避免反复重试已失败的令牌刷新。

在 `createCustomerRequestContext` 中同时接收 Shopify 区域设置和页面区域设置，并将该区域设置对应的 URL 前缀作为 `i18n.pathPrefix` 传入：法语 URL 传入 `/fr`，默认区域设置传入空字符串。如果缺少该参数，Hydrogen 就无法将 `/fr/products/hoodie?variant=123` 识别为商品 URL，进而跳过变体重定向，导致页面可能显示默认变体，而非链接中指定的变体：

```ts title="lib/auth/request.ts"
import { defaultLocale } from "@/lib/i18n";

export function createCustomerRequestContext(
  request: Request,
  i18n: CommerceLocale = shopConfig.localization,
  locale: string = defaultLocale,
): ShopifyRequestContext {
  return createShopifyRequestContext({
    i18n: {
      country: i18n.country,
      language: i18n.language,
      // Hydrogen 会匹配此前缀下的商品 URL 并进行重定向。
      pathPrefix: locale === defaultLocale ? "" : `/${locale}`,
    },
    request,
  });
}
```

有了这些默认值，模板中的其他调用方仍可正常工作。服务端购物车初始数据 (`lib/cart/server.ts`) 同样应传入 `getCommerceLocale`，而不是读取 `shopConfig.localization`。

## 翻译文案 [#copy]

与模板的做法一致，将文案以内联方式保留在各个组件中：

* 使用 [`<T>`](/docs/react/reference/components/t) 包裹 JSX。动态值使用 [`<Var>`](/docs/react/reference/components/var)、[`<Num>`](/docs/react/reference/components/num)、[`<Currency>`](/docs/react/reference/components/currency) 或 [`<DateTime>`](/docs/react/reference/components/datetime) 包裹，计数则使用 [`<Plural>`](/docs/react/reference/components/plural) 包裹。[`<T>`](/docs/react/reference/components/t) 在服务端组件和客户端组件中均可使用。
* 翻译字符串 (例如 `aria-label` 值、元数据和属性) 时，在服务端组件中使用 [`getGT`](/docs/react/nextjs/reference/functions/get-gt)，在客户端组件中使用 [`useGT`](/docs/react/reference/hooks/use-gt)。
* 在组件外部定义的标签 (例如排序选项) 需使用 [`msg`](/docs/react/reference/functions/msg) 进行标记，然后在渲染处通过 [`getMessages`](/docs/react/nextjs/reference/functions/get-messages) 或 [`useMessages`](/docs/react/reference/hooks/use-messages) 进行解析。

```tsx title="app/[locale]/page.tsx"
import { T } from "gt-next";
import { getGT } from "gt-next/server";

export async function generateMetadata(): Promise<Metadata> {
  const gt = await getGT();
  return { title: gt("Home") };
}

export default async function HomePage() {
  const gt = await getGT();
  return (
    <>
      <h1>
        <T>A storefront localized with General Translation</T>
      </h1>
      <ProductsGrid title={gt("Products")} />
    </>
  );
}
```

```tsx title="app/[locale]/search/page.tsx"
<T>
  <Plural
    n={total}
    one={<><Num>{total}</Num> result</>}
    other={<><Num>{total}</Num> results</>}
  />
</T>
```

<Callout type="warn">
  **警告：** CLI 会追踪由 [`getGT()`](/docs/react/nextjs/reference/functions/get-gt) 或 [`useGT()`](/docs/react/reference/hooks/use-gt) 赋值的变量，以此查找 `gt()` 字符串。请将该赋值单独写在一行，例如 `const gt = await getGT()`。通过解构 `Promise.all` 结果读取的字符串不会被提取，并会以源语言渲染。
</Callout>

请使用区域设置和 Shopify 返回的货币代码来格式化 Shopify 价格。该模板中的 `formatPrice(money, locale)` 使用了 Hydrogen 的 `formatMoney`，因此 36.95 EUR 在法语中会渲染为 `36,95 €`。

## 翻译目录内容 [#catalog]

商品标题、描述和选项均来自 Shopify。你可以将它们的译文存储在 Shopify 中，并通过 `@inContext(language:)` 读取 (`getCommerceLocale` 已会传入该参数) ；也可以像示例店面那样，通过 CLI 将译文与店面一起打包。 (参见[翻译 Shopify 商品](/docs/integrations/shopify/guides/translating-products)。)

## Next steps

- /docs/integrations/shopify/guides/localizing-react-router
- /docs/integrations/shopify/guides/translating-products
- /docs/integrations/shopify/guides/selling-in-markets

## Sitemap

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