@magic-translate/vue 0.0.0-stage → 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 - PRESENT i42 GmbH
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.md CHANGED
@@ -1,3 +1,190 @@
1
- # Temporary Holding Version
1
+ # @magic-translate/vue
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![NPM](https://img.shields.io/npm/v/@magic-translate/vue.svg)](https://www.npmjs.com/package/@magic-translate/vue)
4
+ [![npm](https://img.shields.io/npm/dm/@magic-translate/vue.svg)](https://www.npmjs.com/package/@magic-translate/vue)
5
+
6
+ Vue client for [Magic Translate](https://magictranslate.io). Write your
7
+ templates in one language and Magic Translate translates them on the fly.
8
+
9
+ Requires Vue 3.3 or newer. Works with Nuxt too.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install --save @magic-translate/vue
15
+ ```
16
+
17
+ Or with yarn:
18
+
19
+ ```bash
20
+ yarn add @magic-translate/vue
21
+ ```
22
+
23
+ ## Setup
24
+
25
+ Install the plugin in your app. You can get an API key for free at
26
+ [magictranslate.io](https://magictranslate.io).
27
+
28
+ ```ts
29
+ // main.ts
30
+ import { createApp } from 'vue';
31
+ import { createMagicTranslate } from '@magic-translate/vue';
32
+ import App from './App.vue';
33
+
34
+ createApp(App)
35
+ .use(
36
+ createMagicTranslate({
37
+ apiKey: 'YOUR_API_KEY',
38
+ language: 'de',
39
+ })
40
+ )
41
+ .mount('#app');
42
+ ```
43
+
44
+ ## Usage
45
+
46
+ ### `<T>` component
47
+
48
+ Wrap any text, including plain HTML, in `<T>`. The original content is shown
49
+ until its translation has loaded.
50
+
51
+ ```vue
52
+ <script setup lang="ts">
53
+ import { ref } from 'vue';
54
+
55
+ const name = ref('Anna');
56
+ </script>
57
+
58
+ <template>
59
+ <h1><T>Welcome to our shop</T></h1>
60
+ <p>
61
+ <T>Click <a href="/sale">here</a> to see <b>today's</b> deals.</T>
62
+ </p>
63
+ <p>
64
+ <T>Hello {{ name }}</T>
65
+ </p>
66
+ </template>
67
+ ```
68
+
69
+ The plugin registers `<T>` globally. You can also import it explicitly:
70
+ `import { T } from '@magic-translate/vue'`.
71
+
72
+ The content is re-translated whenever it changes, for example when `name`
73
+ changes in the example above.
74
+
75
+ All translations requested at the same time are sent to the API in a single
76
+ request, and translations are cached, so you can use as many `<T>` components
77
+ as you like.
78
+
79
+ Only text and plain HTML elements are allowed inside `<T>`. Other components
80
+ are not, because they cannot survive the translation. Scoped styles keep
81
+ working on the translated markup.
82
+
83
+ ### `useTranslate`
84
+
85
+ Use `useTranslate` for plain strings, such as attributes, or anywhere a
86
+ component does not fit:
87
+
88
+ ```vue
89
+ <script setup lang="ts">
90
+ import { useTranslate } from '@magic-translate/vue';
91
+
92
+ const t = useTranslate();
93
+ </script>
94
+
95
+ <template>
96
+ <input :placeholder="t('Search products')" />
97
+ </template>
98
+ ```
99
+
100
+ `t` returns the original text until the translation has loaded, and the
101
+ component re-renders once it has.
102
+
103
+ ### Options per translation
104
+
105
+ Both the component and `t` can override the target language and the formality:
106
+
107
+ ```vue
108
+ <T lang="fr" formality="formal">How are you?</T>
109
+
110
+ <input :placeholder="t('Your name', { lang: 'fr', formality: 'formal' })" />
111
+ ```
112
+
113
+ ### Switching languages
114
+
115
+ `useLanguageSwitcher` reads and changes the current language. Every `<T>`
116
+ component and `t` call updates automatically.
117
+
118
+ ```vue
119
+ <script setup lang="ts">
120
+ import { useLanguageSwitcher } from '@magic-translate/vue';
121
+
122
+ const { currentLanguage, setLanguage } = useLanguageSwitcher();
123
+ </script>
124
+
125
+ <template>
126
+ <select
127
+ :value="currentLanguage"
128
+ @change="setLanguage(($event.target as HTMLSelectElement).value as any)"
129
+ >
130
+ <option value="en">English</option>
131
+ <option value="de">Deutsch</option>
132
+ <option value="fr">Français</option>
133
+ </select>
134
+ </template>
135
+ ```
136
+
137
+ `currentLanguage` is a read-only ref, so you can also use it in `computed` and
138
+ `watch`.
139
+
140
+ ### Translating in code
141
+
142
+ To translate a string in your script, call `translate`:
143
+
144
+ ```ts
145
+ import { useMagicTranslate } from '@magic-translate/vue';
146
+
147
+ const magicTranslate = useMagicTranslate();
148
+ const message = await magicTranslate.translate('Your order has been placed');
149
+ ```
150
+
151
+ Outside of components, for example in a store, use the object returned by
152
+ `createMagicTranslate` directly. It has the same `translate`, `language` and
153
+ `setLanguage`.
154
+
155
+ ## Configuration
156
+
157
+ `createMagicTranslate` accepts:
158
+
159
+ | Option | Description |
160
+ | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
161
+ | `apiKey` | Required. Your Magic Translate API key. |
162
+ | `language` | Required. The language content is translated to, e.g. `'de'`. |
163
+ | `sourceLanguage` | The language your templates are written in. Detected automatically by default; only set it if automatic detection gives wrong results. |
164
+ | `formality` | `'formal'` or `'informal'`. Not supported for all languages. Can be overridden per translation. |
165
+ | `debug` | Logs requests to the console. |
166
+
167
+ ## Nuxt
168
+
169
+ Install the plugin in a Nuxt plugin:
170
+
171
+ ```ts
172
+ // plugins/magic-translate.ts
173
+ import { createMagicTranslate } from '@magic-translate/vue';
174
+
175
+ export default defineNuxtPlugin((nuxtApp) => {
176
+ nuxtApp.vueApp.use(
177
+ createMagicTranslate({
178
+ apiKey: useRuntimeConfig().public.magicTranslateApiKey,
179
+ language: 'de',
180
+ })
181
+ );
182
+ });
183
+ ```
184
+
185
+ Translations are only loaded in the browser. With server-side rendering the
186
+ server renders the original content, and it is translated after hydration.
187
+
188
+ ## License
189
+
190
+ MIT © [i42 GmbH](https://i42.ch)
package/dist/index.cjs ADDED
@@ -0,0 +1,2 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("@magic-translate/core"),t=require("vue");function n(e){let t=e.cloneNode(!0),n=[],r,i=document.createTreeWalker(t,NodeFilter.SHOW_ELEMENT|NodeFilter.SHOW_COMMENT);for(let e=i.nextNode();e;e=i.nextNode()){if(e.nodeType===Node.COMMENT_NODE){n.push(e);continue}let t=e;for(let{name:e}of Array.from(t.attributes))e.startsWith(`data-v-`)&&(r=e,t.removeAttribute(e))}n.forEach(e=>e.parentNode?.removeChild(e));let[,a,o,s]=/^(\s*)([\s\S]*?)(\s*)$/.exec(t.innerHTML);return{html:o,leadingWhitespace:a,trailingWhitespace:s,scopeAttribute:r}}function r(e){if(Array.isArray(e))for(let n of e){if(!(0,t.isVNode)(n)){r(n);continue}let e=n;if(e.type!==t.Text&&e.type!==t.Comment){if(typeof e.type==`object`||typeof e.type==`function`){let t=e.type.name??e.type.__name;throw Error(`Components are not allowed inside \`<T>...</T>\` components.${t?` The component found inside the T component is ${t}.`:``}`)}r(e.children)}}}var i=(0,t.defineComponent)({name:`T`,props:{lang:String,formality:String},setup(e,{slots:i}){let a=c(),o=(0,t.ref)(),s=(0,t.ref)(),l=(0,t.shallowRef)(null),u=(0,t.ref)(!1),d=0,f,p=(e,t)=>{let n=s.value;n&&(n.innerHTML=`${e.leadingWhitespace}${t}${e.trailingWhitespace}`,e.scopeAttribute&&n.querySelectorAll(`*`).forEach(t=>t.setAttribute(e.scopeAttribute,``)),u.value=!0)};return(0,t.onMounted)(()=>{let e=o.value,t=()=>{let t=n(e),r=l.value;(t.html!==r?.html||t.leadingWhitespace!==r.leadingWhitespace||t.trailingWhitespace!==r.trailingWhitespace)&&(l.value=t)};t(),f=new MutationObserver(t),f.observe(e,{subtree:!0,childList:!0,characterData:!0,attributes:!0})}),(0,t.onBeforeUnmount)(()=>f?.disconnect()),(0,t.watch)([l,()=>e.lang,()=>e.formality,a.language],([e,t,n])=>{if(!e?.html){u.value=!1;return}let r=++d;a.translate(e.html,{lang:t,formality:n}).then(t=>{r===d&&p(e,t)})}),()=>{let e=i.default?.();return r(e),(0,t.h)(`span`,{style:`display: contents`},[(0,t.h)(`span`,{ref:o,style:{display:u.value?`none`:`contents`}},e),(0,t.h)(`span`,{ref:s,style:{display:u.value?`contents`:`none`}})])}}}),a=i,o=Symbol(`magic-translate`);function s(n){if(!n.apiKey)throw Error(`Missing API key. Please get one at ${e.libraryWebsite}`);let{debug:r,apiKey:a,sourceLanguage:s,_apiUrl:c,formality:l}=n,u=n._loaderFactory??e.defaultLoaderFactory,d=(0,t.ref)(n.language),f=(0,t.computed)(()=>new e.DataLoader(u({debug:!!r,apiKey:a,lang:d.value,sourceLang:s,apiUrl:c,formality:l}),{cacheKeyFn:(0,e.cacheKeyFn)(a,s,d.value)})),p={language:(0,t.readonly)(d),apiKey:a,sourceLanguage:s,dataLoader:f,setLanguage(e){d.value=e},translate(e,t={}){return f.value.load({text:e,...t})},cacheKey(t,n={}){return(0,e.cacheKeyFn)(a,s,d.value)({text:t,...n})},install(e){e.provide(o,p),e.component(`T`,i)}};return p}function c(){let e=(0,t.inject)(o,null);if(e===null)throw Error(`useMagicTranslate must be used in an app that installed createMagicTranslate()`);return e}function l(){let{setLanguage:e,language:t}=c();return{setLanguage:e,currentLanguage:t}}function u(){let e=c(),n=(0,t.reactive)(new Map),r=new Set;return(t,i={})=>{if(!t||typeof window>`u`)return t;let a=e.cacheKey(t,i),o=n.get(a);return o===void 0&&!r.has(a)&&(r.add(a),e.translate(t,i).then(e=>{n.set(a,e),r.delete(a)})),o??t}}var d=u;exports.MAGIC_TRANSLATE=o,exports.T=a,exports.Translate=i,exports.createMagicTranslate=s,exports.useLanguageSwitcher=l,exports.useMagicTranslate=c,exports.useT=d,exports.useTranslate=u,Object.keys(e).forEach(function(t){t!=="default"&&!Object.prototype.hasOwnProperty.call(exports,t)&&Object.defineProperty(exports,t,{enumerable:!0,get:function(){return e[t]}})});
2
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","names":["isVNode","Text","Comment","defineComponent","ref","shallowRef","h","libraryWebsite","defaultLoaderFactory","ref","computed","DataLoader","cacheKeyFn","readonly","inject","reactive","_magic_translate_core","vue","readSource","clone","element","comments","scopeAttribute","walker","node","el","name","comment","leadingWhitespace","html","trailingWhitespace","assertNoComponents","children","child","vnode","Translate","magicTranslate","useMagicTranslate","sourceRef","targetRef","source","translated","lastRequestId","observer","render","target","translation","read","next","current","props","lang","formality","requestId","slots","T","MAGIC_TRANSLATE","createMagicTranslate","config","loaderFactory","language","dataLoader","debug","sourceLanguage","_apiUrl","apiKey","params","app","useLanguageSwitcher","useTranslate","translations","pending","text","key","useT","k"],"sources":["../src/translate.ts","../src/magic-translate.ts","../src/use-translate.ts"],"sourcesContent":["import {\n Comment,\n defineComponent,\n h,\n isVNode,\n onBeforeUnmount,\n onMounted,\n PropType,\n ref,\n shallowRef,\n Text,\n VNode,\n watch,\n} from 'vue';\nimport { TranslateOptions } from '@magic-translate/core';\nimport { Languages, useMagicTranslate } from './magic-translate';\n\ninterface Source {\n html: string;\n leadingWhitespace: string;\n trailingWhitespace: string;\n // The `data-v-*` attribute of the parent's scoped styles, so they still\n // apply to the translated markup.\n scopeAttribute?: string;\n}\n\n/**\n * Turns the slot content into the HTML string that gets translated, without\n * the comments and attributes Vue adds to the DOM.\n */\nfunction readSource(element: HTMLElement): Source {\n const clone = element.cloneNode(true) as HTMLElement;\n const comments: Node[] = [];\n let scopeAttribute: string | undefined;\n\n const walker = document.createTreeWalker(\n clone,\n NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT\n );\n for (let node = walker.nextNode(); node; node = walker.nextNode()) {\n if (node.nodeType === Node.COMMENT_NODE) {\n comments.push(node);\n continue;\n }\n\n const el = node as Element;\n for (const { name } of Array.from(el.attributes)) {\n if (name.startsWith('data-v-')) {\n scopeAttribute = name;\n el.removeAttribute(name);\n }\n }\n }\n comments.forEach((comment) => comment.parentNode?.removeChild(comment));\n\n const [, leadingWhitespace, html, trailingWhitespace] =\n /^(\\s*)([\\s\\S]*?)(\\s*)$/.exec(clone.innerHTML)!;\n\n return { html, leadingWhitespace, trailingWhitespace, scopeAttribute };\n}\n\nfunction assertNoComponents(children: unknown) {\n if (!Array.isArray(children)) {\n return;\n }\n\n for (const child of children) {\n if (!isVNode(child)) {\n assertNoComponents(child);\n continue;\n }\n\n const vnode = child as VNode;\n if (vnode.type === Text || vnode.type === Comment) {\n continue;\n }\n if (typeof vnode.type === 'object' || typeof vnode.type === 'function') {\n const name =\n (vnode.type as { name?: string; __name?: string }).name ??\n (vnode.type as { __name?: string }).__name;\n throw new Error(\n `Components are not allowed inside \\`<T>...</T>\\` components.${\n name ? ` The component found inside the T component is ${name}.` : ''\n }`\n );\n }\n // elements and fragments\n assertNoComponents(vnode.children);\n }\n}\n\n/**\n * Translates its content, which can be text and plain HTML elements. The\n * original content is shown until the translation has loaded.\n */\nexport const Translate = defineComponent({\n name: 'T',\n props: {\n lang: String as PropType<Languages>,\n formality: String as PropType<TranslateOptions['formality']>,\n },\n setup(props, { slots }) {\n const magicTranslate = useMagicTranslate();\n const sourceRef = ref<HTMLElement>();\n const targetRef = ref<HTMLElement>();\n const source = shallowRef<Source | null>(null);\n const translated = ref(false);\n let lastRequestId = 0;\n let observer: MutationObserver | undefined;\n\n const render = (source: Source, translation: string) => {\n const target = targetRef.value;\n if (!target) {\n return;\n }\n\n target.innerHTML = `${source.leadingWhitespace}${translation}${source.trailingWhitespace}`;\n if (source.scopeAttribute) {\n target\n .querySelectorAll('*')\n .forEach((el) => el.setAttribute(source.scopeAttribute!, ''));\n }\n translated.value = true;\n };\n\n onMounted(() => {\n const element = sourceRef.value!;\n const read = () => {\n const next = readSource(element);\n const current = source.value;\n if (\n next.html !== current?.html ||\n next.leadingWhitespace !== current.leadingWhitespace ||\n next.trailingWhitespace !== current.trailingWhitespace\n ) {\n source.value = next;\n }\n };\n read();\n\n // Picks up changes to the slot content, such as interpolations.\n observer = new MutationObserver(read);\n observer.observe(element, {\n subtree: true,\n childList: true,\n characterData: true,\n attributes: true,\n });\n });\n\n onBeforeUnmount(() => observer?.disconnect());\n\n watch(\n [\n source,\n () => props.lang,\n () => props.formality,\n // a language switch has to re-translate the content\n magicTranslate.language,\n ],\n ([source, lang, formality]) => {\n if (!source?.html) {\n translated.value = false;\n return;\n }\n\n // make sure that we only ever use the response of the most recent request.\n const requestId = ++lastRequestId;\n magicTranslate\n .translate(source.html, { lang, formality })\n .then((translation) => {\n if (requestId === lastRequestId) {\n render(source, translation);\n }\n });\n }\n );\n\n return () => {\n const children = slots.default?.();\n assertNoComponents(children);\n\n return h('span', { style: 'display: contents' }, [\n h(\n 'span',\n {\n ref: sourceRef,\n style: { display: translated.value ? 'none' : 'contents' },\n },\n children\n ),\n h('span', {\n ref: targetRef,\n style: { display: translated.value ? 'contents' : 'none' },\n }),\n ]);\n };\n },\n});\n\nexport const T = Translate;\n\n// The plugin registers <T> globally, so templates type-check without importing it.\ndeclare module 'vue' {\n export interface GlobalComponents {\n T: typeof Translate;\n }\n}\n","import {\n App,\n computed,\n ComputedRef,\n inject,\n InjectionKey,\n readonly,\n ref,\n Ref,\n} from 'vue';\nimport {\n cacheKeyFn,\n DataLoader,\n DataLoaderLoadParams,\n defaultLoaderFactory,\n Language,\n libraryWebsite,\n MagicTranslateConfig,\n} from '@magic-translate/core';\nimport { Translate } from './translate';\n\n/**\n * Type alias to make the language enum more flexible to also allow passing language strings\n * instead of just the enum.\n */\nexport type Languages = `${Language}`;\n\nexport type TranslateParams = Omit<DataLoaderLoadParams, 'text'>;\n\nexport interface MagicTranslate {\n /**\n * The language content is currently translated to.\n */\n readonly language: Readonly<Ref<Languages>>;\n readonly apiKey: string;\n readonly sourceLanguage?: Languages;\n /** @ignore */\n readonly dataLoader: ComputedRef<\n DataLoader<DataLoaderLoadParams, string, string>\n >;\n setLanguage(language: Languages): void;\n translate(text: string, params?: TranslateParams): Promise<string>;\n /** @ignore */\n cacheKey(text: string, params?: TranslateParams): string;\n install(app: App): void;\n}\n\nexport const MAGIC_TRANSLATE: InjectionKey<MagicTranslate> =\n Symbol('magic-translate');\n\n/**\n * Sets up Magic Translate. Install the result as a plugin with `app.use()`.\n * It can also be used directly, outside of components.\n */\nexport function createMagicTranslate(\n config: MagicTranslateConfig\n): MagicTranslate {\n if (!config.apiKey) {\n throw new Error(`Missing API key. Please get one at ${libraryWebsite}`);\n }\n\n const { debug, apiKey, sourceLanguage, _apiUrl, formality } = config;\n const loaderFactory = config._loaderFactory ?? defaultLoaderFactory;\n const language = ref<Languages>(config.language);\n\n const dataLoader = computed(\n () =>\n new DataLoader<DataLoaderLoadParams, string, string>(\n loaderFactory({\n debug: !!debug,\n apiKey,\n lang: language.value,\n sourceLang: sourceLanguage,\n apiUrl: _apiUrl,\n formality,\n }),\n { cacheKeyFn: cacheKeyFn(apiKey, sourceLanguage, language.value) }\n )\n );\n\n const magicTranslate: MagicTranslate = {\n language: readonly(language),\n apiKey,\n sourceLanguage,\n dataLoader,\n setLanguage(lang) {\n language.value = lang;\n },\n translate(text, params = {}) {\n return dataLoader.value.load({ text, ...params });\n },\n cacheKey(text, params = {}) {\n return cacheKeyFn(\n apiKey,\n sourceLanguage,\n language.value\n )({\n text,\n ...params,\n });\n },\n install(app) {\n app.provide(MAGIC_TRANSLATE, magicTranslate);\n app.component('T', Translate);\n },\n };\n\n return magicTranslate;\n}\n\nexport function useMagicTranslate(): MagicTranslate {\n const magicTranslate = inject(MAGIC_TRANSLATE, null);\n\n if (magicTranslate === null) {\n throw new Error(\n 'useMagicTranslate must be used in an app that installed createMagicTranslate()'\n );\n }\n\n return magicTranslate;\n}\n\nexport function useLanguageSwitcher(): {\n setLanguage: (language: Languages) => void;\n currentLanguage: Readonly<Ref<Languages>>;\n} {\n const { setLanguage, language } = useMagicTranslate();\n\n return { setLanguage, currentLanguage: language };\n}\n","import { reactive } from 'vue';\nimport { TranslateParams, useMagicTranslate } from './magic-translate';\n\n/**\n * Returns a function that translates plain strings, e.g. for attributes:\n * `<input :placeholder=\"t('Search')\" />`. It returns the original text until\n * the translation has loaded, and the component re-renders once it has.\n */\nexport function useTranslate(): (\n text: string,\n params?: TranslateParams\n) => string {\n const magicTranslate = useMagicTranslate();\n const translations = reactive(new Map<string, string>());\n const pending = new Set<string>();\n\n return (text, params = {}) => {\n // Translations are only loaded in the browser.\n if (!text || typeof window === 'undefined') {\n return text;\n }\n\n // reads the current language, so a language switch re-renders\n const key = magicTranslate.cacheKey(text, params);\n const translation = translations.get(key);\n\n if (translation === undefined && !pending.has(key)) {\n pending.add(key);\n magicTranslate.translate(text, params).then((translated) => {\n translations.set(key, translated);\n pending.delete(key);\n });\n }\n\n return translation ?? text;\n };\n}\n\nexport const useT = useTranslate;\n"],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ export * from '@magic-translate/core';
2
+ export * from './magic-translate';
3
+ export * from './translate';
4
+ export * from './use-translate';
package/dist/index.js ADDED
@@ -0,0 +1,152 @@
1
+ import { DataLoader as e, cacheKeyFn as t, defaultLoaderFactory as n, libraryWebsite as r } from "@magic-translate/core";
2
+ import { Comment as i, Text as a, computed as o, defineComponent as s, h as c, inject as l, isVNode as u, onBeforeUnmount as d, onMounted as f, reactive as p, readonly as m, ref as h, shallowRef as g, watch as _ } from "vue";
3
+ export * from "@magic-translate/core";
4
+ // src/translate.ts
5
+ function v(e) {
6
+ let t = e.cloneNode(!0), n = [], r, i = document.createTreeWalker(t, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT);
7
+ for (let e = i.nextNode(); e; e = i.nextNode()) {
8
+ if (e.nodeType === Node.COMMENT_NODE) {
9
+ n.push(e);
10
+ continue;
11
+ }
12
+ let t = e;
13
+ for (let { name: e } of Array.from(t.attributes)) e.startsWith("data-v-") && (r = e, t.removeAttribute(e));
14
+ }
15
+ n.forEach((e) => e.parentNode?.removeChild(e));
16
+ let [, a, o, s] = /^(\s*)([\s\S]*?)(\s*)$/.exec(t.innerHTML);
17
+ return {
18
+ html: o,
19
+ leadingWhitespace: a,
20
+ trailingWhitespace: s,
21
+ scopeAttribute: r
22
+ };
23
+ }
24
+ function y(e) {
25
+ if (Array.isArray(e)) for (let t of e) {
26
+ if (!u(t)) {
27
+ y(t);
28
+ continue;
29
+ }
30
+ let e = t;
31
+ if (e.type !== a && e.type !== i) {
32
+ if (typeof e.type == "object" || typeof e.type == "function") {
33
+ let t = e.type.name ?? e.type.__name;
34
+ throw Error(`Components are not allowed inside \`<T>...</T>\` components.${t ? ` The component found inside the T component is ${t}.` : ""}`);
35
+ }
36
+ y(e.children);
37
+ }
38
+ }
39
+ }
40
+ var b = s({
41
+ name: "T",
42
+ props: {
43
+ lang: String,
44
+ formality: String
45
+ },
46
+ setup(e, { slots: t }) {
47
+ let n = w(), r = h(), i = h(), a = g(null), o = h(!1), s = 0, l, u = (e, t) => {
48
+ let n = i.value;
49
+ n && (n.innerHTML = `${e.leadingWhitespace}${t}${e.trailingWhitespace}`, e.scopeAttribute && n.querySelectorAll("*").forEach((t) => t.setAttribute(e.scopeAttribute, "")), o.value = !0);
50
+ };
51
+ return f(() => {
52
+ let e = r.value, t = () => {
53
+ let t = v(e), n = a.value;
54
+ (t.html !== n?.html || t.leadingWhitespace !== n.leadingWhitespace || t.trailingWhitespace !== n.trailingWhitespace) && (a.value = t);
55
+ };
56
+ t(), l = new MutationObserver(t), l.observe(e, {
57
+ subtree: !0,
58
+ childList: !0,
59
+ characterData: !0,
60
+ attributes: !0
61
+ });
62
+ }), d(() => l?.disconnect()), _([
63
+ a,
64
+ () => e.lang,
65
+ () => e.formality,
66
+ n.language
67
+ ], ([e, t, r]) => {
68
+ if (!e?.html) {
69
+ o.value = !1;
70
+ return;
71
+ }
72
+ let i = ++s;
73
+ n.translate(e.html, {
74
+ lang: t,
75
+ formality: r
76
+ }).then((t) => {
77
+ i === s && u(e, t);
78
+ });
79
+ }), () => {
80
+ let e = t.default?.();
81
+ return y(e), c("span", { style: "display: contents" }, [c("span", {
82
+ ref: r,
83
+ style: { display: o.value ? "none" : "contents" }
84
+ }, e), c("span", {
85
+ ref: i,
86
+ style: { display: o.value ? "contents" : "none" }
87
+ })]);
88
+ };
89
+ }
90
+ }), x = b, S = Symbol("magic-translate");
91
+ function C(i) {
92
+ if (!i.apiKey) throw Error(`Missing API key. Please get one at ${r}`);
93
+ let { debug: a, apiKey: s, sourceLanguage: c, _apiUrl: l, formality: u } = i, d = i._loaderFactory ?? n, f = h(i.language), p = o(() => new e(d({
94
+ debug: !!a,
95
+ apiKey: s,
96
+ lang: f.value,
97
+ sourceLang: c,
98
+ apiUrl: l,
99
+ formality: u
100
+ }), { cacheKeyFn: t(s, c, f.value) })), g = {
101
+ language: m(f),
102
+ apiKey: s,
103
+ sourceLanguage: c,
104
+ dataLoader: p,
105
+ setLanguage(e) {
106
+ f.value = e;
107
+ },
108
+ translate(e, t = {}) {
109
+ return p.value.load({
110
+ text: e,
111
+ ...t
112
+ });
113
+ },
114
+ cacheKey(e, n = {}) {
115
+ return t(s, c, f.value)({
116
+ text: e,
117
+ ...n
118
+ });
119
+ },
120
+ install(e) {
121
+ e.provide(S, g), e.component("T", b);
122
+ }
123
+ };
124
+ return g;
125
+ }
126
+ function w() {
127
+ let e = l(S, null);
128
+ if (e === null) throw Error("useMagicTranslate must be used in an app that installed createMagicTranslate()");
129
+ return e;
130
+ }
131
+ function T() {
132
+ let { setLanguage: e, language: t } = w();
133
+ return {
134
+ setLanguage: e,
135
+ currentLanguage: t
136
+ };
137
+ }
138
+ // src/use-translate.ts
139
+ function E() {
140
+ let e = w(), t = p(/* @__PURE__ */ new Map()), n = /* @__PURE__ */ new Set();
141
+ return (r, i = {}) => {
142
+ if (!r || typeof window > "u") return r;
143
+ let a = e.cacheKey(r, i), o = t.get(a);
144
+ return o === void 0 && !n.has(a) && (n.add(a), e.translate(r, i).then((e) => {
145
+ t.set(a, e), n.delete(a);
146
+ })), o ?? r;
147
+ };
148
+ }
149
+ var D = E;
150
+ export { S as MAGIC_TRANSLATE, x as T, b as Translate, C as createMagicTranslate, T as useLanguageSwitcher, w as useMagicTranslate, D as useT, E as useTranslate };
151
+
152
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":["readSource","clone","element","comments","scopeAttribute","walker","node","el","name","comment","leadingWhitespace","html","trailingWhitespace","assertNoComponents","children","child","isVNode","vnode","Text","Comment","Translate","defineComponent","magicTranslate","useMagicTranslate","sourceRef","ref","targetRef","source","shallowRef","translated","lastRequestId","observer","render","target","translation","onMounted","read","next","current","onBeforeUnmount","watch","props","lang","formality","requestId","slots","h","T","MAGIC_TRANSLATE","createMagicTranslate","config","libraryWebsite","loaderFactory","defaultLoaderFactory","language","dataLoader","computed","DataLoader","debug","sourceLanguage","_apiUrl","cacheKeyFn","apiKey","readonly","params","app","inject","useLanguageSwitcher","useTranslate","translations","reactive","pending","text","key","useT"],"sources":["../src/translate.ts","../src/magic-translate.ts","../src/use-translate.ts"],"sourcesContent":["import {\n Comment,\n defineComponent,\n h,\n isVNode,\n onBeforeUnmount,\n onMounted,\n PropType,\n ref,\n shallowRef,\n Text,\n VNode,\n watch,\n} from 'vue';\nimport { TranslateOptions } from '@magic-translate/core';\nimport { Languages, useMagicTranslate } from './magic-translate';\n\ninterface Source {\n html: string;\n leadingWhitespace: string;\n trailingWhitespace: string;\n // The `data-v-*` attribute of the parent's scoped styles, so they still\n // apply to the translated markup.\n scopeAttribute?: string;\n}\n\n/**\n * Turns the slot content into the HTML string that gets translated, without\n * the comments and attributes Vue adds to the DOM.\n */\nfunction readSource(element: HTMLElement): Source {\n const clone = element.cloneNode(true) as HTMLElement;\n const comments: Node[] = [];\n let scopeAttribute: string | undefined;\n\n const walker = document.createTreeWalker(\n clone,\n NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT\n );\n for (let node = walker.nextNode(); node; node = walker.nextNode()) {\n if (node.nodeType === Node.COMMENT_NODE) {\n comments.push(node);\n continue;\n }\n\n const el = node as Element;\n for (const { name } of Array.from(el.attributes)) {\n if (name.startsWith('data-v-')) {\n scopeAttribute = name;\n el.removeAttribute(name);\n }\n }\n }\n comments.forEach((comment) => comment.parentNode?.removeChild(comment));\n\n const [, leadingWhitespace, html, trailingWhitespace] =\n /^(\\s*)([\\s\\S]*?)(\\s*)$/.exec(clone.innerHTML)!;\n\n return { html, leadingWhitespace, trailingWhitespace, scopeAttribute };\n}\n\nfunction assertNoComponents(children: unknown) {\n if (!Array.isArray(children)) {\n return;\n }\n\n for (const child of children) {\n if (!isVNode(child)) {\n assertNoComponents(child);\n continue;\n }\n\n const vnode = child as VNode;\n if (vnode.type === Text || vnode.type === Comment) {\n continue;\n }\n if (typeof vnode.type === 'object' || typeof vnode.type === 'function') {\n const name =\n (vnode.type as { name?: string; __name?: string }).name ??\n (vnode.type as { __name?: string }).__name;\n throw new Error(\n `Components are not allowed inside \\`<T>...</T>\\` components.${\n name ? ` The component found inside the T component is ${name}.` : ''\n }`\n );\n }\n // elements and fragments\n assertNoComponents(vnode.children);\n }\n}\n\n/**\n * Translates its content, which can be text and plain HTML elements. The\n * original content is shown until the translation has loaded.\n */\nexport const Translate = defineComponent({\n name: 'T',\n props: {\n lang: String as PropType<Languages>,\n formality: String as PropType<TranslateOptions['formality']>,\n },\n setup(props, { slots }) {\n const magicTranslate = useMagicTranslate();\n const sourceRef = ref<HTMLElement>();\n const targetRef = ref<HTMLElement>();\n const source = shallowRef<Source | null>(null);\n const translated = ref(false);\n let lastRequestId = 0;\n let observer: MutationObserver | undefined;\n\n const render = (source: Source, translation: string) => {\n const target = targetRef.value;\n if (!target) {\n return;\n }\n\n target.innerHTML = `${source.leadingWhitespace}${translation}${source.trailingWhitespace}`;\n if (source.scopeAttribute) {\n target\n .querySelectorAll('*')\n .forEach((el) => el.setAttribute(source.scopeAttribute!, ''));\n }\n translated.value = true;\n };\n\n onMounted(() => {\n const element = sourceRef.value!;\n const read = () => {\n const next = readSource(element);\n const current = source.value;\n if (\n next.html !== current?.html ||\n next.leadingWhitespace !== current.leadingWhitespace ||\n next.trailingWhitespace !== current.trailingWhitespace\n ) {\n source.value = next;\n }\n };\n read();\n\n // Picks up changes to the slot content, such as interpolations.\n observer = new MutationObserver(read);\n observer.observe(element, {\n subtree: true,\n childList: true,\n characterData: true,\n attributes: true,\n });\n });\n\n onBeforeUnmount(() => observer?.disconnect());\n\n watch(\n [\n source,\n () => props.lang,\n () => props.formality,\n // a language switch has to re-translate the content\n magicTranslate.language,\n ],\n ([source, lang, formality]) => {\n if (!source?.html) {\n translated.value = false;\n return;\n }\n\n // make sure that we only ever use the response of the most recent request.\n const requestId = ++lastRequestId;\n magicTranslate\n .translate(source.html, { lang, formality })\n .then((translation) => {\n if (requestId === lastRequestId) {\n render(source, translation);\n }\n });\n }\n );\n\n return () => {\n const children = slots.default?.();\n assertNoComponents(children);\n\n return h('span', { style: 'display: contents' }, [\n h(\n 'span',\n {\n ref: sourceRef,\n style: { display: translated.value ? 'none' : 'contents' },\n },\n children\n ),\n h('span', {\n ref: targetRef,\n style: { display: translated.value ? 'contents' : 'none' },\n }),\n ]);\n };\n },\n});\n\nexport const T = Translate;\n\n// The plugin registers <T> globally, so templates type-check without importing it.\ndeclare module 'vue' {\n export interface GlobalComponents {\n T: typeof Translate;\n }\n}\n","import {\n App,\n computed,\n ComputedRef,\n inject,\n InjectionKey,\n readonly,\n ref,\n Ref,\n} from 'vue';\nimport {\n cacheKeyFn,\n DataLoader,\n DataLoaderLoadParams,\n defaultLoaderFactory,\n Language,\n libraryWebsite,\n MagicTranslateConfig,\n} from '@magic-translate/core';\nimport { Translate } from './translate';\n\n/**\n * Type alias to make the language enum more flexible to also allow passing language strings\n * instead of just the enum.\n */\nexport type Languages = `${Language}`;\n\nexport type TranslateParams = Omit<DataLoaderLoadParams, 'text'>;\n\nexport interface MagicTranslate {\n /**\n * The language content is currently translated to.\n */\n readonly language: Readonly<Ref<Languages>>;\n readonly apiKey: string;\n readonly sourceLanguage?: Languages;\n /** @ignore */\n readonly dataLoader: ComputedRef<\n DataLoader<DataLoaderLoadParams, string, string>\n >;\n setLanguage(language: Languages): void;\n translate(text: string, params?: TranslateParams): Promise<string>;\n /** @ignore */\n cacheKey(text: string, params?: TranslateParams): string;\n install(app: App): void;\n}\n\nexport const MAGIC_TRANSLATE: InjectionKey<MagicTranslate> =\n Symbol('magic-translate');\n\n/**\n * Sets up Magic Translate. Install the result as a plugin with `app.use()`.\n * It can also be used directly, outside of components.\n */\nexport function createMagicTranslate(\n config: MagicTranslateConfig\n): MagicTranslate {\n if (!config.apiKey) {\n throw new Error(`Missing API key. Please get one at ${libraryWebsite}`);\n }\n\n const { debug, apiKey, sourceLanguage, _apiUrl, formality } = config;\n const loaderFactory = config._loaderFactory ?? defaultLoaderFactory;\n const language = ref<Languages>(config.language);\n\n const dataLoader = computed(\n () =>\n new DataLoader<DataLoaderLoadParams, string, string>(\n loaderFactory({\n debug: !!debug,\n apiKey,\n lang: language.value,\n sourceLang: sourceLanguage,\n apiUrl: _apiUrl,\n formality,\n }),\n { cacheKeyFn: cacheKeyFn(apiKey, sourceLanguage, language.value) }\n )\n );\n\n const magicTranslate: MagicTranslate = {\n language: readonly(language),\n apiKey,\n sourceLanguage,\n dataLoader,\n setLanguage(lang) {\n language.value = lang;\n },\n translate(text, params = {}) {\n return dataLoader.value.load({ text, ...params });\n },\n cacheKey(text, params = {}) {\n return cacheKeyFn(\n apiKey,\n sourceLanguage,\n language.value\n )({\n text,\n ...params,\n });\n },\n install(app) {\n app.provide(MAGIC_TRANSLATE, magicTranslate);\n app.component('T', Translate);\n },\n };\n\n return magicTranslate;\n}\n\nexport function useMagicTranslate(): MagicTranslate {\n const magicTranslate = inject(MAGIC_TRANSLATE, null);\n\n if (magicTranslate === null) {\n throw new Error(\n 'useMagicTranslate must be used in an app that installed createMagicTranslate()'\n );\n }\n\n return magicTranslate;\n}\n\nexport function useLanguageSwitcher(): {\n setLanguage: (language: Languages) => void;\n currentLanguage: Readonly<Ref<Languages>>;\n} {\n const { setLanguage, language } = useMagicTranslate();\n\n return { setLanguage, currentLanguage: language };\n}\n","import { reactive } from 'vue';\nimport { TranslateParams, useMagicTranslate } from './magic-translate';\n\n/**\n * Returns a function that translates plain strings, e.g. for attributes:\n * `<input :placeholder=\"t('Search')\" />`. It returns the original text until\n * the translation has loaded, and the component re-renders once it has.\n */\nexport function useTranslate(): (\n text: string,\n params?: TranslateParams\n) => string {\n const magicTranslate = useMagicTranslate();\n const translations = reactive(new Map<string, string>());\n const pending = new Set<string>();\n\n return (text, params = {}) => {\n // Translations are only loaded in the browser.\n if (!text || typeof window === 'undefined') {\n return text;\n }\n\n // reads the current language, so a language switch re-renders\n const key = magicTranslate.cacheKey(text, params);\n const translation = translations.get(key);\n\n if (translation === undefined && !pending.has(key)) {\n pending.add(key);\n magicTranslate.translate(text, params).then((translated) => {\n translations.set(key, translated);\n pending.delete(key);\n });\n }\n\n return translation ?? text;\n };\n}\n\nexport const useT = useTranslate;\n"],"mappings":";;;;AA8BA,SAASA,EAAW,GAA8B;CAChD,IAAMC,IAAQC,EAAQ,UAAU,EAAI,GAC9BC,IAAmB,CAAC,GACtBC,GAEEC,IAAS,SAAS,iBACtBJ,GACA,WAAW,eAAe,WAAW,YACvC;CACA,KAAK,IAAIK,IAAOD,EAAO,SAAS,GAAGC,GAAM,IAAOD,EAAO,SAAS,GAAG;EACjE,IAAIC,EAAK,aAAa,KAAK,cAAc;GACvC,EAAS,KAAKA,CAAI;GAClB;EACF;EAEA,IAAMC,IAAKD;EACX,KAAK,IAAM,EAAE,aAAU,MAAM,KAAKC,EAAG,UAAU,GAC7C,AAAIC,EAAK,WAAW,SAAS,MAC3B,IAAiBA,GACjBD,EAAG,gBAAgBC,CAAI;CAG7B;CACA,EAAS,SAAS,MAAYC,EAAQ,YAAY,YAAYA,CAAO,CAAC;CAEtE,IAAM,GAAGC,GAAmBC,GAAMC,KAChC,yBAAyB,KAAKX,EAAM,SAAS;CAE/C,OAAO;EAAE;EAAM;EAAmB;EAAoB;CAAe;AACvE;AAEA,SAASY,EAAmB,GAAmB;CAC7C,IAAK,MAAM,QAAQC,CAAQ,GAI3B,KAAK,IAAMC,KAASD,GAAU;EAC5B,IAAI,CAACE,EAAQD,CAAK,GAAG;GACnB,EAAmBA,CAAK;GACxB;EACF;EAEA,IAAME,IAAQF;EACd,IAAIE,EAAM,SAASC,KAAQD,EAAM,SAASE,GAG1C;OAAI,OAAOF,EAAM,QAAS,YAAY,OAAOA,EAAM,QAAS,YAAY;IACtE,IAAMT,IACHS,EAAM,KAA4C,QAClDA,EAAM,KAA6B;IACtC,MAAU,MACR,+DACET,IAAO,kDAAkDA,EAAK,KAAK,IAEvE;GACF;GAEA,EAAmBS,EAAM,QAAQ;EAFjC;CAGF;AACF;AAMA,IAAaG,IAAYC,EAAgB;CACvC,MAAM;CACN,OAAO;EACL,MAAM;EACN,WAAW;CACb;CACA,MAAM,GAAO,EAAE,YAAS;EACtB,IAAMC,IAAiBC,EAAkB,GACnCC,IAAYC,EAAiB,GAC7BC,IAAYD,EAAiB,GAC7BE,IAASC,EAA0B,IAAI,GACvCC,IAAaJ,EAAI,EAAK,GACxBK,IAAgB,GAChBC,GAEEC,KAAU,GAAgB,MAAwB;GACtD,IAAMC,IAASP,EAAU;GACzB,AAAKO,MAIL,EAAO,YAAY,GAAGN,EAAO,oBAAoBO,IAAcP,EAAO,sBAClEA,EAAO,kBACTM,EACG,iBAAiB,GAAG,CAAC,CACrB,SAAS,MAAO1B,EAAG,aAAaoB,EAAO,gBAAiB,EAAE,CAAC,GAEhE,EAAW,QAAQ;EACrB;EAuDA,OArDAQ,QAAgB;GACd,IAAMjC,IAAUsB,EAAU,OACpBY,UAAa;IACjB,IAAMC,IAAOrC,EAAWE,CAAO,GACzBoC,IAAUX,EAAO;IACvB,CACEU,EAAK,SAASC,GAAS,QACvBD,EAAK,sBAAsBC,EAAQ,qBACnCD,EAAK,uBAAuBC,EAAQ,wBAEpC,EAAO,QAAQD;GAEnB;GAKA,AAJAD,EAAK,GAGL,IAAW,IAAI,iBAAiBA,CAAI,GACpCL,EAAS,QAAQ7B,GAAS;IACxB,SAAS;IACT,WAAW;IACX,eAAe;IACf,YAAY;GACd,CAAC;EACH,CAAC,GAEDqC,QAAsBR,GAAU,WAAW,CAAC,GAE5CS,EACE;GACEb;SACMc,EAAM;SACNA,EAAM;GAEZnB,EAAe;EACjB,IACC,CAACK,GAAQe,GAAMC,OAAe;GAC7B,IAAI,CAAChB,GAAQ,MAAM;IACjB,EAAW,QAAQ;IACnB;GACF;GAGA,IAAMiB,IAAY,EAAEd;GACpB,EACG,UAAUH,EAAO,MAAM;IAAE;IAAM;GAAU,CAAC,CAAC,CAC3C,MAAM,MAAgB;IACrB,AAAIiB,MAAcd,KAChBE,EAAOL,GAAQO,CAAW;GAE9B,CAAC;EACL,CACF,SAEa;GACX,IAAMpB,IAAW+B,EAAM,UAAU;GAGjC,OAFAhC,EAAmBC,CAAQ,GAEpBgC,EAAE,QAAQ,EAAE,OAAO,oBAAoB,GAAG,CAC/CA,EACE,QACA;IACE,KAAKtB;IACL,OAAO,EAAE,SAASK,EAAW,QAAQ,SAAS,WAAW;GAC3D,GACAf,CACF,GACAgC,EAAE,QAAQ;IACR,KAAKpB;IACL,OAAO,EAAE,SAASG,EAAW,QAAQ,aAAa,OAAO;GAC3D,CAAC,CACH,CAAC;EACH;CACF;AACF,CAAC,GAEYkB,IAAI3B,GCzJJ4B,IACX,OAAO,iBAAiB;AAM1B,SAAgBC,EACd,GACgB;CAChB,IAAI,CAACC,EAAO,QACV,MAAU,MAAM,sCAAsCC,GAAgB;CAGxE,IAAM,EAAE,UAAO,WAAQ,mBAAgB,YAAS,iBAAcD,GACxDE,IAAgBF,EAAO,kBAAkBG,GACzCC,IAAW7B,EAAeyB,EAAO,QAAQ,GAEzCK,IAAaC,QAEf,IAAIC,EACFL,EAAc;EACZ,OAAO,CAAC,CAACM;EACT;EACA,MAAMJ,EAAS;EACf,YAAYK;EACZ,QAAQC;EACR;CACF,CAAC,GACD,EAAE,YAAYC,EAAWC,GAAQH,GAAgBL,EAAS,KAAK,EAAE,CACnE,CACJ,GAEMhC,IAAiC;EACrC,UAAUyC,EAAST,CAAQ;EAC3B;EACA;EACA;EACA,YAAY,GAAM;GAChB,EAAS,QAAQZ;EACnB;EACA,UAAU,GAAM,IAAS,CAAC,GAAG;GAC3B,OAAOa,EAAW,MAAM,KAAK;IAAE;IAAM,GAAGS;GAAO,CAAC;EAClD;EACA,SAAS,GAAM,IAAS,CAAC,GAAG;GAC1B,OAAOH,EACLC,GACAH,GACAL,EAAS,KACX,CAAC,CAAC;IACA;IACA,GAAGU;GACL,CAAC;EACH;EACA,QAAQ,GAAK;GAEX,AADAC,EAAI,QAAQjB,GAAiB1B,CAAc,GAC3C2C,EAAI,UAAU,KAAK7C,CAAS;EAC9B;CACF;CAEA,OAAOE;AACT;AAEA,SAAgBC,IAAoC;CAClD,IAAMD,IAAiB4C,EAAOlB,GAAiB,IAAI;CAEnD,IAAI1B,MAAmB,MACrB,MAAU,MACR,gFACF;CAGF,OAAOA;AACT;AAEA,SAAgB6C,IAGd;CACA,IAAM,EAAE,gBAAa,gBAAa5C,EAAkB;CAEpD,OAAO;EAAE;EAAa,iBAAiB+B;CAAS;AAClD;;ACzHA,SAAgBc,IAGJ;CACV,IAAM9C,IAAiBC,EAAkB,GACnC8C,IAAeC,kBAAS,IAAI,IAAoB,CAAC,GACjDC,oBAAU,IAAI,IAAY;CAEhC,QAAQ,GAAM,IAAS,CAAC,MAAM;EAE5B,IAAI,CAACC,KAAQ,OAAO,SAAW,KAC7B,OAAOA;EAIT,IAAMC,IAAMnD,EAAe,SAASkD,GAAMR,CAAM,GAC1C9B,IAAcmC,EAAa,IAAII,CAAG;EAUxC,OARIvC,MAAgB,KAAA,KAAa,CAACqC,EAAQ,IAAIE,CAAG,MAC/CF,EAAQ,IAAIE,CAAG,GACfnD,EAAe,UAAUkD,GAAMR,CAAM,CAAC,CAAC,MAAM,MAAe;GAE1D,AADAK,EAAa,IAAII,GAAK5C,CAAU,GAChC0C,EAAQ,OAAOE,CAAG;EACpB,CAAC,IAGIvC,KAAesC;CACxB;AACF;AAEA,IAAaE,IAAON"}
@@ -0,0 +1,34 @@
1
+ import { App, ComputedRef, InjectionKey, Ref } from 'vue';
2
+ import { DataLoader, DataLoaderLoadParams, Language, MagicTranslateConfig } from '@magic-translate/core';
3
+ /**
4
+ * Type alias to make the language enum more flexible to also allow passing language strings
5
+ * instead of just the enum.
6
+ */
7
+ export type Languages = `${Language}`;
8
+ export type TranslateParams = Omit<DataLoaderLoadParams, 'text'>;
9
+ export interface MagicTranslate {
10
+ /**
11
+ * The language content is currently translated to.
12
+ */
13
+ readonly language: Readonly<Ref<Languages>>;
14
+ readonly apiKey: string;
15
+ readonly sourceLanguage?: Languages;
16
+ /** @ignore */
17
+ readonly dataLoader: ComputedRef<DataLoader<DataLoaderLoadParams, string, string>>;
18
+ setLanguage(language: Languages): void;
19
+ translate(text: string, params?: TranslateParams): Promise<string>;
20
+ /** @ignore */
21
+ cacheKey(text: string, params?: TranslateParams): string;
22
+ install(app: App): void;
23
+ }
24
+ export declare const MAGIC_TRANSLATE: InjectionKey<MagicTranslate>;
25
+ /**
26
+ * Sets up Magic Translate. Install the result as a plugin with `app.use()`.
27
+ * It can also be used directly, outside of components.
28
+ */
29
+ export declare function createMagicTranslate(config: MagicTranslateConfig): MagicTranslate;
30
+ export declare function useMagicTranslate(): MagicTranslate;
31
+ export declare function useLanguageSwitcher(): {
32
+ setLanguage: (language: Languages) => void;
33
+ currentLanguage: Readonly<Ref<Languages>>;
34
+ };
@@ -0,0 +1,30 @@
1
+ import { PropType, VNode } from 'vue';
2
+ import { TranslateOptions } from '@magic-translate/core';
3
+ import { Languages } from './magic-translate';
4
+ /**
5
+ * Translates its content, which can be text and plain HTML elements. The
6
+ * original content is shown until the translation has loaded.
7
+ */
8
+ export declare const Translate: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
9
+ lang: PropType<Languages>;
10
+ formality: PropType<TranslateOptions["formality"]>;
11
+ }>, () => VNode<import("vue").RendererNode, import("vue").RendererElement, {
12
+ [key: string]: any;
13
+ }>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
14
+ lang: PropType<Languages>;
15
+ formality: PropType<TranslateOptions["formality"]>;
16
+ }>> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
17
+ export declare const T: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
18
+ lang: PropType<Languages>;
19
+ formality: PropType<TranslateOptions["formality"]>;
20
+ }>, () => VNode<import("vue").RendererNode, import("vue").RendererElement, {
21
+ [key: string]: any;
22
+ }>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
23
+ lang: PropType<Languages>;
24
+ formality: PropType<TranslateOptions["formality"]>;
25
+ }>> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
26
+ declare module 'vue' {
27
+ interface GlobalComponents {
28
+ T: typeof Translate;
29
+ }
30
+ }
@@ -0,0 +1,8 @@
1
+ import { TranslateParams } from './magic-translate';
2
+ /**
3
+ * Returns a function that translates plain strings, e.g. for attributes:
4
+ * `<input :placeholder="t('Search')" />`. It returns the original text until
5
+ * the translation has loaded, and the component re-renders once it has.
6
+ */
7
+ export declare function useTranslate(): (text: string, params?: TranslateParams) => string;
8
+ export declare const useT: typeof useTranslate;
package/package.json CHANGED
@@ -1,6 +1,71 @@
1
1
  {
2
2
  "name": "@magic-translate/vue",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.0.1",
4
+ "keywords": [
5
+ "vue",
6
+ "nuxt",
7
+ "magic-translate",
8
+ "i18n",
9
+ "translations",
10
+ "multilanguage"
11
+ ],
12
+ "description": "Vue client for Magic Translate (https://magictranslate.io)",
13
+ "main": "dist/index.cjs",
14
+ "module": "dist/index.js",
15
+ "types": "dist/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "import": "./dist/index.js",
20
+ "require": "./dist/index.cjs"
21
+ }
22
+ },
23
+ "sideEffects": false,
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "author": "i42 GmbH <info@i42.ch>",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/magic-translate/magic-translate-vue.git"
31
+ },
32
+ "homepage": "https://magictranslate.io",
33
+ "license": "MIT",
34
+ "scripts": {
35
+ "build": "vite build && tsc -p tsconfig.build.json",
36
+ "start": "vite build --watch",
37
+ "test": "vitest run --coverage",
38
+ "test:watch": "vitest",
39
+ "prepare": "husky",
40
+ "lint": "eslint ."
41
+ },
42
+ "engines": {
43
+ "node": ">=22.22.1"
44
+ },
45
+ "peerDependencies": {
46
+ "vue": ">=3.3.0"
47
+ },
48
+ "dependencies": {
49
+ "@magic-translate/core": "^1.4.0"
50
+ },
51
+ "devDependencies": {
52
+ "@commitlint/cli": "^21.2.4",
53
+ "@commitlint/config-conventional": "^21.2.4",
54
+ "@eslint/js": "^10.0.1",
55
+ "@types/node": "^26.6.5",
56
+ "@vitest/coverage-v8": "^5.0.5",
57
+ "@vue/test-utils": "^2.5.1",
58
+ "eslint": "^10.13.0",
59
+ "eslint-config-prettier": "^10.1.8",
60
+ "globals": "^17.13.0",
61
+ "husky": "^9.1.7",
62
+ "jsdom": "^30.1.3",
63
+ "lint-staged": "^17.6.0",
64
+ "prettier": "^3.9.10",
65
+ "typescript": "~6.0.3",
66
+ "typescript-eslint": "^8.72.0",
67
+ "vite": "^8.3.5",
68
+ "vitest": "^5.0.5",
69
+ "vue": "^3.5.44"
70
+ }
71
+ }