# Internationalization (i18n) Guide

## Overview

`marko-web-theme-monorail` supports internationalization through an `i18n` function that must be provided by consuming sites. This guide explains how the theme handles translations and how to contribute components that respect multilingual requirements.

## Core Principle

**Any hardcoded user-facing text in theme components MUST be wrapped with the i18n function.** This ensures sites can translate UI text without modifying the theme package itself.

---

## How It Works

### For Sites (Consuming Projects)

Sites provide an i18n function that maps English strings to their translated equivalents:

**Example: `sites/mundopmmi.com/config/i18n.js`**
```javascript
module.exports = {
  "we hope you've enjoyed your articles.": "Esperamos haya disfrutado sus artículos.",
  'create a free account': 'Cree una cuenta gratuita',
  'account to continue reading': 'para continuar leyendo',
  // ... hundreds more translations
};
```

Then in the site's start-server call:
```javascript
// sites/mundopmmi.com/index.js
const i18n = require('./config/i18n');

module.exports = startServer({
  // ... config ...
  i18n: (v) => i18n[`${v}`.toLowerCase()] || v,
});
```

This makes `out.global.i18n` available to all theme components.

> ⚠️ **Key casing.** Note the `.toLowerCase()` above — on this site the keys must be
> **lowercase**, even though the theme passes mixed-case English. A key written as
> `'Create a free account'` never matches, and because the lookup ends in `|| v` it
> fails **silently**, rendering the English string as though no translation existed.
>
> Normalization is a per-site choice, so check the site's own function before adding
> keys. When adding a batch, verify them *through that function* rather than by
> checking the config object directly — a direct property check passes on keys the
> site can never look up.

### For Theme Components

#### Marko Components (Server-side)

1. **Destructure i18n with a fallback:**
   ```marko
   $ const { i18n } = out.global;
   $ const i18nFunc = typeof i18n === 'function' ? i18n : (v) => v;
   ```

2. **Wrap user-facing strings:**
   ```marko
   $ const title = i18nFunc("We hope you've enjoyed your articles.");
   $ const buttonLabel = i18nFunc("Create a free account");
   ```

3. **Handle dynamic content carefully:**
   ```marko
   // For singular/plural variants:
   $ const remaining = viewLimit - views;
   $ let message;
   $ if (remaining === 1) {
   $   message = i18nFunc("You have 1 article view remaining.");
   $ } else {
   $   // Use placeholder for numbers
   $   message = i18nFunc("You have {count} article views remaining.");
   $   message = message.replace('{count}', remaining);
   $ }
   ```

#### Vue/Browser Components (Client-side)

For client-side Vue components, translations must be passed as props from the Marko template:

**In Marko template:**
```marko
$ const { i18n } = out.global;
$ const i18nFunc = typeof i18n === 'function' ? i18n : (v) => v;

$ const locale = {
  "Sign up": i18nFunc("Sign up"),
  "Email address": i18nFunc("Email address"),
  "Subscribe": i18nFunc("Subscribe"),
};

<marko-web-browser-component
  name="InlineNewsletterForm"
  props={ locale, defaultNewsletter, ... }
/>
```

**In Vue component:**
```vue
<template>
  <form @submit="onSubmit">
    <label>{{ locale['Email address'] || 'Email address' }}</label>
    <input type="email" placeholder="user@example.com" />
    <button type="submit">{{ locale['Subscribe'] || 'Subscribe' }}</button>
  </form>
</template>

<script>
export default {
  props: {
    locale: {
      type: Object,
      default: () => ({}),
    },
  },
  methods: {
    onSubmit() {
      // ...
    },
  },
}
</script>
```

**Watch for Vue components you render indirectly.** A Marko component is not
finished just because its own strings are wrapped — any browser component it
renders is a separate surface with its own hardcoded English, and it will not
inherit `out.global.i18n`.

`content-meter.marko` hit this. Its login form is a Marko tag that renders the
Vue component `IdentityXLogin`, whose submit button is:

```vue
{{ buttonLabels.continue || "Continue" }}
```

Every string in the meter translated, and the button stayed English, because
nothing passed `button-labels` down. Adding a `'continue'` key to the site
config does nothing here — the string never reaches the server-side `i18n`.
The fix belongs in the theme:

```marko
$ const buttonLabels = defaultValue(input.buttonLabels, {
  continue: i18nFunc("Continue"),
  profile: i18nFunc("Modify Profile"),
  logout: i18nFunc("Logout"),
});

<marko-web-identity-x-form-login button-labels=buttonLabels ... />
```

Keep the English defaults byte-identical to the Vue component's own prop
defaults, so sites without i18n render exactly as before.

When converting a component, grep its subtree for `marko-web-browser-component`
and for any tag that forwards a label prop, and check each one — the failure is
silent and looks like "the translations didn't take".

---

## Best Practices

### 1. Use Simple, Descriptive English Strings

✅ **Good:**
```marko
$ const label = i18nFunc("Create a free account");
```

❌ **Bad:**
```marko
$ const label = i18nFunc("Create Account"); // Too vague, hard to translate consistently
```

### 2. Avoid Inline Interpolation with Numbers

✅ **Good:**
```marko
$ const template = i18nFunc("You have {count} article views remaining.");
$ const message = template.replace('{count}', remaining);
```

❌ **Bad:**
```marko
$ const message = i18nFunc(`You have ${remaining} article views remaining.`);
// Translators can't translate this—the number is embedded
```

