Internationalize a Mini Shop

A hands-on React tutorial that internationalizes a simple shop using GT React components, hooks, and shared strings

Get a small, fully local “mini shop” running and translated — no external services, no routing, no UI frameworks. You’ll use the core GT React features end-to-end and see how they fit together in a simple, realistic UI.

Prerequisites: React, basic JavaScript/TypeScript

What you’ll build

  • A single-page “shop” with a product grid and a simple in-memory cart
  • Language switcher and shared navigation labels
  • Properly internationalized numbers, currency, and pluralization
  • Optional: local translation storage for production builds

Links used in this tutorial


Install and Wrap Your App

Install packages and wrap your app with the provider.

npm i gt-react
npm i --save-dev gtx-cli
yarn add gt-react
yarn add --dev gtx-cli
bun add gt-react
bun add --dev gtx-cli
pnpm add gt-react
pnpm add --save-dev gtx-cli

Optional: Starter Project (Vite)

If you’re starting from scratch, scaffold a Vite React + TypeScript app and then install GT packages:

npm create vite@latest mini-shop -- --template react-ts
cd mini-shop
npm i gt-react
npm i --save-dev gtx-cli

Then add the files in the sections below (e.g., src/main.tsx, src/App.tsx, src/components/*, src/data.ts, src/nav.ts).

Create a minimal provider setup.

src/main.tsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { GTProvider } from 'gt-react'; // See: /docs/react/api/components/gtprovider
import App from './App';

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <GTProvider locales={["es", "fr"]}> {/* Enable Spanish and French */}
      <App />
    </GTProvider>
  </StrictMode>
);

Optionally add a gt.config.json now (useful later for CI and local storage):

gt.config.json
{
  "defaultLocale": "en",
  "locales": ["es", "fr"]
}

Development API Keys

You can follow this tutorial without keys (it will render the default language). To see live translations and test language switching in development, add development keys.

Learn more in Production vs Development.

.env.local
VITE_GT_API_KEY="your-dev-api-key"
VITE_GT_PROJECT_ID="your-project-id"
.env.local
REACT_APP_GT_API_KEY="your-dev-api-key"
REACT_APP_GT_PROJECT_ID="your-project-id"

Seed Data and App Structure

We’ll hardcode a tiny product array and keep everything client-side. No servers, no routing.

src/data.ts
export type Product = {
  id: string;
  name: string;
  description: string;
  price: number;
  currency: 'USD' | 'EUR';
  inStock: boolean;
  addedAt: string; // ISO date string
};

export const products: Product[] = [
  {
    id: 'p-1',
    name: 'Wireless Headphones',
    description: 'Noise-cancelling over-ear design with 30h battery',
    price: 199.99,
    currency: 'USD',
    inStock: true,
    addedAt: new Date().toISOString()
  },
  {
    id: 'p-2',
    name: 'Travel Mug',
    description: 'Double-wall insulated stainless steel (12oz)',
    price: 24.5,
    currency: 'USD',
    inStock: false,
    addedAt: new Date().toISOString()
  }
];

Shared Navigation Labels with msg and useMessages

Use msg to mark shared strings in config, then decode them via useMessages at render time.

src/nav.ts
import { msg } from 'gt-react'; // See: /docs/react/api/strings/msg

export const nav = [
  { label: msg('Home'), href: '#' },
  { label: msg('Products'), href: '#products' },
  { label: msg('Cart'), href: '#cart' }
];
src/components/Header.tsx
import { LocaleSelector, T } from 'gt-react';
import { useMessages } from 'gt-react'; // See: /docs/react/api/strings/useMessages
import { nav } from '../nav';

export default function Header() {
  const m = useMessages();
  return (
    <header style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
      <T><h1>Mini Shop</h1></T> {/* See: /docs/react/api/components/t */}
      <nav style={{ display: 'flex', gap: 12 }}>
        {nav.map(item => (
          <a key={item.href} href={item.href} title={m(item.label)}>
            {m(item.label)}
          </a>
        ))}
      </nav>
      <div style={{ marginLeft: 'auto' }}>
        <LocaleSelector /> {/* See: /docs/react/api/components/localeSelector */}
      </div>
    </header>
  );
}

Product Cards with <T>, Variables, Branch, and Currency

Use <T> for JSX translation. Wrap dynamic content with variable components like <Var>, <Num>, <Currency>, and <DateTime>. Handle stock state via <Branch>.

