跳到正文
DEV Community· Peter Panda·· 2 小時前AI 評分48

如何從單一 JSON 來源生成本地化價格表

Generating Localised Price Tables From One JSON Source

AI 導讀

將價格和方案資料集中在單一 JSON 來源,再按語系生成價格表,可避免不同語言版本出現不一致的價格資訊。各貨幣價格明確設定,並以 Intl.NumberFormat 按當地格式顯示;測試則檢查每個語系和方案是否缺少價格或翻譯鍵。

正文

Peter Panda

A pricing page that exists in six languages usually ends up with six slightly different price tables. Someone updates the English plan limits, forgets the Japanese page, and now an AI assistant or a customer quotes the wrong number. The fix is to treat prices as data and render every language version from one source.

One source of truth

{
  "currency_by_locale": {"en": "USD", "de": "EUR", "ja": "JPY", "pt-BR": "BRL"},
  "plans": [
    {"id": "starter", "price": {"USD": 29, "EUR": 27, "JPY": 4300, "BRL": 149},
     "seats": 3, "features": ["chat", "reports"]},
    {"id": "growth", "price": {"USD": 99, "EUR": 92, "JPY": 14800, "BRL": 499},
     "seats": 10, "features": ["chat", "reports", "api"]}
  ],
  "updated": "2026-10-31"
}

Prices per currency are set explicitly rather than converted on the fly. Rounded local prices look intentional, and you avoid tables that change every time an exchange rate moves.

Rendering with Intl

import data from "./pricing.json" assert { type: "json" };
import t from "./i18n.json" assert { type: "json" };

export function renderTable(locale) {
  const cur = data.currency_by_locale[locale];
  const fmt = new Intl.NumberFormat(locale, { style: "currency", currency: cur,
    maximumFractionDigits: cur === "JPY" ? 0 : 2 });
  const rows = data.plans.map(p => `
    <tr><th>${t[locale].plans[p.id]}</th>
      <td>${fmt.format(p.price[cur])} / ${t[locale].month}</td>
      <td>${p.seats} ${t[locale].seats}</td>
      <td>${p.features.map(f => t[locale].features[f]).join(", ")}</td></tr>`);
  return `<table>${rows.join("")}</table>
    <p>${t[locale].updated}: ${new Date(data.updated).toLocaleDateString(locale)}</p>`;
}

Intl.NumberFormat handles symbol position, separators and spacing, which differ more than people expect: €27,00 in some locales, 27,00 € in German, R$ 149,00 in Brazil.

Fail the build on missing data

Add a test that loops over every locale and plan and fails if a price or translation key is missing. It is the cheapest way to stop a half-updated table from shipping.

for (const loc of Object.keys(data.currency_by_locale))
  for (const p of data.plans) {
    if (p.price[data.currency_by_locale[loc]] == null) throw new Error(`${loc}:${p.id}`);
    if (!t[loc]?.plans?.[p.id]) throw new Error(`missing label ${loc}:${p.id}`);
  }

Why it matters beyond tidiness

Consistent, dated prices across language versions make your pricing page safer to quote. Search engines and AI assistants that read several versions of the page see the same facts, and buyers comparing currencies see a deliberate policy rather than a mistake.

Translation tooling is the other half of this workflow; this multilingual website translation tools comparison covers how teams split work between machine translation, plugins and human review.

Published by PandaWM, an overseas marketing team based in Hong Kong.

來源:DEV Community · dev.to