Esc

glosa

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

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>.

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.

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

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.

  • 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.

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.

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.

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.
{
  "previous": {
    "73ce46e85e": {
      "hash": "1e87f750d2",
      "source": "You have new mail.",
      "translations": { "es": "Tienes correo nuevo." }
    }
  }
}
locales/.glosa.json

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.

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.

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.

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)));
}
ts

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.

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.