Skip to content

Runtime Playground

Public docs can now run a Vue-based demo that mirrors selected runtime scenarios from apps/playground-ts.

Interactive Demo

Public Vue demo based on the same generated keys/table and translation resources used by apps/playground-ts.

It contains a subset of scenarios focused on ICU features and missing translation handling, with side-by-side result comparisons between the three runtimes. The "basic" runtime uses the core lookup and formatting logic without ICU support, while the "icu-subset" and "icu-formatjs" runtimes use different approaches to support ICU message syntax and features. Use the controls below to switch languages, toggle missing translation strategies, and filter scenarios by group.

Static title

core

Baseline lookup without placeholders.

keygreeting_title
RuntimeResult
basicWelcome to typekit-i18n
icu-subsetWelcome to typekit-i18n
icu-formatjsWelcome to typekit-i18n

Legacy formatter placeholder

core

Backward-compatible {amount|currency} rendering.

keyinvoice_totalplaceholder[{"key":"amount","value":99.99}]
RuntimeResult
basicInvoice total: $99.99
icu-subsetInvoice total: $99.99
icu-formatjsInvoice total: $99.99

Select + plural

icu

Combined ICU select/plural expression.

keyinbox_summaryplaceholder[{"key":"gender","value":"female"},{"key":"count","value":1}]
RuntimeResult
basic{gender, select, male {He has} female {She has} other {They have}} {count, plural, =0 {no messages} one {# message} other {# messages}}.
icu-subsetShe has 1 message.
icu-formatjsShe has 1 message.

ICU skeleton arguments

icu

Compact number and date/time skeleton forms.

keyicu_argument_skeleton_demoplaceholder[{"key":"amount","value":1234567},{"key":"when","value":"2025-01-15T12:34:56.000Z"}]
RuntimeResult
basicCompact: {amount, number, ::compact-short}; Date skeleton: {when, date, ::yyyy-MM-dd}; Time skeleton: {when, time, ::HH:mm:ss}
icu-subsetCompact: 1.2M; Date skeleton: 01/15/2025; Time skeleton: 12:34:56
icu-formatjsCompact: 1.2M; Date skeleton: 01/15/2025; Time skeleton: 12:34:56

ICU rounding mode floor

icu

FormatJS extension token support comparison.

keyicu_formatjs_rounding_floor_demoplaceholder[{"key":"amount","value":1.239}]
RuntimeResult
basicRounding floor mode: {amount, number, ::.00 rounding-mode-floor}
icu-subsetICU syntax error for key "icu_formatjs_rounding_floor_demo" in "en" at line 1, column 22: Invalid ICU number style for expression "amount, number, ::.00 rounding-mode-floor".
icu-formatjsRounding floor mode: 1.23

Missing language fallback

fallback

In strict mode this key throws because ES text is empty.

keyfallback_demo
RuntimeResult
basicThis text should fallback
icu-subsetThis text should fallback
icu-formatjsThis text should fallback

Shared Translation Base

The docs demo consumes the same generated keys and table from apps/playground-ts/generated. apps/docs-site also runs playground-ts generation before docs dev/build, so translation changes stay centralized.

Shared config source:

ts
import { defineTypekitI18nConfig } from '@number10/typekit-i18n/codegen'

const config = defineTypekitI18nConfig({
  input: [
    './translations/ui.csv',
    './translations/features.yaml',
    './translations/diagnostics.csv',
    './translations/icu.yaml',
  ],
  output: './generated/translationTable.ts',
  outputKeys: './generated/translationKeys.ts',
  languages: ['en', 'de', 'es', 'fr', 'ar', 'pl'] as const,
  defaultLanguage: 'en',
})

export default config

Generated key unions:

ts
/*
   This file is generated.
   Source files:
   [1/4] "translations/diagnostics.csv"
   [2/4] "translations/features.yaml"
   [3/4] "translations/icu.yaml"
   [4/4] "translations/ui.csv"
*/
// cspell:disable

export type TranslateKey = "diagnostics_title" | "no_issues" | "missing_count" | "greeting_title" | "greeting_body" | "item_count" | "price_formatted" | "date_formatted" | "fallback_demo" | "placeholder_basic" | "placeholder_number" | "inbox_summary" | "invoice_total" | "icu_argument_style_demo" | "icu_argument_skeleton_demo" | "ranking_place" | "group_invite" | "icu_escape_demo" | "plural_categories_demo" | "icu_formatjs_sign_demo" | "icu_formatjs_unit_demo" | "icu_formatjs_accounting_demo" | "icu_formatjs_precision_demo" | "icu_formatjs_rounding_floor_demo" | "icu_formatjs_scientific_demo" | "icu_formatjs_grouping_demo" | "icu_formatjs_unit_width_demo" | "settings_title" | "language_label" | "mode_label" | "mode_fallback" | "mode_strict" | "features_title" | "error_message"
export type TranslateKeys = TranslateKey
export const TranslationCategories = ["default", "playground"] as const
export type TranslateCategory = "default" | "playground"
export interface TranslateKeysByCategory {
  "default": "diagnostics_title" | "no_issues" | "missing_count" | "greeting_body" | "item_count" | "price_formatted" | "date_formatted" | "fallback_demo" | "placeholder_basic" | "placeholder_number" | "inbox_summary" | "invoice_total" | "icu_argument_style_demo" | "icu_argument_skeleton_demo" | "ranking_place" | "group_invite" | "icu_escape_demo" | "plural_categories_demo" | "icu_formatjs_sign_demo" | "icu_formatjs_unit_demo" | "icu_formatjs_accounting_demo" | "icu_formatjs_precision_demo" | "icu_formatjs_rounding_floor_demo" | "icu_formatjs_scientific_demo" | "icu_formatjs_grouping_demo" | "icu_formatjs_unit_width_demo" | "settings_title" | "language_label" | "mode_label" | "mode_fallback" | "mode_strict" | "features_title" | "error_message"
  "playground": "greeting_title"
}
export type TranslateKeyOf<C extends TranslateCategory> = TranslateKeysByCategory[C]
export const LanguageCodes = ["en", "de", "es", "fr", "ar", "pl"] as const
export type TranslateLanguage = (typeof LanguageCodes)[number]

Playground Source Alignment

The Vue demo is a public adaptation of the original React sample:

tsx
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import {
  Alert,
  Badge,
  Code,
  Container,
  Divider,
  Group,
  Paper,
  Select,
  SegmentedControl,
  Stack,
  Text,
  Title,
} from '@mantine/core'
import { createIcuTranslator, createTranslator } from '@number10/typekit-i18n'
import { createFormatjsIcuTranslator } from '@number10/typekit-i18n/runtime/icu-formatjs'
import type {
  MissingTranslationEvent,
  Placeholder,
  PlaceholderFormatterMap,
  PlaceholderValue,
} from '@number10/typekit-i18n'
import { LanguageCodes, type TranslateKey, type TranslateLanguage } from '@gen/translationKeys'
import { translationTable } from '@gen/translationTable'

type TranslationMode = 'fallback' | 'strict'
type ScenarioGroup = 'all' | 'core' | 'icu' | 'fallback'
type RuntimeId = 'basic' | 'icu-subset' | 'icu-formatjs'

interface ScenarioDefinition {
  id: string
  group: Exclude<ScenarioGroup, 'all'>
  title: string
  description: string
  key: TranslateKey
  placeholder?: Placeholder
}

interface RuntimeDescriptor {
  id: RuntimeId
  label: string
  subtitle: string
  color: string
}

interface RuntimeResult {
  status: 'ok' | 'error'
  value: string
}

interface LoggedMissingEvent extends MissingTranslationEvent<TranslateKey, TranslateLanguage> {
  runtime: RuntimeId
}

const runtimeDescriptors: ReadonlyArray<RuntimeDescriptor> = [
  {
    id: 'basic',
    label: 'basic',
    subtitle: 'createTranslator',
    color: 'gray',
  },
  {
    id: 'icu-subset',
    label: 'icu-subset',
    subtitle: 'createIcuTranslator',
    color: 'blue',
  },
  {
    id: 'icu-formatjs',
    label: 'icu-formatjs',
    subtitle: 'createFormatjsIcuTranslator',
    color: 'teal',
  },
]

const scenarioGroups: { value: ScenarioGroup; label: string }[] = [
  { value: 'all', label: 'All' },
  { value: 'core', label: 'Core' },
  { value: 'icu', label: 'ICU' },
  { value: 'fallback', label: 'Fallback' },
]

const numberLocaleByLanguage: Record<TranslateLanguage, string> = {
  en: 'en-US',
  de: 'de-DE',
  es: 'es-ES',
  fr: 'fr-FR',
  ar: 'ar-SA',
  pl: 'pl-PL',
}

const currencyByLanguage: Record<TranslateLanguage, 'USD' | 'EUR' | 'SAR' | 'PLN'> = {
  en: 'USD',
  de: 'EUR',
  es: 'EUR',
  fr: 'EUR',
  ar: 'SAR',
  pl: 'PLN',
}

const fixedDate = new Date('2025-01-15T12:34:56.000Z')

const scenarios: ReadonlyArray<ScenarioDefinition> = [
  {
    id: 'core-title',
    group: 'core',
    title: 'Static title',
    description: 'Identical baseline lookup without placeholders.',
    key: 'greeting_title',
  },
  {
    id: 'core-placeholder',
    group: 'core',
    title: 'Simple placeholder',
    description: 'Raw placeholder substitution for {name}.',
    key: 'greeting_body',
    placeholder: {
      data: [{ key: 'name', value: 'Developer' }],
    },
  },
  {
    id: 'core-formatter',
    group: 'core',
    title: 'Legacy formatter placeholder',
    description: 'Backward-compatible {amount|currency} rendering.',
    key: 'invoice_total',
    placeholder: {
      data: [{ key: 'amount', value: 99.99 }],
    },
  },
  {
    id: 'core-date-formatter',
    group: 'core',
    title: 'Date formatter hook',
    description: 'Custom formatter callback with Date values.',
    key: 'date_formatted',
    placeholder: {
      data: [{ key: 'date', value: fixedDate }],
    },
  },
  {
    id: 'icu-select-plural',
    group: 'icu',
    title: 'Select + plural',
    description: 'Combined ICU select/plural expression.',
    key: 'inbox_summary',
    placeholder: {
      data: [
        { key: 'gender', value: 'female' },
        { key: 'count', value: 1 },
      ],
    },
  },
  {
    id: 'icu-plural-categories',
    group: 'icu',
    title: 'Plural categories',
    description: 'Locale category handling (zero/one/few/many/other).',
    key: 'plural_categories_demo',
    placeholder: {
      data: [{ key: 'count', value: 11 }],
    },
  },
  {
    id: 'icu-ordinal',
    group: 'icu',
    title: 'Selectordinal',
    description: 'Ordinal suffix handling by language.',
    key: 'ranking_place',
    placeholder: {
      data: [{ key: 'place', value: 3 }],
    },
  },
  {
    id: 'icu-offset',
    group: 'icu',
    title: 'Plural offset',
    description: 'Offset subtraction for group invite style messages.',
    key: 'group_invite',
    placeholder: {
      data: [{ key: 'count', value: 5 }],
    },
  },
  {
    id: 'icu-argument-style',
    group: 'icu',
    title: 'ICU style arguments',
    description: 'Number/date/time styles in ICU argument expressions.',
    key: 'icu_argument_style_demo',
    placeholder: {
      data: [
        { key: 'amount', value: 1234.56 },
        { key: 'ratio', value: 0.42 },
        { key: 'when', value: fixedDate },
      ],
    },
  },
  {
    id: 'icu-argument-skeleton',
    group: 'icu',
    title: 'ICU skeleton arguments',
    description: 'Compact number and date/time skeleton forms.',
    key: 'icu_argument_skeleton_demo',
    placeholder: {
      data: [
        { key: 'amount', value: 1234567 },
        { key: 'when', value: fixedDate },
      ],
    },
  },
  {
    id: 'icu-escape',
    group: 'icu',
    title: 'ICU escaping',
    description: 'Apostrophe escaping and literal brace output.',
    key: 'icu_escape_demo',
    placeholder: {
      data: [{ key: 'name', value: 'Alice' }],
    },
  },
  {
    id: 'divergence-sign',
    group: 'icu',
    title: 'ICU sign-always skeleton',
    description: '::sign-always compact-short; runtime differences are visible side by side.',
    key: 'icu_formatjs_sign_demo',
    placeholder: {
      data: [{ key: 'amount', value: 1234 }],
    },
  },
  {
    id: 'divergence-unit',
    group: 'icu',
    title: 'ICU unit skeleton',
    description: '::unit/kilometer; runtime differences are visible side by side.',
    key: 'icu_formatjs_unit_demo',
    placeholder: {
      data: [{ key: 'distance', value: 5 }],
    },
  },
  {
    id: 'icu-plus-accounting',
    group: 'icu',
    title: 'ICU accounting sign',
    description: '::sign-accounting currency/USD for accounting-style negatives.',
    key: 'icu_formatjs_accounting_demo',
    placeholder: {
      data: [{ key: 'amount', value: -1234.56 }],
    },
  },
  {
    id: 'icu-plus-precision',
    group: 'icu',
    title: 'ICU fixed fraction precision',
    description: '::.00 enforces exactly two fractional digits.',
    key: 'icu_formatjs_precision_demo',
    placeholder: {
      data: [{ key: 'amount', value: 1234.5 }],
    },
  },
  {
    id: 'icu-plus-rounding-floor',
    group: 'icu',
    title: 'ICU rounding mode floor',
    description: '::.00 rounding-mode-floor rounds down instead of standard rounding.',
    key: 'icu_formatjs_rounding_floor_demo',
    placeholder: {
      data: [{ key: 'amount', value: 1.239 }],
    },
  },
  {
    id: 'icu-plus-scientific',
    group: 'icu',
    title: 'ICU scientific notation',
    description: '::scientific formats values in exponent notation.',
    key: 'icu_formatjs_scientific_demo',
    placeholder: {
      data: [{ key: 'amount', value: 12345 }],
    },
  },
  {
    id: 'icu-plus-grouping-off',
    group: 'icu',
    title: 'ICU grouping off',
    description: '::group-off disables thousands grouping.',
    key: 'icu_formatjs_grouping_demo',
    placeholder: {
      data: [{ key: 'amount', value: 12345 }],
    },
  },
  {
    id: 'icu-plus-unit-width',
    group: 'icu',
    title: 'ICU unit width full name',
    description: '::unit/kilometer unit-width-full-name spells out the unit.',
    key: 'icu_formatjs_unit_width_demo',
    placeholder: {
      data: [{ key: 'distance', value: 5 }],
    },
  },
  {
    id: 'fallback-missing-language',
    group: 'fallback',
    title: 'Missing language fallback',
    description: 'In "strict" mode this key throws because ES text is empty.',
    key: 'fallback_demo',
  },
]

/**
 * Custom formatters used by all runtime variants.
 */
const formatters: PlaceholderFormatterMap<string, TranslateLanguage> = {
  currency: (value, context) => {
    const num = typeof value === 'number' ? value : parseFloat(String(value))
    const locale = numberLocaleByLanguage[context.language]
    const currency = currencyByLanguage[context.language]
    return new Intl.NumberFormat(locale, {
      style: 'currency',
      currency,
    }).format(num)
  },
  dateShort: (value, context) => {
    const date = value instanceof Date ? value : new Date(String(value))
    const locale = numberLocaleByLanguage[context.language]
    return new Intl.DateTimeFormat(locale, {
      year: 'numeric',
      month: 'short',
      day: 'numeric',
    }).format(date)
  },
}

const toSerializableValue = (value: PlaceholderValue): string | number | boolean => {
  if (value instanceof Date) {
    return value.toISOString()
  }
  if (typeof value === 'bigint') {
    return `${value.toString()}n`
  }
  return value
}

const toPlaceholderPreview = (placeholder?: Placeholder): string | null => {
  if (!placeholder) {
    return null
  }

  const serialized = placeholder.data.map((entry) => ({
    key: entry.key,
    value: toSerializableValue(entry.value),
  }))

  return JSON.stringify(serialized)
}

const toErrorMessage = (error: unknown): string =>
  error instanceof Error ? error.message : String(error)

/**
 * Playground app that compares runtime output across three translator variants.
 *
 * @returns Interactive comparison view.
 */
export const App = (): JSX.Element => {
  const [language, setLanguage] = useState<TranslateLanguage>('en')
  const [mode, setMode] = useState<TranslationMode>('fallback')
  const [activeGroup, setActiveGroup] = useState<ScenarioGroup>('all')
  const [missingEvents, setMissingEvents] = useState<LoggedMissingEvent[]>([])

  const missingEventsRef = useRef<LoggedMissingEvent[]>([])
  const seenMissingEventKeysRef = useRef<Set<string>>(new Set<string>())

  const clearDiagnostics = useCallback((): void => {
    missingEventsRef.current = []
    seenMissingEventKeysRef.current.clear()
    setMissingEvents([])
  }, [])

  const reportMissing = useCallback(
    (runtime: RuntimeId, event: MissingTranslationEvent<TranslateKey, TranslateLanguage>): void => {
      const dedupeKey = `${runtime}::${event.key}::${event.language}::${event.reason}`
      if (seenMissingEventKeysRef.current.has(dedupeKey)) {
        return
      }
      seenMissingEventKeysRef.current.add(dedupeKey)
      missingEventsRef.current.push({
        ...event,
        runtime,
      })
    },
    []
  )

  const basic = useMemo(
    () =>
      createTranslator(translationTable, {
        language,
        missingStrategy: mode,
        formatters,
        onMissingTranslation: (event) => reportMissing('basic', event),
      }),
    [language, mode, reportMissing]
  )

  const icuSubset = useMemo(
    () =>
      createIcuTranslator(translationTable, {
        language,
        missingStrategy: mode,
        formatters,
        onMissingTranslation: (event) => reportMissing('icu-subset', event),
      }),
    [language, mode, reportMissing]
  )

  const icuFormatjs = useMemo(
    () =>
      createFormatjsIcuTranslator(translationTable, {
        language,
        missingStrategy: mode,
        formatters,
        onMissingTranslation: (event) => reportMissing('icu-formatjs', event),
      }),
    [language, mode, reportMissing]
  )

  const filteredScenarios = useMemo(
    () =>
      activeGroup === 'all'
        ? scenarios
        : scenarios.filter((scenario) => scenario.group === activeGroup),
    [activeGroup]
  )

  useEffect(() => {
    if (missingEventsRef.current.length !== missingEvents.length) {
      setMissingEvents([...missingEventsRef.current])
    }
  }, [missingEvents.length, language, mode, activeGroup])

  const runScenario = useCallback(
    (runtime: RuntimeId, key: TranslateKey, placeholder?: Placeholder): RuntimeResult => {
      try {
        if (runtime === 'basic') {
          return {
            status: 'ok',
            value: placeholder ? basic(key, placeholder) : basic(key),
          }
        }
        if (runtime === 'icu-subset') {
          return {
            status: 'ok',
            value: placeholder ? icuSubset(key, placeholder) : icuSubset(key),
          }
        }
        return {
          status: 'ok',
          value: placeholder ? icuFormatjs(key, placeholder) : icuFormatjs(key),
        }
      } catch (error: unknown) {
        return {
          status: 'error',
          value: toErrorMessage(error),
        }
      }
    },
    [basic, icuSubset, icuFormatjs]
  )

  const handleLanguageChange = (value: string | null): void => {
    if (!value) {
      return
    }
    clearDiagnostics()
    setLanguage(value as TranslateLanguage)
  }

  const handleModeChange = (value: string | null): void => {
    if (!value) {
      return
    }
    clearDiagnostics()
    setMode(value as TranslationMode)
  }

  return (
    <Container size="xl" py="lg">
      <Stack gap="md">
        <Paper
          p="lg"
          radius="lg"
          withBorder
          style={{
            background:
              'linear-gradient(160deg, rgba(32, 40, 62, 0.95) 0%, rgba(19, 29, 52, 0.95) 45%, rgba(10, 46, 57, 0.95) 100%)',
          }}
        >
          <Stack gap="xs">
            <Group justify="space-between" align="flex-start">
              <Stack gap={2}>
                <Text size="xs" c="cyan.2" tt="uppercase" fw={700}>
                  Runtime Comparison Playground
                </Text>
                <Title order={1} size="1.6rem" c="white">
                  {basic('greeting_title')}
                </Title>
                <Text c="gray.2">
                  Compare output of all three runtimes on the same translation key and payload.
                </Text>
              </Stack>
              <Badge size="lg" variant="light" color="cyan">
                {language.toUpperCase()} / {mode}
              </Badge>
            </Group>
          </Stack>
        </Paper>

        <Paper p="md" radius="md" withBorder>
          <Stack gap="sm">
            <Group grow align="flex-end">
              <Select
                label={basic('language_label')}
                value={language}
                onChange={handleLanguageChange}
                data={LanguageCodes.map((lang) => ({
                  value: lang,
                  label: lang.toUpperCase(),
                }))}
              />
              <Select
                label={basic('mode_label')}
                value={mode}
                onChange={handleModeChange}
                data={[
                  { value: 'fallback', label: basic('mode_fallback') },
                  { value: 'strict', label: basic('mode_strict') },
                ]}
              />
            </Group>
            <SegmentedControl
              fullWidth
              value={activeGroup}
              onChange={(value) => setActiveGroup(value as ScenarioGroup)}
              data={scenarioGroups}
            />
          </Stack>
        </Paper>

        <Paper p="md" radius="md" withBorder>
          <Stack gap="xs">
            <Text fw={600}>Runtime Labels</Text>
            <Group gap="xs">
              {runtimeDescriptors.map((runtime) => (
                <Group key={runtime.id} gap={6}>
                  <Badge color={runtime.color} variant="light">
                    {runtime.label}
                  </Badge>
                  <Code>{runtime.subtitle}</Code>
                </Group>
              ))}
            </Group>
          </Stack>
        </Paper>

        <Stack gap="md">
          {filteredScenarios.map((scenario) => {
            const placeholderPreview = toPlaceholderPreview(scenario.placeholder)
            return (
              <Paper key={scenario.id} p="md" radius="md" withBorder>
                <Stack gap="sm">
                  <Group justify="space-between" align="center">
                    <Stack gap={2}>
                      <Text fw={600}>{scenario.title}</Text>
                      <Text size="sm" c="dimmed">
                        {scenario.description}
                      </Text>
                    </Stack>
                    <Badge variant="light" color="indigo">
                      {scenario.group}
                    </Badge>
                  </Group>

                  <Group gap="xs">
                    <Text size="xs" c="dimmed">
                      key
                    </Text>
                    <Code>{scenario.key}</Code>
                    {placeholderPreview && (
                      <>
                        <Text size="xs" c="dimmed">
                          placeholder
                        </Text>
                        <Code>{placeholderPreview}</Code>
                      </>
                    )}
                  </Group>

                  <Divider />

                  <Stack gap="xs">
                    {runtimeDescriptors.map((runtime) => {
                      const result = runScenario(runtime.id, scenario.key, scenario.placeholder)
                      return (
                        <Paper key={runtime.id} p="sm" radius="sm" withBorder>
                          <Stack gap={6}>
                            <Group justify="space-between" align="center">
                              <Badge color={runtime.color} variant="light">
                                {runtime.label}
                              </Badge>
                              <Code c="dimmed" fz="xs">
                                {runtime.subtitle}
                              </Code>
                            </Group>
                            {result.status === 'ok' ? (
                              <Text ff="monospace" size="sm">
                                {result.value}
                              </Text>
                            ) : (
                              <Alert color="red" variant="light" title="Runtime Error">
                                <Text ff="monospace" size="xs">
                                  {result.value}
                                </Text>
                              </Alert>
                            )}
                          </Stack>
                        </Paper>
                      )
                    })}
                  </Stack>
                </Stack>
              </Paper>
            )
          })}
        </Stack>

        <Paper p="md" radius="md" withBorder>
          <Stack gap="sm">
            <Group justify="space-between" align="center">
              <Text fw={600}>{basic('diagnostics_title')}</Text>
              <Badge color={missingEvents.length > 0 ? 'orange' : 'green'} variant="light">
                {missingEvents.length}
              </Badge>
            </Group>
            {missingEvents.length === 0 ? (
              <Alert color="green" variant="light">
                <Text size="sm">{basic('no_issues')}</Text>
              </Alert>
            ) : (
              <Stack gap="xs">
                {missingEvents.map((event, index) => (
                  <Paper
                    key={`${event.runtime}-${event.key}-${index}`}
                    p="xs"
                    withBorder
                    radius="sm"
                  >
                    <Group gap="xs">
                      <Badge variant="light">{event.runtime}</Badge>
                      <Code>{event.key}</Code>
                      <Code>{event.language}</Code>
                      <Code>{event.reason}</Code>
                    </Group>
                  </Paper>
                ))}
              </Stack>
            )}
          </Stack>
        </Paper>
      </Stack>
    </Container>
  )
}

Vue implementation inside docs:

vue
<script setup lang="ts">
import { computed, ref } from 'vue'
import { createIcuTranslator, createTranslator } from '@number10/typekit-i18n'
import { createFormatjsIcuTranslator } from '@number10/typekit-i18n/runtime/icu-formatjs'
import type {
  Placeholder,
  PlaceholderFormatterMap,
  PlaceholderValue,
} from '@number10/typekit-i18n'
import { LanguageCodes, type TranslateKey, type TranslateLanguage } from '@playground-gen/translationKeys'
import { translationTable } from '@playground-gen/translationTable'

type TranslationMode = 'fallback' | 'strict'
type ScenarioGroup = 'all' | 'core' | 'icu' | 'fallback'
type RuntimeId = 'basic' | 'icu-subset' | 'icu-formatjs'

interface ScenarioDefinition {
  id: string
  group: Exclude<ScenarioGroup, 'all'>
  title: string
  description: string
  key: TranslateKey
  placeholder?: Placeholder
}

interface RuntimeDescriptor {
  id: RuntimeId
  label: string
}

interface RuntimeResult {
  status: 'ok' | 'error'
  value: string
}

interface ScenarioRow {
  scenario: ScenarioDefinition
  placeholderPreview: string | null
  results: ReadonlyArray<{
    runtime: RuntimeDescriptor
    result: RuntimeResult
  }>
}

const runtimeDescriptors: ReadonlyArray<RuntimeDescriptor> = [
  {
    id: 'basic',
    label: 'basic',
  },
  {
    id: 'icu-subset',
    label: 'icu-subset',
  },
  {
    id: 'icu-formatjs',
    label: 'icu-formatjs',
  },
]

const scenarioGroups: ReadonlyArray<{ value: ScenarioGroup; label: string }> = [
  { value: 'all', label: 'All' },
  { value: 'core', label: 'Core' },
  { value: 'icu', label: 'ICU' },
  { value: 'fallback', label: 'Fallback' },
]

const numberLocaleByLanguage: Record<TranslateLanguage, string> = {
  en: 'en-US',
  de: 'de-DE',
  es: 'es-ES',
  fr: 'fr-FR',
  ar: 'ar-SA',
  pl: 'pl-PL',
}

const currencyByLanguage: Record<TranslateLanguage, 'USD' | 'EUR' | 'SAR' | 'PLN'> = {
  en: 'USD',
  de: 'EUR',
  es: 'EUR',
  fr: 'EUR',
  ar: 'SAR',
  pl: 'PLN',
}

const fixedDate = new Date('2025-01-15T12:34:56.000Z')

const scenarios: ReadonlyArray<ScenarioDefinition> = [
  {
    id: 'core-title',
    group: 'core',
    title: 'Static title',
    description: 'Baseline lookup without placeholders.',
    key: 'greeting_title',
  },
  {
    id: 'core-formatter',
    group: 'core',
    title: 'Legacy formatter placeholder',
    description: 'Backward-compatible {amount|currency} rendering.',
    key: 'invoice_total',
    placeholder: {
      data: [{ key: 'amount', value: 99.99 }],
    },
  },
  {
    id: 'icu-select-plural',
    group: 'icu',
    title: 'Select + plural',
    description: 'Combined ICU select/plural expression.',
    key: 'inbox_summary',
    placeholder: {
      data: [
        { key: 'gender', value: 'female' },
        { key: 'count', value: 1 },
      ],
    },
  },
  {
    id: 'icu-argument-skeleton',
    group: 'icu',
    title: 'ICU skeleton arguments',
    description: 'Compact number and date/time skeleton forms.',
    key: 'icu_argument_skeleton_demo',
    placeholder: {
      data: [
        { key: 'amount', value: 1234567 },
        { key: 'when', value: fixedDate },
      ],
    },
  },
  {
    id: 'icu-plus-rounding-floor',
    group: 'icu',
    title: 'ICU rounding mode floor',
    description: 'FormatJS extension token support comparison.',
    key: 'icu_formatjs_rounding_floor_demo',
    placeholder: {
      data: [{ key: 'amount', value: 1.239 }],
    },
  },
  {
    id: 'fallback-missing-language',
    group: 'fallback',
    title: 'Missing language fallback',
    description: 'In strict mode this key throws because ES text is empty.',
    key: 'fallback_demo',
  },
]

const formatters: PlaceholderFormatterMap<string, TranslateLanguage> = {
  currency: (value, context) => {
    const num = typeof value === 'number' ? value : parseFloat(String(value))
    const locale = numberLocaleByLanguage[context.language]
    const currency = currencyByLanguage[context.language]
    return new Intl.NumberFormat(locale, {
      style: 'currency',
      currency,
    }).format(num)
  },
  dateShort: (value, context) => {
    const date = value instanceof Date ? value : new Date(String(value))
    const locale = numberLocaleByLanguage[context.language]
    return new Intl.DateTimeFormat(locale, {
      year: 'numeric',
      month: 'short',
      day: 'numeric',
    }).format(date)
  },
}

const language = ref<TranslateLanguage>('en')
const mode = ref<TranslationMode>('fallback')
const activeGroup = ref<ScenarioGroup>('all')

const basic = computed(() =>
  createTranslator(translationTable, {
    language: language.value,
    missingStrategy: mode.value,
    formatters,
  })
)

const icuSubset = computed(() =>
  createIcuTranslator(translationTable, {
    language: language.value,
    missingStrategy: mode.value,
    formatters,
  })
)

const icuFormatjs = computed(() =>
  createFormatjsIcuTranslator(translationTable, {
    language: language.value,
    missingStrategy: mode.value,
    formatters,
  })
)

const filteredScenarios = computed(() =>
  activeGroup.value === 'all'
    ? scenarios
    : scenarios.filter((scenario) => scenario.group === activeGroup.value)
)

const toErrorMessage = (error: unknown): string =>
  error instanceof Error ? error.message : String(error)

const runScenario = (runtime: RuntimeId, key: TranslateKey, placeholder?: Placeholder): RuntimeResult => {
  try {
    if (runtime === 'basic') {
      return {
        status: 'ok',
        value: placeholder ? basic.value(key, placeholder) : basic.value(key),
      }
    }
    if (runtime === 'icu-subset') {
      return {
        status: 'ok',
        value: placeholder ? icuSubset.value(key, placeholder) : icuSubset.value(key),
      }
    }
    return {
      status: 'ok',
      value: placeholder ? icuFormatjs.value(key, placeholder) : icuFormatjs.value(key),
    }
  } catch (error: unknown) {
    return {
      status: 'error',
      value: toErrorMessage(error),
    }
  }
}

const toSerializableValue = (value: PlaceholderValue): string | number | boolean => {
  if (value instanceof Date) {
    return value.toISOString()
  }
  if (typeof value === 'bigint') {
    return `${value.toString()}n`
  }
  return value
}

const toPlaceholderPreview = (placeholder?: Placeholder): string | null => {
  if (!placeholder) {
    return null
  }
  const serialized = placeholder.data.map((entry) => ({
    key: entry.key,
    value: toSerializableValue(entry.value),
  }))
  return JSON.stringify(serialized)
}

const scenarioRows = computed<ReadonlyArray<ScenarioRow>>(() =>
  filteredScenarios.value.map((scenario) => ({
    scenario,
    placeholderPreview: toPlaceholderPreview(scenario.placeholder),
    results: runtimeDescriptors.map((runtime) => ({
      runtime,
      result: runScenario(runtime.id, scenario.key, scenario.placeholder),
    })),
  }))
)
</script>

<template>
  <section class="runtime-playground">
    <p>
      Public Vue demo based on the same generated keys/table and translation resources used by
      <code>apps/playground-ts</code>.
    </p>
    <p>
      It contains a subset of scenarios focused on ICU features and missing translation handling, with side-by-side result comparisons between the three runtimes. The "basic" runtime uses the core lookup and formatting logic without ICU support, while the "icu-subset" and "icu-formatjs" runtimes use different approaches to support ICU message syntax and features. Use the controls below to switch languages, toggle missing translation strategies, and filter scenarios by group.
    </p>

    <div class="control-grid">
      <label>
        Language
        <select v-model="language">
          <option v-for="lang in LanguageCodes" :key="lang" :value="lang">
            {{ lang.toUpperCase() }}
          </option>
        </select>
      </label>

      <label>
        Missing strategy
        <select v-model="mode">
          <option value="fallback">fallback</option>
          <option value="strict">strict</option>
        </select>
      </label>
    </div>

    <div class="group-switch">
      <button
        v-for="group in scenarioGroups"
        :key="group.value"
        type="button"
        :class="{ active: activeGroup === group.value }"
        @click="activeGroup = group.value"
      >
        {{ group.label }}
      </button>
    </div>

    <article v-for="row in scenarioRows" :key="row.scenario.id" class="scenario-card">
      <header>
        <h3>{{ row.scenario.title }}</h3>
        <span class="badge">{{ row.scenario.group }}</span>
      </header>

      <p>{{ row.scenario.description }}</p>

      <div class="meta-row">
        <span>key</span>
        <code>{{ row.scenario.key }}</code>
        <template v-if="row.placeholderPreview">
          <span>placeholder</span>
          <code>{{ row.placeholderPreview }}</code>
        </template>
      </div>

      <table>
        <thead>
          <tr>
            <th>Runtime</th>
            <th>Result</th>
          </tr>
        </thead>
        <tbody>
          <tr v-for="entry in row.results" :key="entry.runtime.id">
            <td>
              <strong style="white-space: nowrap;">{{ entry.runtime.label }}</strong>
            </td>
            <td :class="entry.result.status">
              <code>{{ entry.result.value }}</code>
            </td>
          </tr>
        </tbody>
      </table>
    </article>
  </section>
</template>

<style scoped>
.runtime-playground {
  display: grid;
  gap: 1rem;
}

.control-grid {
  display: grid;
  gap: 0.75rem;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
}

label {
  display: grid;
  gap: 0.4rem;
  font-size: 0.9rem;
}

select {
  border: 1px solid var(--vp-c-divider);
  border-radius: 0.5rem;
  padding: 0.45rem 0.6rem;
  background: var(--vp-c-bg-soft);
}

.group-switch {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.group-switch button {
  border: 1px solid var(--vp-c-divider);
  border-radius: 999px;
  padding: 0.35rem 0.8rem;
  background: transparent;
  cursor: pointer;
}

.group-switch button.active {
  color: var(--vp-c-bg);
  background: var(--vp-c-brand-1);
  border-color: var(--vp-c-brand-1);
}

.scenario-card {
  border: 1px solid var(--vp-c-divider);
  border-radius: 0.75rem;
  padding: 0.9rem;
  background: var(--vp-c-bg-soft);
}

.scenario-card header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 0.5rem;
}

.scenario-card h3 {
  margin: 0;
  font-size: 1rem;
}

.badge {
  border: 1px solid var(--vp-c-brand-1);
  color: var(--vp-c-brand-1);
  border-radius: 999px;
  padding: 0.1rem 0.5rem;
  font-size: 0.75rem;
}

.meta-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem;
  margin-top: 0.6rem;
  margin-bottom: 0.8rem;
  font-size: 0.82rem;
}

.meta-row span {
  color: var(--vp-c-text-2);
}

table {
  width: 100%;
  border-collapse: collapse;
}

th,
td {
  text-align: left;
  border-top: 1px solid var(--vp-c-divider);
  vertical-align: top;
  padding: 0.45rem 0;
}

.subtitle {
  color: var(--vp-c-text-2);
  font-size: 0.78rem;
}

td.ok code {
  color: var(--vp-c-text-1);
}

td.error code {
  color: var(--vp-c-danger-1);
}

code {
  white-space: pre-wrap;
}
</style>