Internationalization

Locales

πŸ“˜

Private Beta

This feature is currently in private beta. If you would like access, please reach out to Support.

The locales feature enables you to quickly deploy your full storefront to various locales tailored to each country you operate in. Learn how to create and manage locales below.

Adding Locales

  1. Navigate to Site β†’ Settings β†’ Locales
  2. Add desired locales and define your Page Path Prefix
  3. On your next publish, your full storefront will be enabled on the added locales

Publishing

Once you have added locales and published your site, all pages on your storefront will be available on each locale page path defined via Settings. Shogun Frontend will automatically include the following in each subsequent publication:

  • Locale subpaths (for /yourpage, /en-ca/yourpage and /en-gb/yourpage now exist)
  • Metatags setup
  • Href setup

Locale Hooks

You can now develop locale specific functionality on your storefront using the hooks below.

useStoreLocales

The @getshogun/frontend-hooks package exposes a hook called useStoreLocales which provides the following:

  • locales - The list of available store locales
  • selectedLocale - The currently active/selected store locale
  • buildLinkWithLocale - A helper function to construct a new locale-dependent URL given a pathname and locale prefix

useRouter

Please note that usage of useRouter requires a change to correctly get the current locale-aware pathname. Usually, it is done via:

const { pathname } = useRouter()

And that needs to be changed to:

const { location: { pathname } } = useRouter()

Locale-Aware Components

LocaleLink

Using the useStoreLocales hook, we can create a custom locale-aware link component which can be used to navigate the store:

import React from 'react'
import { useRouter } from 'frontend-router'
import { useStoreLocales } from '@getshogun/frontend-hooks'
import Link from 'frontend-link'

const LocaleLink = ({ href, ...props }) => {
  const { location: { pathname } } = useRouter()
  const { selectedLocale, buildLinkWithLocale } = useStoreLocales()

  let path = href

  if(selectedLocale && !selectedLocale.default) {
    path = buildLinkWithLocale(href, selectedLocale.pathPrefix)
  }

  return <Link href={path} {...props} />
}

export default LocaleLink

Existing usages of Link or a should be replaced with LocaleLink to provide locale-aware navigation inside the store. For example, instead of:

