# How it works

> What becomes a message, what doesn't, how IDs and catalogs work, and what happens when the source text changes.

## Text is a message

The plugin walks each component's markup and cuts it into runs of text. Each run is one message. Inline elements inside a run become numbered tags, and expressions become named placeholders.

| You write                                                | The message                          |
| -------------------------------------------------------- | ------------------------------------ |
| `<h1>Welcome back</h1>`                                  | `Welcome back`                       |
| `<h1>Welcome back, {{ user.name }}</h1>`                 | `Welcome back, {name}`               |
| `<p>Read the <a href="/guide">guide</a> first.</p>`      | `Read the <0>guide</0> first.`       |
| `<p>Line one<br />line two</p>`                          | `Line one<0/>line two`               |
| `<p>{{ count + 1 }} items</p>`                           | `{0} items`                          |

A placeholder takes the last name in its expression, so `user.name` is `{name}`. Any other expression is numbered. The examples are Vue; JSX and Svelte give the same messages from `{user.name}`.

Inline elements are the ones that sit inside a sentence: `a`, `b`, `strong`, `em`, `span`, `small`, `abbr` and the rest of HTML's phrasing elements, plus router links (`RouterLink`, `NuxtLink`, `Link`). A tag keeps the element's attributes in the code, so a translator only sees `<0>guide</0>`.

## Where a message ends

Block elements such as `div` and `p` end a run. So does anything that can't sit inside a translated sentence: a condition, a loop or a slot.

```vue
<p>Status: <span v-if="ok">all good</span><span v-else>broken</span></p>
```

That's three messages: `Status:`, `all good` and `broken`. The same goes for `{#if}` and `{#each}` in Svelte, and for `{ok && <span>…</span>}` or a `.map()` in JSX.

## What isn't a message

- An element with `translate="no"` stays as written, with everything inside it. In a sentence it becomes a tag the translator moves but can't edit: `<p>Send <span translate="no">NIM</span> to a friend</p>` is `Send <0/> to a friend`.
- `code`, `kbd`, `samp`, `var`, `pre`, `svg`, `math`, `script`, `style` and `textarea` are never translated. Inside a sentence, `code` and its kind become `<0/>` too.
- `v-pre` in Vue keeps an element as written.
- Text with no letters, or only a URL or an email address, isn't a message.
- A component with no children inside a sentence, such as `<Icon />`, is a `<0/>` tag.
- Strings in `<script>` and plain JavaScript aren't read.

The build warns about text it skips because it looks like a placeholder, such as a literal `{name}` in the markup.

## Attributes

These attributes are messages too: `title`, `alt`, `placeholder`, `label`, `aria-label`, `aria-description`, `aria-placeholder` and `aria-roledescription`. Only static values count: `alt="Company logo"` is a message, and `:alt="logo"` or `alt={logo}` isn't. In React, an attribute is translated inside a component, where the plugin can call a hook, and left alone in JSX outside one.

## IDs and catalogs

A message's ID is the first ten hex characters of the SHA-256 of its source text. The same text anywhere in the app is one message, translated once. Changing a link, a class or an expression keeps the ID, since the text stays the same.

The plugin keeps three kinds of files in `locales/`:

| File                    | What it holds                                                               | Who writes it   |
| ----------------------- | --------------------------------------------------------------------------- | --------------- |
| `en.json`               | Every message, by ID, in the source locale.                                 | The plugin      |
| `es.json`, `fr.json`, … | Translations, by the same IDs.                                              | You, or x18n    |
| `.glosa.json`           | Where each message is used, by file and line, and what an edit replaced.    | The plugin      |

Each is flat JSON, so any tool that reads locale files reads them. The plugin scans every component under the Vite root when it starts, not only the ones a build imports, so a catalog never loses a message a lazy route uses.

## When the source changes

Changing the words changes the ID. Each time it writes the catalogs, the plugin matches every new message with the one it replaced: one that left the same file, with the most similar text.

| You changed                                                  | What happens to the translations                                                     |
| ------------------------------------------------------------ | ------------------------------------------------------------------------------------ |
| A placeholder's name, `{{ user.name }}` to `{{ account.displayName }}` | They move to the new ID, with the placeholder renamed.                     |
| The words, `You have new mail.` to `You have new messages.`  | The old text and its translations are kept in `.glosa.json` as `previous`.          |
| A link, a class or an expression                             | Nothing. The ID is the same.                                                         |

```json [locales/.glosa.json]
{
  "previous": {
    "73ce46e85e": {
      "hash": "1e87f750d2",
      "source": "You have new mail.",
      "translations": { "es": "Tienes correo nuevo." }
    }
  }
}
```

A translator, or a model, updates the new message from there. Once every locale has a translation, the entry goes away. Translations under old IDs stay in their files until you remove them.

## Translations are checked

A translation may move tags and placeholders, but it keeps every one of them, and its tags nest. When one doesn't, the page shows the source text instead of a broken sentence.

## Server rendering

`createGlosa` makes a store: the locale and its catalogs. On the server, make one per request, so two requests never share a locale. Load the request's locale before you render, and the same one in the browser before you hydrate.

```ts
import config from "virtual:glosa";
import { createGlosa, glosaVue } from "glosa/vue";
import { createSSRApp } from "vue";
import { renderToString } from "vue/server-renderer";
import App from "./App.vue";

export async function render(locale: string) {
  const glosa = createGlosa(config);
  await glosa.setLocale(locale);
  return renderToString(createSSRApp(App).use(glosaVue(glosa)));
}
```

In React, wrap each request's tree in its own `<GlosaProvider>`. In Svelte, pass the store to the root component as a prop and call `setGlosa` with it.

## Live updates

In dev, saving a component updates `en.json`. Saving a catalog, from your editor, x18n or its devtools, sends it to the page, which re-renders its messages without a reload. A translation shows up as you write it.

## Sitemap

See the full list of pages in [llms.txt](https://glosa.onmax.me/llms.txt).
