langsys-js-vue 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,23 @@
1
+ ## 0.1.0 - Unreleased
2
+
3
+ Initial release. `langsys-js-vue` is a thin Vue 3 binding over the framework-agnostic [`langsys-js-typescript`](https://github.com/langsys/langsys-js-typescript) package — the Vue sibling of `langsys-js-react` and `langsys-js-svelte`. The base SDK owns the API client, translation lifecycle, token discovery, DOM tokenizer, and SSR-aware token strategies; this package adds only the Vue-native concerns.
4
+
5
+ ### Added
6
+
7
+ - **`LangsysApp`** — wrapper whose `init` accepts a `Signal<string>` as `UserLocaleStore` and delegates every other method to the base SDK singleton.
8
+ - **Composables** bridging base-SDK signals into `shallowRef`s with effect-scope disposal:
9
+ - `useT()` — the current translation function, updating on translations/locale change. Signature: `t(phrase, category?, params?)` with compile-time-checked interpolation params.
10
+ - `useCurrentLocale()` — the loaded locale.
11
+ - `useTranslations()` — the raw catalog.
12
+ - `useLocaleStore(initial?)` — all-in-one `{ locale, setLocale, store }` for the user-locale store.
13
+ - `useSignal(signal)` — low-level Signal → ref bridge.
14
+ - **`createLocaleStore(initial?)`** — user-locale `Signal<string>` factory (the `writable` analog), and **`refToLocaleSource(ref)`** — adapt an existing Vue `Ref<string>` (Pinia, Nuxt `useState`) into the SDK's store contract with a `flush: 'sync'` watcher.
15
+ - **`<Translate>`** — Vue component wrapping the base SDK's vanilla DOM `Translate` class: tokenizes children into a content block, registers it, re-translates on locale change. Props: `category?`, `custom_id?`, `label?`, `tag?` (default `translate`), `params?` (runtime `{name}` interpolation across content-block text, attributes, and select options, re-applied reactively via `setParams()`); `class` falls through. In markup, author placeholders as `%name%` — the base SDK normalizes them to canonical `{name}` at capture, sidestepping the framework `{{ }}`/`{ }` collision.
16
+ - **`<Phrase>`** — wraps the vanilla `Phrase` rich-text handler. Keeps a markup-bearing run as ONE translatable phrase, encoding inline markup as neutral tokens and reconstituting the real elements at render. Props: `category?`, `params?`, `tag?` (default `span`).
17
+ - **`<DontTranslate>`** — marks a region as never-translated (renders `translate="no"` + `data-ls-dont-translate`), preserved verbatim. Props: `tag?` (default `span`).
18
+ - **Re-exports** from the base SDK: raw signals (`t`, `currentlyLoadedLocale`, `sTranslations`), `createSignal`, `canonicalizeLocale`, `LangsysAppAPI`, and the framework-agnostic types.
19
+ - **SSR (Nuxt) support** via `initialTranslations` / `initialTranslationsLocale` seeding — see `README-SSR.md`.
20
+
21
+ ### Notes
22
+
23
+ - Built against `langsys-js-typescript` `^0.4.1` — includes `TranslateOptions.params` / `Translate.setParams()` (content-block interpolation) and the `%name%` → `{name}` markup normalization, both surfaced through `<Translate params>` / `<Phrase params>` with no Vue-side placeholder handling.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Flexark International Ltda.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README-SSR.md ADDED
@@ -0,0 +1,211 @@
1
+ # SSR Usage Guide (Nuxt)
2
+
3
+ This guide shows how to use `langsys-js-vue` with Server-Side Rendering (SSR) to eliminate duplicate API calls and improve performance.
4
+
5
+ ## The problem
6
+
7
+ In a traditional SSR flow:
8
+ 1. The server fetches translations during render.
9
+ 2. The client re-fetches the same translations after hydration.
10
+ 3. Duplicate API calls, slower initial render, possible flash of untranslated content.
11
+
12
+ ## The solution
13
+
14
+ Pass pre-fetched translations from server to client using the `initialTranslations` config option. The client SDK uses them as-is and skips the initial fetch. Because `useSignal` seeds its ref synchronously from the signal's current value, the first paint already reflects the seeded translations.
15
+
16
+ ## Nuxt
17
+
18
+ ### Step 1: Fetch translations on the server
19
+
20
+ Expose a server route that proxies the Langsys API using a **server-only** key (never ship the write key to the browser):
21
+
22
+ ```typescript
23
+ // server/api/langsys.get.ts
24
+ import type { iCategories } from 'langsys-js-vue';
25
+
26
+ export default defineEventHandler(async (event) => {
27
+ const config = useRuntimeConfig();
28
+ const locale = getQuery(event).locale?.toString() ?? 'en';
29
+
30
+ const res = await $fetch<{ data: iCategories }>(
31
+ `https://api.langsys.dev/api/projects/${config.langsysProjectId}/translations`,
32
+ {
33
+ query: { locale },
34
+ headers: { 'x-Authorization': config.langsysApiKey, 'Content-Type': 'application/json' },
35
+ }
36
+ );
37
+
38
+ return { locale, translations: res.data };
39
+ });
40
+ ```
41
+
42
+ ```typescript
43
+ // nuxt.config.ts
44
+ export default defineNuxtConfig({
45
+ runtimeConfig: {
46
+ langsysProjectId: '', // NUXT_LANGSYS_PROJECT_ID (server-only)
47
+ langsysApiKey: '', // NUXT_LANGSYS_API_KEY (server-only)
48
+ public: {
49
+ langsysProjectId: '', // NUXT_PUBLIC_LANGSYS_PROJECT_ID
50
+ langsysApiKey: '', // NUXT_PUBLIC_LANGSYS_API_KEY (read-only key!)
51
+ },
52
+ },
53
+ });
54
+ ```
55
+
56
+ ### Step 2: Load the payload during SSR and initialize on the client
57
+
58
+ ```vue
59
+ <!-- app.vue -->
60
+ <script setup lang="ts">
61
+ import { LangsysApp, refToLocaleSource } from 'langsys-js-vue';
62
+
63
+ // Runs on the server during SSR; the payload transfers to the client for free.
64
+ const { data } = await useAsyncData('langsys', () => $fetch('/api/langsys', { query: { locale: 'en' } }));
65
+
66
+ // Nuxt-owned locale state, adapted to the SDK's store contract.
67
+ const locale = useState('locale', () => data.value?.locale ?? 'en');
68
+
69
+ onMounted(() => {
70
+ const config = useRuntimeConfig();
71
+ LangsysApp.init({
72
+ projectid: config.public.langsysProjectId,
73
+ key: config.public.langsysApiKey, // read-only key on the client
74
+ UserLocaleStore: refToLocaleSource(locale),
75
+ baseLocale: 'en',
76
+ initialTranslations: data.value?.translations,
77
+ initialTranslationsLocale: data.value?.locale,
78
+ ssrTokenStrategy: 'client',
79
+ });
80
+ });
81
+ </script>
82
+
83
+ <template>
84
+ <NuxtPage />
85
+ </template>
86
+ ```
87
+
88
+ ### Step 3: Use translations in any component
89
+
90
+ ```vue
91
+ <script setup lang="ts">
92
+ import { useT } from 'langsys-js-vue';
93
+
94
+ const t = useT();
95
+ </script>
96
+
97
+ <template>
98
+ <h1>{{ t('Welcome', 'HomePage') }}</h1>
99
+ <p>{{ t('Hello, {name}!', 'HomePage', { name: 'Sarah' }) }}</p>
100
+ </template>
101
+ ```
102
+
103
+ ### Detecting the visitor's locale on the server
104
+
105
+ ```typescript
106
+ // server/api/langsys.get.ts (variation)
107
+ import { LangsysApp } from 'langsys-js-vue';
108
+
109
+ const acceptLanguage = getHeader(event, 'accept-language');
110
+ const locale = LangsysApp.detectPreferredLocale(acceptLanguage) || 'en';
111
+ ```
112
+
113
+ ## Locale switching
114
+
115
+ Write to the same locale state the SDK was initialized with; the SDK reacts and fetches the new locale's translations:
116
+
117
+ ```vue
118
+ <script setup lang="ts">
119
+ import { LangsysApp } from 'langsys-js-vue';
120
+
121
+ const locale = useState<string>('locale');
122
+
123
+ function changeLocale(next: string) {
124
+ locale.value = next; // subscribers in the SDK trigger a fetch
125
+ return LangsysApp.translationsLoadingPromise; // optional: await the in-flight fetch
126
+ }
127
+ </script>
128
+
129
+ <template>
130
+ <select :value="locale" @change="changeLocale(($event.target as HTMLSelectElement).value)">
131
+ <option value="en">English</option>
132
+ <option value="es">Español</option>
133
+ <option value="fr">Français</option>
134
+ </select>
135
+ </template>
136
+ ```
137
+
138
+ > Keep one locale store for the app (created where you call `init`) and share it via `useState`/Pinia, rather than creating fresh stores in unrelated trees.
139
+
140
+ ## Plain Vite SSR (no Nuxt)
141
+
142
+ The same handoff works with any Vue SSR setup: fetch the catalog in your server entry, serialize `{ locale, translations }` into the page payload, and call `LangsysApp.init` with `initialTranslations` / `initialTranslationsLocale` in your client entry before mounting.
143
+
144
+ ## Benefits
145
+
146
+ ### Performance
147
+ - No duplicate API calls (server + client).
148
+ - Translations ready immediately on hydration.
149
+ - Faster Time to Interactive (TTI).
150
+ - Reduced API usage and costs.
151
+
152
+ ### User experience
153
+ - No flash of untranslated content.
154
+ - Instant translation display.
155
+ - Better SEO with server-rendered translations.
156
+
157
+ ### Developer experience
158
+ - Simple configuration.
159
+ - Full TypeScript support, including compile-time-checked interpolation params on `t()`.
160
+
161
+ ## Configuration options
162
+
163
+ ### SSR token strategy
164
+
165
+ ```typescript
166
+ { ssrTokenStrategy: 'client' | 'server' | 'auto' }
167
+ ```
168
+
169
+ - `'client'` (default) — queue tokens, send from client after hydration.
170
+ - `'server'` — send tokens immediately from server.
171
+ - `'auto'` — small batches (≤5) from server, larger batches from client.
172
+
173
+ ### Debug mode
174
+
175
+ ```typescript
176
+ { debug: true, initialTranslations: translations, initialTranslationsLocale: locale }
177
+ ```
178
+
179
+ Look for:
180
+ - `SSR initial translations config:` on init — confirms pre-fetched data is detected.
181
+ - `Using pre-fetched translations for locale` — confirms the initial fetch was skipped.
182
+ - `Locale change detected!` — fires on a subsequent locale switch.
183
+
184
+ ## Important notes
185
+
186
+ 1. **One-time use.** `initialTranslations` is consumed only at init. Locale changes after init go through the normal fetch path.
187
+ 2. **Matching locales.** Always provide `initialTranslationsLocale` with `initialTranslations` so the SDK knows what locale the data represents.
188
+ 3. **Data format.** The translations payload must match the `iCategories` shape returned by `LangsysAppAPI.getTranslations()`.
189
+ 4. **Cache.** The 60-second locale cache still applies. Pre-fetched translations count as cached.
190
+ 5. **Token creation.** Use a read-only API key for the client in production — missing tokens won't be sent. Keep the write key on the server (and ideally pre-populate tokens via your local dev environment).
191
+ 6. **Client-side init.** `LangsysApp.init` belongs in `onMounted` (client-only) so the server render and the first client render agree; the seeded signals cover the server output.
192
+
193
+ ## Troubleshooting
194
+
195
+ ### Translations not appearing
196
+ - Check that `initialTranslationsLocale` matches the `UserLocaleStore` value at init.
197
+ - Verify the translations payload matches the `iCategories` shape.
198
+ - Enable `debug: true` and look for the messages above.
199
+
200
+ ### Still seeing duplicate API calls
201
+ - Confirm both `initialTranslations` *and* `initialTranslationsLocale` are passed.
202
+ - Confirm init runs before any rendering that calls `t(...)`.
203
+ - Confirm the locale hasn't drifted between server and client.
204
+
205
+ ### Hydration mismatch warnings
206
+ - Make sure the `locale` you seed on the server matches the initial value of the locale state on the client.
207
+ - Keep `LangsysApp.init` inside `onMounted` (client-only) so the server render and the first client render agree.
208
+
209
+ ### TypeScript errors on `t()`
210
+ - Placeholders are compile-time-checked: `t('Hello, {name}!', 'Cat')` *requires* a params object with `name`. Either add the key or remove the placeholder.
211
+ - Allowed param value types: `string | number | Date | boolean`.
package/README.md ADDED
@@ -0,0 +1,307 @@
1
+ # Langsys SDK - Vue
2
+
3
+ Langsys revolutionizes localization for apps with easy to integrate, realtime, continuous translations. Read more about Langsys Translation Manager [at the website](https://Langsys.dev/).
4
+
5
+ Integrate the Langsys Translation Manager into your Vue 3, Nuxt, or Vite applications using this SDK.
6
+
7
+ ## Requirements
8
+
9
+ - **Vue 3.4+** (the reactive layer is built on `shallowRef` + effect-scope disposal).
10
+
11
+ [![GitHub Release](https://img.shields.io/github/release/langsys/langsys-js-vue.svg?style=flat)]()
12
+ [![GitHub last commit](https://img.shields.io/github/last-commit/langsys/langsys-js-vue.svg?style=flat)]()
13
+ [![GitHub pull requests](https://img.shields.io/github/issues-pr/langsys/langsys-js-vue.svg?style=flat)]()
14
+ [![PR's Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat)](http://makeapullrequest.com)
15
+ [![NPM License](https://img.shields.io/npm/l/all-contributors.svg?style=flat)](https://github.com/langsys/langsys-js-vue/blob/main/LICENSE)
16
+
17
+ ## How it's layered
18
+
19
+ `langsys-js-vue` is a thin Vue binding over the framework-agnostic [`langsys-js-typescript`](https://github.com/langsys/langsys-js-typescript) package — which owns the API client, translation lifecycle, token discovery, DOM tokenizer, and SSR-aware token strategies. This package adds only the Vue-native concerns:
20
+
21
+ - A `LangsysApp` whose `init` accepts a `Signal<string>` (made with `createLocaleStore`, or adapted from a ref with `refToLocaleSource`) for the user locale
22
+ - Composables — `useT`, `useCurrentLocale`, `useTranslations`, `useLocaleStore` — that update components when translations or the loaded locale change
23
+ - Components — `<Translate>` (HTML content blocks), `<Phrase>` (markup-bearing phrases for pluralization), `<DontTranslate>` (never-translated regions)
24
+
25
+ If you need the SDK outside Vue (a Node script, a non-Vue web app), import from `langsys-js-typescript` directly.
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ npm install langsys-js-vue
31
+ ```
32
+
33
+ `langsys-js-typescript` is installed automatically as a transitive dependency. `vue` is a peer dependency you already have.
34
+
35
+ ## Creating a Langsys project
36
+
37
+ Visit [Langsys.dev](https://Langsys.dev/) to create your account, then create your project. Take note of your project ID and API key.
38
+
39
+ ### API key permissions
40
+
41
+ - **Write key** (development): the SDK auto-creates new translation tokens and content blocks as they appear in your app.
42
+ - **Read-only key** (production): the SDK fetches translations only — no token creation, no content-block writes.
43
+
44
+ The SDK detects the key type automatically and behaves accordingly.
45
+
46
+ ## Initialization
47
+
48
+ Initialize once, high in your tree. Create the user-locale store with `useLocaleStore` and pass it to `LangsysApp.init`:
49
+
50
+ ```vue
51
+ <!-- src/LangsysGate.vue -->
52
+ <script setup lang="ts">
53
+ import { onMounted, ref } from 'vue';
54
+ import { LangsysApp, useLocaleStore } from 'langsys-js-vue';
55
+
56
+ const { store } = useLocaleStore('en-US');
57
+ const ready = ref(false);
58
+ const error = ref<string | null>(null);
59
+
60
+ onMounted(() => {
61
+ LangsysApp.init({
62
+ projectid: import.meta.env.VITE_LANGSYS_PROJECT_ID,
63
+ key: import.meta.env.VITE_LANGSYS_API_KEY,
64
+ UserLocaleStore: store,
65
+ baseLocale: 'en-US',
66
+ debug: false,
67
+ ssrTokenStrategy: 'client',
68
+ }).then((res) => {
69
+ if (res.status) ready.value = true;
70
+ else error.value = res.errors?.join(', ') ?? 'Init failed';
71
+ });
72
+ });
73
+ </script>
74
+
75
+ <template>
76
+ <p v-if="error">Langsys init failed: {{ error }}</p>
77
+ <p v-else-if="!ready">Loading…</p>
78
+ <slot v-else />
79
+ </template>
80
+ ```
81
+
82
+ `UserLocaleStore` is a `Signal<string>` — switch it with `setLocale(...)` (from the same `useLocaleStore` call) or `store.set('fr-FR')`, and the SDK reacts. If you'd rather keep the locale store at module scope, `const localeStore = createLocaleStore('en-US')` works too. And if your app already owns the locale in a `ref` (Pinia state, Nuxt's `useState`), adapt it with `refToLocaleSource(localeRef)` instead.
83
+
84
+ Locale identifiers are canonicalized to BCP 47 by the base SDK (v0.3.0+): lowercase input like `'en-us'` still works, but `useCurrentLocale()` and `detectPreferredLocale()` always return the canonical form (`'en-US'`) — compare against that, or normalize your own values with the re-exported `canonicalizeLocale()`.
85
+
86
+ ### SSR token strategy
87
+
88
+ `ssrTokenStrategy` (default `'client'`) controls when missing tokens are sent during server rendering:
89
+
90
+ - `'client'` — tokens collected on the server are flushed from the client after hydration. Best for performance.
91
+ - `'server'` — tokens are sent immediately during SSR. Best for reliability and immediate registration.
92
+ - `'auto'` — small batches (≤5) sent from server, larger queued for client.
93
+
94
+ ## Using translations
95
+
96
+ ### `useT()` — the everyday API
97
+
98
+ `useT()` returns the current translation function as a reactive ref, updating the component whenever translations or the loaded locale change.
99
+
100
+ ```vue
101
+ <script setup lang="ts">
102
+ import { useT } from 'langsys-js-vue';
103
+
104
+ const t = useT();
105
+ </script>
106
+
107
+ <template>
108
+ <h1>{{ t('Welcome to my app', 'UI') }}</h1>
109
+ <p>{{ t('Hello, {name}!', 'UI', { name: 'Sarah' }) }}</p>
110
+ </template>
111
+ ```
112
+
113
+ In templates the ref auto-unwraps, so `t(...)` calls the function directly. In script code, call `t.value(...)`.
114
+
115
+ The translation function signature is **`t(phrase, category?, params?)`**:
116
+
117
+ ```typescript
118
+ t('Save'); // no category, no params
119
+ t('Save', 'UI'); // categorized
120
+ t('Hello, {name}!', { name: 'X' }); // no category, with params
121
+ t('Hello, {name}!', 'Greetings', { name: 'X' }); // category + params
122
+ ```
123
+
124
+ The **phrase itself is the lookup key** *and* the base-language default — there's no separate keys file to maintain. The first render of a phrase registers it in the Translation Manager (when using a write key); from then on, translations are fetched and rendered automatically as locales change.
125
+
126
+ #### Interpolation
127
+
128
+ Curly-brace placeholders are substituted from the params argument:
129
+
130
+ ```typescript
131
+ t('You have {count} new messages', 'Notifications', { count: 3 });
132
+ ```
133
+
134
+ Placeholder names are extracted from the phrase at compile time and **type-checked**: omitting a required key or adding an extra one is a TypeScript error.
135
+
136
+ ```typescript
137
+ t('You have {count} new messages', 'Notifications', {});
138
+ // ❌ Property 'count' is missing in type '{}'
139
+
140
+ t('You have {count} new messages', 'Notifications', { count: 3, extra: 'x' });
141
+ // ❌ Object literal may only specify known properties, and 'extra' does not exist
142
+ ```
143
+
144
+ Allowed value types: `string | number | Date | boolean`. Since base SDK 0.3.0, values are locale-formatted: numbers go through `Intl.NumberFormat` (`1234.5` → `1.234,5` in `de-DE`) and `Date` values through `Intl.DateTimeFormat` with the medium date style. Pass a string to opt out of formatting. Formatting always uses the catalog locale (falling back to `en`), never the host's default locale, so server and client render identically.
145
+
146
+ > Future versions will swap the simple `{name}` runtime for full ICU MessageFormat — adding plural / select — without changing the public signature. Style-less ICU arguments (`{n, number}`, `{d, date}`, `{t, time}`) already format as of base SDK 0.3.0. Today's `t('{count} items', 'Cart', { count })` will evolve to `t('{count, plural, one {# item} other {# items}}', 'Cart', { count })`.
147
+
148
+ #### Categorization disambiguates context
149
+
150
+ Different categories give the *same* phrase different translations:
151
+
152
+ ```vue
153
+ <strong>{{ t('Home', 'Main Menu') }}</strong> <!-- "Inicio" in Spanish -->
154
+ <strong>{{ t('Home', 'Home repairs') }}</strong> <!-- "Hogar" in Spanish -->
155
+ ```
156
+
157
+ Without categorization, "Home" would only have one translation — which can't work for both contexts. Langsys's philosophy is *translate once, use everywhere*; categorize when the same phrase legitimately means different things.
158
+
159
+ A good rule for category names: the module or feature the phrase lives in (`Account`, `Errors`, `Checkout`, `UI`).
160
+
161
+ ### `<Translate>` — HTML content blocks
162
+
163
+ For larger blocks of HTML where the structure should be preserved for the translator:
164
+
165
+ ```vue
166
+ <script setup lang="ts">
167
+ import { Translate } from 'langsys-js-vue';
168
+ </script>
169
+
170
+ <template>
171
+ <Translate category="Blog" tag="article">
172
+ <h1 class="title">My article title</h1>
173
+ <p>My content <strong>is the best</strong> when internationalized by Langsys.</p>
174
+ <p>Translators see this exactly as users do — same styling, same structure.</p>
175
+ </Translate>
176
+ </template>
177
+ ```
178
+
179
+ The component:
180
+ - Recursively tokenizes text nodes and translatable attributes (`placeholder`, `alt`, `title`, `aria-label`, plus button/input `value` attributes and `<option>` text).
181
+ - Captures semantic CSS so translators see the styled appearance in the Translation Manager.
182
+ - Registers the whole thing as a **content block** that translators handle as one unit while still translating the individual phrases inside.
183
+ - Auto re-translates on locale change.
184
+
185
+ `<Translate>` mounts the SDK's DOM walker on its host element and lets it mutate the rendered output in place, so **keep its children static** — prose, marketing copy, CMS-rendered articles, forms with placeholders. For dynamic per-string values that Vue owns, use `useT()`.
186
+
187
+ ```vue
188
+ <!-- CMS content goes through Translate as-is -->
189
+ <Translate category="News" tag="div">
190
+ <div v-html="article?.content ?? ''" />
191
+ </Translate>
192
+ ```
193
+
194
+ #### Runtime values with `params` — write placeholders as `%name%`
195
+
196
+ `<Translate>` accepts a `params` prop for runtime interpolation across its content — text nodes, translatable attributes, and `<select>` options. In markup, **write placeholders with percent delimiters (`%name%`), not `{name}`**:
197
+
198
+ ```vue
199
+ <Translate category="Dashboard" tag="section" :params="{ name: user.name, count: unread }">
200
+ <p>Welcome back, %name%. You have %count% new messages.</p>
201
+ </Translate>
202
+ ```
203
+
204
+ Why `%name%`: the base SDK normalizes `%name%` back to canonical `{name}` at capture time, so **translators still only ever see `{name}`** and both spellings register the same content block. A single `{name}` actually works in Vue markup (Vue only consumes `{{ }}`, not single braces) — but `%name%` is the portable form the React/Svelte bindings require too, and it avoids the `{{ }}` collision entirely. Only identifiers between the percents match (`%[A-Za-z_][A-Za-z0-9_]*%`), so literal `%` in prose ("50% off", "width: 100%") is left untouched. To keep a *literal* `%WORD%` (e.g. a Windows env var like `%PATH%` in docs text), wrap it in `<DontTranslate>`. The `params` prop is reactive — a changed `count` re-renders via the base SDK's `setParams()`. Placeholders inside `$t()` stay single-brace `{name}` (they live in a JS string, no collision).
205
+
206
+ `<Translate>` props: `category?`, `custom_id?`, `label?`, `tag?` (defaults to `translate`), `params?`. `class` and other attributes fall through to the host element.
207
+
208
+ ### `<Phrase>` — markup-bearing phrases (pluralization)
209
+
210
+ Keeps a run that contains inline markup as **one** translatable phrase — so a count variable stays next to the noun it pluralizes, and the translator sees the whole sentence:
211
+
212
+ ```vue
213
+ <script setup lang="ts">
214
+ import { Phrase } from 'langsys-js-vue';
215
+ </script>
216
+
217
+ <template>
218
+ <Phrase category="ProductCard" :params="{ n: reviewCount }">
219
+ Based on {n} <strong>reviews</strong>
220
+ </Phrase>
221
+ </template>
222
+ ```
223
+
224
+ The inline elements never reach the translator — they're replaced with neutral markup tokens (`{m0o}`…`{m0c}`) and the real framework-owned elements are reconstituted around the translated text at render. This is also what lets reordering languages move emphasis correctly (`<span>White</span> House` → `Casa <span>Blanca</span>`). Pass interpolation values via `params`; keep the markup children static.
225
+
226
+ > Write the placeholder as `%n%` (the base SDK normalizes it to `{n}` at capture). A single `{n}` also passes through in Vue templates since Vue only consumes `{{ }}` — but `%n%` is the portable form shared with the React/Svelte bindings.
227
+
228
+ `<Phrase>` props: `category?`, `params?`, `tag?` (defaults to `span`). `class` falls through to the host.
229
+
230
+ ### `<DontTranslate>` — never-translated regions
231
+
232
+ Marks content that must be preserved verbatim (brand names, domains, code):
233
+
234
+ ```vue
235
+ Built with <DontTranslate>Kangen®</DontTranslate> on <DontTranslate>langsys.dev</DontTranslate>
236
+ ```
237
+
238
+ Renders the host with `translate="no"`, which the base SDK's tokenizer and renderer already honor — the content is never tokenized, registered, or replaced.
239
+
240
+ `<DontTranslate>` props: `tag?` (defaults to `span`). `class` falls through to the host.
241
+
242
+ ## Composables & reactive primitives
243
+
244
+ | Export | Type | Notes |
245
+ |---|---|---|
246
+ | `useT()` | `() => Readonly<ShallowRef<TFunction>>` | Updates on translations/locale change. Template: `{{ t('Phrase', 'Cat', params?) }}`; script: `t.value(...)`. |
247
+ | `useCurrentLocale()` | `() => Readonly<ShallowRef<string>>` | The locale whose translations are currently loaded (lags the user-selected locale until the fetch completes). |
248
+ | `useTranslations()` | `() => Readonly<ShallowRef<iCategories>>` | Raw translation catalog. Rarely needed in app code. |
249
+ | `useLocaleStore(initial?)` | `() => { locale, setLocale, store }` | Creates a user-locale `Signal<string>`, reads it reactively, returns a setter. Pass `store` to `init`. |
250
+ | `useSignal(signal)` | `<T>(s: Signal<T>) => Readonly<ShallowRef<T>>` | Low-level: subscribe the current scope to any base-SDK signal. |
251
+ | `createLocaleStore(initial?)` | `(s?: string) => Signal<string>` | Make a user-locale store outside components (module scope). |
252
+ | `refToLocaleSource(ref)` | `(r: Ref<string>) => Signal<string>` | Adapt an existing Vue ref (Pinia, `useState`) into the SDK's locale-store contract. |
253
+ | `t` / `currentlyLoadedLocale` / `sTranslations` | `Signal<…>` | Raw signals for direct subscription outside Vue. In components, prefer the composables. |
254
+ | `canonicalizeLocale(locale)` | `(s: string) => string` | Normalize a locale identifier to canonical BCP 47 (`'en-us'` → `'en-US'`) — the same normalization the SDK applies internally. |
255
+
256
+ ## Server-Side Rendering (Nuxt)
257
+
258
+ The SDK is SSR-compatible. The main pattern is to pre-fetch translations server-side and seed them through `initialTranslations` / `initialTranslationsLocale` so the client doesn't refetch on hydration. `useSignal` seeds its ref synchronously from the signal's current value, so components hydrate without a flash of untranslated content when seeded.
259
+
260
+ 📖 **See [README-SSR.md](./README-SSR.md)** for a complete Nuxt walkthrough.
261
+
262
+ ## Utilities
263
+
264
+ `LangsysApp` exposes localized helpers (call them from lifecycle hooks / event handlers):
265
+
266
+ ```typescript
267
+ import { LangsysApp, type iCountryList, type iCurrencyList, type iLocaleDefault } from 'langsys-js-vue';
268
+
269
+ const countries: iCountryList = await LangsysApp.getCountries(); // [{ code: "US", label: "United States" }, ...]
270
+ const dialCodes = await LangsysApp.getDialCodes(); // [{ country_code: "US", dial_code: "+1", name: "United States" }, ...]
271
+ const currencies: iCurrencyList = await LangsysApp.getCurrencies(); // [{ code: "USD", name: "US Dollar", symbol: "$", ... }, ...]
272
+ const locales: iLocaleDefault = await LangsysApp.getLocales(); // { "English": [{ code: "en-US", name: "English (US)" }, ...], ... }
273
+ const localeName = await LangsysApp.getLocaleNameWithLookup('es-ES', true, 'fr-FR'); // "espagnol"
274
+ ```
275
+
276
+ ### Detecting the user's preferred locale
277
+
278
+ ```typescript
279
+ // Browser: navigator.languages → fallback to navigator.language
280
+ const locale = LangsysApp.detectPreferredLocale();
281
+ // Returns 'en-US', 'fr', etc., or false if not detected
282
+
283
+ // SSR (server route / middleware): parses Accept-Language
284
+ const locale = LangsysApp.detectPreferredLocale(event.node.req.headers['accept-language']);
285
+
286
+ // Matched against your app's supported locales
287
+ const supportedLocales = (await LangsysApp.getLocalesFlat()).map((l) => l.code);
288
+ const locale = LangsysApp.detectPreferredLocale(acceptLanguage, supportedLocales);
289
+ ```
290
+
291
+ The matcher tries exact match first (e.g. `en-US`), then language-only (`en` matches `en-GB`), and is script-aware via CLDR likely-subtags (base SDK 0.3.0+): `zh-TW` matches `zh-Hant` and never falls back to `zh-Hans`. Results are always canonical BCP 47; it returns `false` if no match.
292
+
293
+ ### Waiting for translations to load
294
+
295
+ When changing locale mid-session, you may want to re-run dependent code after the new translations arrive:
296
+
297
+ ```typescript
298
+ watch(locale, () => {
299
+ LangsysApp.translationsLoadingPromise.then(() => {
300
+ // re-render content / regenerate UI here
301
+ });
302
+ });
303
+ ```
304
+
305
+ ## License
306
+
307
+ MIT © Langsys