src/components/ProductCard.tsx
import { T, Var, Num, Currency, DateTime, Branch } from 'gt-react';
import type { Product } from '../data';

export default function ProductCard({ product, onAdd }: { product: Product; onAdd: () => void; }) {
  return (
    <div style={{ border: '1px solid #ddd', padding: 12, borderRadius: 8 }}>
      <T>
        <h3><Var>{product.name}</Var></h3>
        <p><Var>{product.description}</Var></p>
        <p>
          Price: <Currency currency={product.currency}>{product.price}</Currency>
        </p>
        <p>
          Added: <DateTime options={{ dateStyle: 'medium', timeStyle: 'short' }}>{product.addedAt}</DateTime>
        </p>
        <Branch
          branch={product.inStock}
          true={<p>In stock</p>}
          false={<p style={{ color: 'tomato' }}>Out of stock</p>}
        />
        <button onClick={onAdd} disabled={!product.inStock}>
          Add to cart
        </button>
      </T>
    </div>
  );
}

Cart with Pluralization and Totals

Use <Plural> to express “X items in cart” and <Currency> for totals. Combine with <T>, <Var>, and <Num>.

src/components/Cart.tsx
import { T, Plural, Var, Num, Currency } from 'gt-react';
import type { Product } from '../data';

export default function Cart({ items, onClear }: { items: Product[]; onClear: () => void; }) {
  const total = items.reduce((sum, p) => sum + p.price, 0);
  const itemCount = items.length;
  return (
    <div style={{ borderTop: '1px solid #eee', paddingTop: 12 }}>
      <T>
        <h2>Cart</h2>
        <Plural
          n={itemCount}
          zero={<p>Your cart is empty</p>}
          one={<p>You have <Num>{itemCount}</Num> item</p>}
          other={<p>You have <Num>{itemCount}</Num> items</p>}
        />
        {items.map((p) => (
          <p key={p.id}>
            <Var>{p.name}</Var> — <Currency currency={p.currency}>{p.price}</Currency>
          </p>
        ))}
        <p>
          Total: <Currency currency={items[0]?.currency || 'USD'}>{total}</Currency>
        </p>
        <button onClick={onClear} disabled={itemCount === 0}>Clear cart</button>
      </T>
    </div>
  );
}

Attributes and Placeholders with useGT

Use useGT for plain string translations like input placeholders and ARIA labels.

src/components/Search.tsx
import { useGT } from 'gt-react';

export default function Search({ onQuery }: { onQuery: (q: string) => void; }) {
  const t = useGT();
  return (
    <input
      type="search"
      placeholder={t('Search products...')}
      aria-label={t('Search input')}
      onChange={(e) => onQuery(e.target.value)}
      style={{ padding: 8, width: '100%', maxWidth: 320 }}
    />
  );
}

Put It Together

A single-page app with in-memory cart and simple search filter.

src/App.tsx
import { useMemo, useState } from 'react';
import Header from './components/Header';
import Search from './components/Search';
import ProductCard from './components/ProductCard';
import Cart from './components/Cart';
import { products } from './data';

export default function App() {
  const [query, setQuery] = useState('');
  const [cart, setCart] = useState<string[]>([]);

  const filtered = useMemo(() => {
    const q = query.toLowerCase();
    return products.filter(p =>
      p.name.toLowerCase().includes(q) || p.description.toLowerCase().includes(q)
    );
  }, [query]);

  const items = products.filter(p => cart.includes(p.id));

  return (
    <div style={{ margin: '24px auto', maxWidth: 960, padding: 16 }}>
      <Header />
      <div style={{ margin: '16px 0' }}>
        <Search onQuery={setQuery} />
      </div>
      <section id="products" style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', gap: 16 }}>
        {filtered.map(p => (
          <ProductCard
            key={p.id}
            product={p}
            onAdd={() => setCart(c => (p.inStock ? [...new Set([...c, p.id])] : c))}
          />
        ))}
      </section>
      <section id="cart" style={{ marginTop: 24 }}>
        <Cart
          items={items}
          onClear={() => setCart([])}
        />
      </section>
    </div>
  );
}

Run Locally

Add a simple dev script to your package.json, then start the app.

package.json
{
  "scripts": {
    "dev": "vite"
  }
}

Run:

npm run dev
package.json
{
  "scripts": {
    "start": "react-scripts start"
  }
}

Run:

npm start

What You Learned

Next Steps

How is this guide?

Internationalize a Mini Shop