<Link href={`/products/${productslug}`>{product.name}<Link/>

It should now use the LocaleLink component:

<LocaleLink href={`/products/${productslug}`>{product.name}<LocaleLink/>

LocaleSwitcher

We can also create a component to allow switching locales:

import React from 'react'
import { useRouter } from 'frontend-router'
import { useStoreLocales } from '@getshogun/frontend-hooks'
import Link from 'frontend-link'

import styles from './styles.module.css'

const LocaleSwitcher = () => {
  const ICONS = {
    'en-US': 'πŸ‡ΊπŸ‡Έ',
    'en-GB': 'πŸ‡¬πŸ‡§',
    'default': '🏳'
  }

  const { locales, buildLinkWithLocale } = useStoreLocales()
  const { location: { pathname } } = useRouter()

  return (
    <>
      {locales.map(locale => {
                const href = buildLinkWithLocale(pathname, locale.pathPrefix)
                const icon = ICONS[locale.code] || ICONS.default
                const label = locale.code.toUpperCase()

        return <Link href={href}>{icon} {label}</Link>
      })}
    <>
  )
}

export default LocaleSwitcher

Shopify Markets

The frontend-checkout package now supports Shopify Markets. Learn more here. Below are some example for implementing international pricing on Shogun Frontend using Shopify Markets.

Building the country selector

import { useCartActions, useCartState } from "frontend-checkout";

const CountrySelector = () => {
  const { countryCode } = useCartState();
  const { selectCountry } = useCartActions();

  return (
    <select
      defaultValue={countryCode}
      onChange={(e) => {
        selectCountry(e.target.value);
      }}
    >
      <option value="US">US</option>
      <option value="FR">FR</option>
      <option value="CA">CA</option>
    </select>
  );
};

🚧

Selected country is persisted between sessions in localStorage. There’s no need to implement that.

Updating the cart currency

Here is how to update the cart currency:

const CartDrawer = () => {
  const { hideCart } = useCartActions();
  const { isCartShown, currencyCode } = useCartState(); // here you get the currency

  return (
    <Drawer isOpen={isCartShown} onClose={hideCart} size="md">
      <CartItems inDrawer />
    </Drawer>
  );
};

You may want to update the cart item component to format the price according to country’s currency:

function formatMoney({ money, locales, options }) {
  if (typeof money === "string") {
    money = Number(money);
  }

  const formatter = new Intl.NumberFormat(locales, {
    style: "currency",
    currency: "USD",
    ...options,
  });

  return formatter.format(money);
}

...

<Text>
  {formatMoney({
    money: Number(checkoutProduct.variant.priceV2.amount) * quantity,
    options: { currency: currencyCode },
  })}
</Text>

Retrieving product information

🚧

product.price should not be used anymore. Instead priceV2 from variants should be used in order to get the right amount in the selected currency (via country).

const ProductGridItem = ({ imageLoading, product: cmsProduct }) => {
  const product = useNormalizedProduct(cmsProduct);
  const { countryCode } = useCartState();
  const { fetchProduct } = useCartActions();

  React.useEffect(() => {
    if (!product) return;

    async function handleFetchProduct() {
      const productResponse = await fetchProduct(product.id, {
        country: countryCode,
      });
      console.log({ productResponse });
    }

    handleFetchProduct();
  }, [fetchProduct, product, store.country]);

  // ...
};

Here is an example productResponse response:

{
  "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzY1NzQ2MTg2MDc3NDU=",
  "availableForSale": true,
  "createdAt": "2022-08-29T06:34:32Z",
  "updatedAt": "2022-09-02T10:56:22Z",
  "descriptionHtml": "<p>Of Course I Still Love You. Paleo pickled pork belly cardigan chillwave crucifix. Irregular Apocalypse. Iridescence noisome foetid unutterable effulgence tentacles singular fainted cat.</p>",
  "description": "Of Course I Still Love You. Paleo pickled pork belly cardigan chillwave crucifix. Irregular Apocalypse. Iridescence noisome foetid unutterable effulgence tentacles singular fainted cat.",
  "handle": "aerodynamic-granite-watch",
  "productType": "Automotive",
  "title": "Aerodynamic Granite Watch",
  "vendor": "Hessel-Bernier",
  "publishedAt": "2022-09-02T10:56:21Z",
  "onlineStoreUrl": "https://storefront-lde.shogun.dev/products/aerodynamic-granite-watch",
  "options": [
    {
      "name": "Color",
      "values": [
        "green",
        "turquoise",
        "plum"
      ]
    }
  ],
  "images": [
    {
      "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMjg2NTUxNTUyNDkyODE=",
      "src": "https://cdn.shopify.com/s/files/1/0553/3938/4961/products/source-404_41b0b7f1-e8b2-4e18-9068-083d3364fa58.jpg?v=1661754872",
      "altText": "Bora Horza Gobuchul"
    }
  ],
  "variants": [
    {
      "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8zOTQwMzYxODMwNDEyOQ==",
      "title": "green",
      "price": "24.00",
      "priceV2": {
        "amount": "24.95",
        "currencyCode": "EUR"
      },
      "weight": 0.8995,
      "quantityAvailable": 22,
      "available": true,
      "sku": "",
      "compareAtPrice": null,
      "compareAtPriceV2": null,
      "image": {
        "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMjg2NTUxNTUyNDkyODE=",
        "src": "https://cdn.shopify.com/s/files/1/0553/3938/4961/products/source-404_41b0b7f1-e8b2-4e18-9068-083d3364fa58.jpg?v=1661754872",
        "altText": "Bora Horza Gobuchul"
      },
      "selectedOptions": [
        {
          "name": "Color",
          "value": "green"
        }
      ],
      "unitPrice": null,
      "unitPriceMeasurement": {
        "measuredType": null,
        "quantityUnit": null,
        "quantityValue": 0,
        "referenceUnit": null,
        "referenceValue": 0
      }
    },
    {
      "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8zOTQwMzYxODMzNjg5Nw==",
      "title": "turquoise",
      "price": "24.00",
      "priceV2": {
        "amount": "24.95",
        "currencyCode": "EUR"
      },
      "weight": 0.194,
      "quantityAvailable": 18,
      "available": true,
      "sku": "aerodynamic-granite-watch-turquoise",
      "compareAtPrice": null,
      "compareAtPriceV2": null,
      "image": {
        "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMjg2NTUxNTUyNDkyODE=",
        "src": "https://cdn.shopify.com/s/files/1/0553/3938/4961/products/source-404_41b0b7f1-e8b2-4e18-9068-083d3364fa58.jpg?v=1661754872",
        "altText": "Bora Horza Gobuchul"
      },
      "selectedOptions": [
        {
          "name": "Color",
          "value": "turquoise"
        }
      ],
      "unitPrice": null,
      "unitPriceMeasurement": {
        "measuredType": null,
        "quantityUnit": null,
        "quantityValue": 0,
        "referenceUnit": null,
        "referenceValue": 0
      }
    },
    {
      "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8zOTQwMzYxODM2OTY2NQ==",
      "title": "plum",
      "price": "24.00",
      "priceV2": {
        "amount": "24.95",
        "currencyCode": "EUR"
      },
      "weight": 0.8201,
      "quantityAvailable": 8,
      "available": true,
      "sku": "aerodynamic-granite-watch-plum",
      "compareAtPrice": null,
      "compareAtPriceV2": null,
      "image": {
        "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMjg2NTUxNTUyNDkyODE=",
        "src": "https://cdn.shopify.com/s/files/1/0553/3938/4961/products/source-404_41b0b7f1-e8b2-4e18-9068-083d3364fa58.jpg?v=1661754872",
        "altText": "Bora Horza Gobuchul"
      },
      "selectedOptions": [
        {
          "name": "Color",
          "value": "plum"
        }
      ],
      "unitPrice": null,
      "unitPriceMeasurement": {
        "measuredType": null,
        "quantityUnit": null,
        "quantityValue": 0,
        "referenceUnit": null,
        "referenceValue": 0
      }
    }
  ]
}