### 3. Keep Related Strings Together

✅ **Good:**
```marko
$ const createLabel = i18nFunc("Create a free");
$ const accountLabel = i18nFunc("account to continue reading");
$ const message = `${createLabel} <strong>${siteName}</strong> ${accountLabel}`;
```

This allows translators to reorder the sentence structure if needed.

❌ **Bad:**
```marko
$ const message = i18nFunc(`Create a free ${siteName} account to continue reading`);
```

### 4. Always Provide a Fallback

Always check if i18n exists and fall back to English:

```marko
$ const i18nFunc = typeof i18n === 'function' ? i18n : (v) => v;
$ const label = i18nFunc("Your label");
// If i18n doesn't exist, label will be "Your label" (English)
```

This ensures backward compatibility with sites that don't have an i18n config.

### 5. Document Required Translation Strings

When adding new user-facing text, document it in the component or in a comment:

```marko
<!--
Required translation strings for this component:
- "We hope you've enjoyed your articles."
- "You have 1 article view remaining."
- "You have {count} article views remaining."
- "This is your last free article."
- "Enjoy this free article."
- "Create a free account"
- "Create a free"
- "account to continue reading"
-->
```

Or add a comment in your PR describing what strings need to be added to site i18n configs.

---

## Example: Adding i18n to a Component

### Before (No i18n)
```marko
$ const title = "View All Products";
$ const emptyMessage = "No products found.";

<div class="products">
  <h2>${title}</h2>
  <if(!products.length)>
    <p>${emptyMessage}</p>
  </if>
</div>
```

### After (With i18n)
```marko
$ const { i18n } = out.global;
$ const i18nFunc = typeof i18n === 'function' ? i18n : (v) => v;

$ const title = i18nFunc("View All Products");
$ const emptyMessage = i18nFunc("No products found.");

<div class="products">
  <h2>${title}</h2>
  <if(!products.length)>
    <p>${emptyMessage}</p>
  </if>
</div>
```

### Site Implementation
Sites using the component add translations to their i18n config:

```javascript
// sites/example-spanish/config/i18n.js
// Keys lowercase — this site's lookup lowercases before indexing.
module.exports = {
  'view all products': 'Ver Todos los Productos',
  'no products found.': 'No se encontraron productos.',
};
```

---

## Handling Missing Translations

If a site doesn't have a translation in its i18n config, the fallback behavior is:

```javascript
// sites/example-spanish/config/i18n.js
i18n: (v) => i18n[`${v}`.toLowerCase()] || v
//                                           ^ returns English if not found
```

This means:
- ✅ Sites will display in English if a translation is missing
- ✅ No runtime errors
- ✅ Graceful degradation
- ⚠️ Encourages sites to keep their i18n configs up-to-date with new theme versions

---

## Release Notes Template

When releasing a new version with i18n changes, include:

```markdown
## vX.Y.Z

### New Features
- Added internationalization support to `content-meter` block component

### i18n Changes (⚠️ Action Required for Multilingual Sites)
The following new translation strings are required:

**content-meter.marko:**
- `We hope you've enjoyed your articles.`
- `You have 1 article view remaining.`
- `You have {count} article views remaining.`
- `This is your last free article.`
- `Enjoy this free article.`
- `Create a free account`
- `Create a free`
- `account to continue reading`

**Action:** Add these strings to your site's `config/i18n.js` file and provide translations,
keyed in whatever form your site's i18n function looks up (most lowercase — see the
key-casing warning in the guide).

See [I18N_GUIDE.md](./I18N_GUIDE.md) for details.
```

---

## Common Questions

### Q: What if my site doesn't have multilingual requirements?
A: You don't need to do anything. The i18n function is optional, and the theme will fall back to English text automatically.

### Q: Can I override a translation string in my site?
A: Yes. Your site's i18n config is the source of truth. Add (or modify) a translation string and it will be used instead of the theme's default English.

### Q: Should all text go through i18n, or just user-facing labels?
A: Only **user-facing text** should be translated. Technical strings (class names, data attributes, IDs, etc.) should not.

✅ Translate:
- Button labels
- Form placeholders
- Error messages
- Section titles
- Empty state messages

❌ Don't translate:
- CSS class names
- Data attributes
- HTML IDs
- Variable names
- Code comments

### Q: What about accessibility strings (aria-labels)?
A: Yes, translate these too. They're user-facing for screen readers.

```marko
$ const ariaLabel = i18nFunc("Content Meter");
<div role="region" aria-label=ariaLabel>...</div>
```

### Q: How do I handle pluralization in other languages?
A: Use placeholder strings and handle pluralization at the template level:

```marko
$ const count = items.length;
$ let message;
$ if (count === 0) {
$   message = i18nFunc("No items");
$ } else if (count === 1) {
$   message = i18nFunc("1 item");
$ } else {
$   message = i18nFunc("{count} items").replace('{count}', count);
$ }
```

Then sites provide translations for each variant:
```javascript
{
  "No items": "Sin artículos",
  "1 item": "1 artículo",
  "{count} items": "{count} artículos",
}
```

---

## References

- [marko-web-theme-monorail Repository](https://github.com/parameter1/mindful-web/tree/main/packages/marko-web-theme-monorail)
- [Consuming Site Example: mundopmmi](https://github.com/parameter1/pmmi-media-group-websites/tree/main/sites/mundopmmi.com)
