@magic-translate/vue 1.0.3 → 1.2.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/README.md CHANGED
@@ -105,6 +105,34 @@ const t = useTranslate();
105
105
  `t` returns the original text until the translation has loaded, and the
106
106
  component re-renders once it has.
107
107
 
108
+ ### Variables
109
+
110
+ Pass values that change, such as names or counts, in `v` and reference them
111
+ as `$name`. The text is translated once as a template, `Hi {0}, you have {1}
112
+ new messages`, and cached, however many different values it is shown with. The
113
+ values themselves are never sent to the API.
114
+
115
+ ```vue
116
+ <T :v="{ name: user.name, count }">Hi $name, you have $count new messages</T>
117
+ ```
118
+
119
+ A translation can move the values to wherever the language needs them, and the
120
+ text around them can contain HTML. When only the values change, the cached
121
+ translation is reused and no request is made.
122
+
123
+ - Only keys of `v` are replaced, so text like `$5` stays as it is. If two keys
124
+ start the same way, the longest one wins.
125
+ - Values are inserted as text, never as HTML. Put markup in the text around
126
+ them instead: `Hi <b>$name</b>`.
127
+ - Variables are replaced in text only, not in attributes such as `href`.
128
+ - A key that is not found in the text logs a warning.
129
+
130
+ `t` and `translate` take the values in `v` as well:
131
+
132
+ ```vue
133
+ <input :placeholder="t('Search $count products', { v: { count } })" />
134
+ ```
135
+
108
136
  ### Options per translation
109
137
 
110
138
  Both the component and `t` can override the target language and the formality:
@@ -115,6 +143,17 @@ Both the component and `t` can override the target language and the formality:
115
143
  <input :placeholder="t('Your name', { lang: 'fr', formality: 'formal' })" />
116
144
  ```
117
145
 
146
+ An `id` lets you edit one use of a text on its own in the webapp, e.g. to
147
+ translate the same "Save" differently on two buttons. Its edited translation
148
+ stays when the text changes. Ids are unique within your account and at most 128
149
+ characters.
150
+
151
+ ```vue
152
+ <T id="checkout-save">Save</T>
153
+
154
+ <button :title="t('Save', { id: 'checkout-save' })" />
155
+ ```
156
+
118
157
  ### Switching languages
119
158
 
120
159
  `useLanguageSwitcher` reads and changes the current language. Every `<T>`
@@ -188,7 +227,9 @@ export default defineNuxtPlugin((nuxtApp) => {
188
227
  ```
189
228
 
190
229
  Translations are only loaded in the browser. With server-side rendering the
191
- server renders the original content, and it is translated after hydration.
230
+ server renders the original content, and it is translated after hydration. A
231
+ `<T>` with `v` renders empty on the server, so `$name` never shows, and
232
+ appears once the app runs in the browser.
192
233
 
193
234
  ## License
194
235
 
package/dist/index.cjs CHANGED
@@ -1,2 +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]}})});
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,id:String,v:Object},setup(i,{slots:a}){let o=c(),s=(0,t.ref)(),l=(0,t.ref)(),u=(0,t.shallowRef)(null),d=(0,t.ref)(!1),f=0,p,m,h=(t,n,r=[])=>{let i=l.value;i&&(i.innerHTML=`${t.leadingWhitespace}${n}${t.trailingWhitespace}`,r.length>0&&(0,e.fillElement)(i,r),t.scopeAttribute&&i.querySelectorAll(`*`).forEach(e=>e.setAttribute(t.scopeAttribute,``)),d.value=!0)};return(0,t.onMounted)(()=>{let e=s.value,t=()=>{let t=n(e),r=u.value;(t.html!==r?.html||t.leadingWhitespace!==r.leadingWhitespace||t.trailingWhitespace!==r.trailingWhitespace)&&(u.value=t)};t(),m=new MutationObserver(t),m.observe(e,{subtree:!0,childList:!0,characterData:!0,attributes:!0})}),(0,t.onBeforeUnmount)(()=>m?.disconnect()),(0,t.watch)([u,()=>i.lang,()=>i.formality,()=>i.id,()=>i.v,o.language],([t,n,r,i,a])=>{if(!t?.html){d.value=!1;return}let{template:s,values:c,unused:l}=a?(0,e.toHtmlTemplate)(t.html,a):{template:t.html,values:[],unused:[]};s!==p&&(p=s,(0,e.warnAboutUnused)(l)),a&&!d.value&&h(t,s,c);let u=++f;o.translate(s,{lang:n,formality:r,id:i}).then(e=>{u===f&&h(t,e,c)})}),()=>{let e=a.default?.();return r(e),(0,t.h)(`span`,{style:`display: contents`},[(0,t.h)(`span`,{ref:s,style:{display:d.value||i.v?`none`:`contents`}},e),(0,t.h)(`span`,{ref:l,style:{display:d.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(t,{v:n,...r}={}){if(!n)return f.value.load({text:t,...r});let{template:i,values:a,unused:o}=(0,e.toTemplate)(t,n);return(0,e.warnAboutUnused)(o),f.value.load({text:i,...r}).then(t=>(0,e.fillPlaceholders)(t,a))},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 n=c(),r=(0,t.reactive)(new Map),i=new Set;return(t,{v:a,...o}={})=>{let{template:s,values:c,unused:l}=a?(0,e.toTemplate)(t,a):{template:t,values:[],unused:[]};if(!t||typeof window>`u`)return(0,e.fillPlaceholders)(s,c);let u=n.cacheKey(s,o),d=r.get(u);return d===void 0&&!i.has(u)&&((0,e.warnAboutUnused)(l),i.add(u),n.translate(s,o).then(e=>{r.set(u,e),i.delete(u)})),(0,e.fillPlaceholders)(d??s,c)}}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
2
  //# sourceMappingURL=index.cjs.map
@@ -1 +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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"}
1
+ {"version":3,"file":"index.cjs","names":["isVNode","Text","Comment","defineComponent","ref","shallowRef","toHtmlTemplate","h","libraryWebsite","defaultLoaderFactory","ref","computed","DataLoader","cacheKeyFn","readonly","toTemplate","fillPlaceholders","inject","reactive","toTemplate","fillPlaceholders","_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","lastTemplate","observer","render","target","translation","values","read","next","current","props","lang","formality","id","variables","template","unused","requestId","slots","T","MAGIC_TRANSLATE","createMagicTranslate","config","loaderFactory","language","dataLoader","debug","sourceLanguage","_apiUrl","apiKey","params","v","text","app","useLanguageSwitcher","useTranslate","translations","pending","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 {\n fillElement,\n toHtmlTemplate,\n TranslateOptions,\n Variables,\n warnAboutUnused,\n} 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 * Lets this use of the text be edited on its own in the webapp. Its\n * edited translation stays when the text changes.\n */\n id: String,\n /**\n * Values that are not translated, referenced as `$name` in the content.\n * The text is translated once as a template and cached, whatever the\n * values are.\n */\n v: Object as PropType<Variables>,\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 lastTemplate: string | undefined;\n let observer: MutationObserver | undefined;\n\n const render = (\n source: Source,\n translation: string,\n values: readonly string[] = []\n ) => {\n const target = targetRef.value;\n if (!target) {\n return;\n }\n\n target.innerHTML = `${source.leadingWhitespace}${translation}${source.trailingWhitespace}`;\n if (values.length > 0) {\n fillElement(target, values);\n }\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 () => props.id,\n () => props.v,\n // a language switch has to re-translate the content\n magicTranslate.language,\n ],\n ([source, lang, formality, id, variables]) => {\n if (!source?.html) {\n translated.value = false;\n return;\n }\n\n const { template, values, unused } = variables\n ? toHtmlTemplate(source.html, variables)\n : { template: source.html, values: [], unused: [] };\n if (template !== lastTemplate) {\n lastTemplate = template;\n warnAboutUnused(unused);\n }\n if (variables && !translated.value) {\n render(source, template, values);\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(template, { lang, formality, id })\n .then((translation) => {\n if (requestId === lastRequestId) {\n render(source, translation, values);\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 // With variables, the source would show `$name`, so it stays\n // hidden and the text is shown with the values filled in until\n // the translation arrives.\n style: {\n display: translated.value || props.v ? 'none' : 'contents',\n },\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 fillPlaceholders,\n Language,\n libraryWebsite,\n MagicTranslateConfig,\n toTemplate,\n Variables,\n warnAboutUnused,\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 interface TranslateParams extends Omit<DataLoaderLoadParams, 'text'> {\n /**\n * Values that are not translated, referenced as `$name` in the text. The text\n * is translated once as a template and cached, whatever the values are.\n */\n v?: Variables;\n}\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?: Omit<TranslateParams, 'v'>): 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, { v, ...params } = {}) {\n if (!v) {\n return dataLoader.value.load({ text, ...params });\n }\n const { template, values, unused } = toTemplate(text, v);\n warnAboutUnused(unused);\n return dataLoader.value\n .load({ text: template, ...params })\n .then((translation) => fillPlaceholders(translation, values));\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 {\n fillPlaceholders,\n toTemplate,\n warnAboutUnused,\n} from '@magic-translate/core';\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 *\n * Values that should not be translated go in `v` and are referenced as `$name`:\n * `t('Search $count products', { v: { count } })`.\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, { v, ...params } = {}) => {\n // Only the template is translated, so new values reuse the translation.\n const { template, values, unused } = v\n ? toTemplate(text, v)\n : { template: text, values: [], unused: [] };\n\n // Translations are only loaded in the browser.\n if (!text || typeof window === 'undefined') {\n return fillPlaceholders(template, values);\n }\n\n // reads the current language, so a language switch re-renders\n const key = magicTranslate.cacheKey(template, params);\n const translation = translations.get(key);\n\n if (translation === undefined && !pending.has(key)) {\n warnAboutUnused(unused);\n pending.add(key);\n magicTranslate.translate(template, params).then((translated) => {\n translations.set(key, translated);\n pending.delete(key);\n });\n }\n\n return fillPlaceholders(translation ?? template, values);\n };\n}\n\nexport const useT = useTranslate;\n"],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
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";
1
+ import { DataLoader as e, cacheKeyFn as t, defaultLoaderFactory as n, fillElement as r, fillPlaceholders as i, libraryWebsite as a, toHtmlTemplate as o, toTemplate as s, warnAboutUnused as c } from "@magic-translate/core";
2
+ import { Comment as l, Text as u, computed as d, defineComponent as f, h as p, inject as m, isVNode as h, onBeforeUnmount as g, onMounted as _, reactive as v, readonly as y, ref as b, shallowRef as x, watch as S } from "vue";
3
3
  export * from "@magic-translate/core";
4
4
  // src/translate.ts
5
- function v(e) {
5
+ function C(e) {
6
6
  let t = e.cloneNode(!0), n = [], r, i = document.createTreeWalker(t, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT);
7
7
  for (let e = i.nextNode(); e; e = i.nextNode()) {
8
8
  if (e.nodeType === Node.COMMENT_NODE) {
@@ -21,132 +21,153 @@ function v(e) {
21
21
  scopeAttribute: r
22
22
  };
23
23
  }
24
- function y(e) {
24
+ function w(e) {
25
25
  if (Array.isArray(e)) for (let t of e) {
26
- if (!u(t)) {
27
- y(t);
26
+ if (!h(t)) {
27
+ w(t);
28
28
  continue;
29
29
  }
30
30
  let e = t;
31
- if (e.type !== a && e.type !== i) {
31
+ if (e.type !== u && e.type !== l) {
32
32
  if (typeof e.type == "object" || typeof e.type == "function") {
33
33
  let t = e.type.name ?? e.type.__name;
34
34
  throw Error(`Components are not allowed inside \`<T>...</T>\` components.${t ? ` The component found inside the T component is ${t}.` : ""}`);
35
35
  }
36
- y(e.children);
36
+ w(e.children);
37
37
  }
38
38
  }
39
39
  }
40
- var b = s({
40
+ var T = f({
41
41
  name: "T",
42
42
  props: {
43
43
  lang: String,
44
- formality: String
44
+ formality: String,
45
+ id: String,
46
+ v: Object
45
47
  },
46
48
  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);
49
+ let n = k(), i = b(), a = b(), s = x(null), l = b(!1), u = 0, d, f, m = (e, t, n = []) => {
50
+ let i = a.value;
51
+ i && (i.innerHTML = `${e.leadingWhitespace}${t}${e.trailingWhitespace}`, n.length > 0 && r(i, n), e.scopeAttribute && i.querySelectorAll("*").forEach((t) => t.setAttribute(e.scopeAttribute, "")), l.value = !0);
50
52
  };
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);
53
+ return _(() => {
54
+ let e = i.value, t = () => {
55
+ let t = C(e), n = s.value;
56
+ (t.html !== n?.html || t.leadingWhitespace !== n.leadingWhitespace || t.trailingWhitespace !== n.trailingWhitespace) && (s.value = t);
55
57
  };
56
- t(), l = new MutationObserver(t), l.observe(e, {
58
+ t(), f = new MutationObserver(t), f.observe(e, {
57
59
  subtree: !0,
58
60
  childList: !0,
59
61
  characterData: !0,
60
62
  attributes: !0
61
63
  });
62
- }), d(() => l?.disconnect()), _([
63
- a,
64
+ }), g(() => f?.disconnect()), S([
65
+ s,
64
66
  () => e.lang,
65
67
  () => e.formality,
68
+ () => e.id,
69
+ () => e.v,
66
70
  n.language
67
- ], ([e, t, r]) => {
71
+ ], ([e, t, r, i, a]) => {
68
72
  if (!e?.html) {
69
- o.value = !1;
73
+ l.value = !1;
70
74
  return;
71
75
  }
72
- let i = ++s;
73
- n.translate(e.html, {
76
+ let { template: s, values: f, unused: p } = a ? o(e.html, a) : {
77
+ template: e.html,
78
+ values: [],
79
+ unused: []
80
+ };
81
+ s !== d && (d = s, c(p)), a && !l.value && m(e, s, f);
82
+ let h = ++u;
83
+ n.translate(s, {
74
84
  lang: t,
75
- formality: r
85
+ formality: r,
86
+ id: i
76
87
  }).then((t) => {
77
- i === s && u(e, t);
88
+ h === u && m(e, t, f);
78
89
  });
79
90
  }), () => {
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", {
91
+ let n = t.default?.();
92
+ return w(n), p("span", { style: "display: contents" }, [p("span", {
85
93
  ref: i,
86
- style: { display: o.value ? "contents" : "none" }
94
+ style: { display: l.value || e.v ? "none" : "contents" }
95
+ }, n), p("span", {
96
+ ref: a,
97
+ style: { display: l.value ? "contents" : "none" }
87
98
  })]);
88
99
  };
89
100
  }
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,
101
+ }), E = T, D = Symbol("magic-translate");
102
+ function O(r) {
103
+ if (!r.apiKey) throw Error(`Missing API key. Please get one at ${a}`);
104
+ let { debug: o, apiKey: l, sourceLanguage: u, _apiUrl: f, formality: p } = r, m = r._loaderFactory ?? n, h = b(r.language), g = d(() => new e(m({
105
+ debug: !!o,
106
+ apiKey: l,
107
+ lang: h.value,
108
+ sourceLang: u,
109
+ apiUrl: f,
110
+ formality: p
111
+ }), { cacheKeyFn: t(l, u, h.value) })), _ = {
112
+ language: y(h),
113
+ apiKey: l,
114
+ sourceLanguage: u,
115
+ dataLoader: g,
105
116
  setLanguage(e) {
106
- f.value = e;
117
+ h.value = e;
107
118
  },
108
- translate(e, t = {}) {
109
- return p.value.load({
119
+ translate(e, { v: t, ...n } = {}) {
120
+ if (!t) return g.value.load({
110
121
  text: e,
111
- ...t
122
+ ...n
112
123
  });
124
+ let { template: r, values: a, unused: o } = s(e, t);
125
+ return c(o), g.value.load({
126
+ text: r,
127
+ ...n
128
+ }).then((e) => i(e, a));
113
129
  },
114
130
  cacheKey(e, n = {}) {
115
- return t(s, c, f.value)({
131
+ return t(l, u, h.value)({
116
132
  text: e,
117
133
  ...n
118
134
  });
119
135
  },
120
136
  install(e) {
121
- e.provide(S, g), e.component("T", b);
137
+ e.provide(D, _), e.component("T", T);
122
138
  }
123
139
  };
124
- return g;
140
+ return _;
125
141
  }
126
- function w() {
127
- let e = l(S, null);
142
+ function k() {
143
+ let e = m(D, null);
128
144
  if (e === null) throw Error("useMagicTranslate must be used in an app that installed createMagicTranslate()");
129
145
  return e;
130
146
  }
131
- function T() {
132
- let { setLanguage: e, language: t } = w();
147
+ function A() {
148
+ let { setLanguage: e, language: t } = k();
133
149
  return {
134
150
  setLanguage: e,
135
151
  currentLanguage: t
136
152
  };
137
153
  }
138
154
  // 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;
155
+ function j() {
156
+ let e = k(), t = v(/* @__PURE__ */ new Map()), n = /* @__PURE__ */ new Set();
157
+ return (r, { v: a, ...o } = {}) => {
158
+ let { template: l, values: u, unused: d } = a ? s(r, a) : {
159
+ template: r,
160
+ values: [],
161
+ unused: []
162
+ };
163
+ if (!r || typeof window > "u") return i(l, u);
164
+ let f = e.cacheKey(l, o), p = t.get(f);
165
+ return p === void 0 && !n.has(f) && (c(d), n.add(f), e.translate(l, o).then((e) => {
166
+ t.set(f, e), n.delete(f);
167
+ })), i(p ?? l, u);
147
168
  };
148
169
  }
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 };
170
+ var M = j;
171
+ export { D as MAGIC_TRANSLATE, E as T, T as Translate, O as createMagicTranslate, A as useLanguageSwitcher, k as useMagicTranslate, M as useT, j as useTranslate };
151
172
 
152
173
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +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":"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"}
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","lastTemplate","observer","render","target","translation","values","fillElement","onMounted","read","next","current","onBeforeUnmount","watch","props","lang","formality","id","variables","toHtmlTemplate","template","warnAboutUnused","unused","requestId","slots","h","T","MAGIC_TRANSLATE","createMagicTranslate","config","libraryWebsite","loaderFactory","defaultLoaderFactory","language","dataLoader","computed","DataLoader","debug","sourceLanguage","_apiUrl","cacheKeyFn","apiKey","readonly","params","v","toTemplate","text","fillPlaceholders","app","inject","useLanguageSwitcher","useTranslate","translations","reactive","pending","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 {\n fillElement,\n toHtmlTemplate,\n TranslateOptions,\n Variables,\n warnAboutUnused,\n} 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 * Lets this use of the text be edited on its own in the webapp. Its\n * edited translation stays when the text changes.\n */\n id: String,\n /**\n * Values that are not translated, referenced as `$name` in the content.\n * The text is translated once as a template and cached, whatever the\n * values are.\n */\n v: Object as PropType<Variables>,\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 lastTemplate: string | undefined;\n let observer: MutationObserver | undefined;\n\n const render = (\n source: Source,\n translation: string,\n values: readonly string[] = []\n ) => {\n const target = targetRef.value;\n if (!target) {\n return;\n }\n\n target.innerHTML = `${source.leadingWhitespace}${translation}${source.trailingWhitespace}`;\n if (values.length > 0) {\n fillElement(target, values);\n }\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 () => props.id,\n () => props.v,\n // a language switch has to re-translate the content\n magicTranslate.language,\n ],\n ([source, lang, formality, id, variables]) => {\n if (!source?.html) {\n translated.value = false;\n return;\n }\n\n const { template, values, unused } = variables\n ? toHtmlTemplate(source.html, variables)\n : { template: source.html, values: [], unused: [] };\n if (template !== lastTemplate) {\n lastTemplate = template;\n warnAboutUnused(unused);\n }\n if (variables && !translated.value) {\n render(source, template, values);\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(template, { lang, formality, id })\n .then((translation) => {\n if (requestId === lastRequestId) {\n render(source, translation, values);\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 // With variables, the source would show `$name`, so it stays\n // hidden and the text is shown with the values filled in until\n // the translation arrives.\n style: {\n display: translated.value || props.v ? 'none' : 'contents',\n },\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 fillPlaceholders,\n Language,\n libraryWebsite,\n MagicTranslateConfig,\n toTemplate,\n Variables,\n warnAboutUnused,\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 interface TranslateParams extends Omit<DataLoaderLoadParams, 'text'> {\n /**\n * Values that are not translated, referenced as `$name` in the text. The text\n * is translated once as a template and cached, whatever the values are.\n */\n v?: Variables;\n}\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?: Omit<TranslateParams, 'v'>): 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, { v, ...params } = {}) {\n if (!v) {\n return dataLoader.value.load({ text, ...params });\n }\n const { template, values, unused } = toTemplate(text, v);\n warnAboutUnused(unused);\n return dataLoader.value\n .load({ text: template, ...params })\n .then((translation) => fillPlaceholders(translation, values));\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 {\n fillPlaceholders,\n toTemplate,\n warnAboutUnused,\n} from '@magic-translate/core';\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 *\n * Values that should not be translated go in `v` and are referenced as `$name`:\n * `t('Search $count products', { v: { count } })`.\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, { v, ...params } = {}) => {\n // Only the template is translated, so new values reuse the translation.\n const { template, values, unused } = v\n ? toTemplate(text, v)\n : { template: text, values: [], unused: [] };\n\n // Translations are only loaded in the browser.\n if (!text || typeof window === 'undefined') {\n return fillPlaceholders(template, values);\n }\n\n // reads the current language, so a language switch re-renders\n const key = magicTranslate.cacheKey(template, params);\n const translation = translations.get(key);\n\n if (translation === undefined && !pending.has(key)) {\n warnAboutUnused(unused);\n pending.add(key);\n magicTranslate.translate(template, params).then((translated) => {\n translations.set(key, translated);\n pending.delete(key);\n });\n }\n\n return fillPlaceholders(translation ?? template, values);\n };\n}\n\nexport const useT = useTranslate;\n"],"mappings":"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"}
@@ -1,11 +1,17 @@
1
1
  import { App, ComputedRef, InjectionKey, Ref } from 'vue';
2
- import { DataLoader, DataLoaderLoadParams, Language, MagicTranslateConfig } from '@magic-translate/core';
2
+ import { DataLoader, DataLoaderLoadParams, Language, MagicTranslateConfig, Variables } from '@magic-translate/core';
3
3
  /**
4
4
  * Type alias to make the language enum more flexible to also allow passing language strings
5
5
  * instead of just the enum.
6
6
  */
7
7
  export type Languages = `${Language}`;
8
- export type TranslateParams = Omit<DataLoaderLoadParams, 'text'>;
8
+ export interface TranslateParams extends Omit<DataLoaderLoadParams, 'text'> {
9
+ /**
10
+ * Values that are not translated, referenced as `$name` in the text. The text
11
+ * is translated once as a template and cached, whatever the values are.
12
+ */
13
+ v?: Variables;
14
+ }
9
15
  export interface MagicTranslate {
10
16
  /**
11
17
  * The language content is currently translated to.
@@ -18,7 +24,7 @@ export interface MagicTranslate {
18
24
  setLanguage(language: Languages): void;
19
25
  translate(text: string, params?: TranslateParams): Promise<string>;
20
26
  /** @ignore */
21
- cacheKey(text: string, params?: TranslateParams): string;
27
+ cacheKey(text: string, params?: Omit<TranslateParams, 'v'>): string;
22
28
  install(app: App): void;
23
29
  }
24
30
  export declare const MAGIC_TRANSLATE: InjectionKey<MagicTranslate>;
@@ -1,5 +1,5 @@
1
1
  import { PropType, VNode } from 'vue';
2
- import { TranslateOptions } from '@magic-translate/core';
2
+ import { TranslateOptions, Variables } from '@magic-translate/core';
3
3
  import { Languages } from './magic-translate';
4
4
  /**
5
5
  * Translates its content, which can be text and plain HTML elements. The
@@ -8,20 +8,64 @@ import { Languages } from './magic-translate';
8
8
  export declare const Translate: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
9
9
  lang: PropType<Languages>;
10
10
  formality: PropType<TranslateOptions["formality"]>;
11
+ /**
12
+ * Lets this use of the text be edited on its own in the webapp. Its
13
+ * edited translation stays when the text changes.
14
+ */
15
+ id: StringConstructor;
16
+ /**
17
+ * Values that are not translated, referenced as `$name` in the content.
18
+ * The text is translated once as a template and cached, whatever the
19
+ * values are.
20
+ */
21
+ v: PropType<Variables>;
11
22
  }>, () => VNode<import("vue").RendererNode, import("vue").RendererElement, {
12
23
  [key: string]: any;
13
24
  }>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
14
25
  lang: PropType<Languages>;
15
26
  formality: PropType<TranslateOptions["formality"]>;
27
+ /**
28
+ * Lets this use of the text be edited on its own in the webapp. Its
29
+ * edited translation stays when the text changes.
30
+ */
31
+ id: StringConstructor;
32
+ /**
33
+ * Values that are not translated, referenced as `$name` in the content.
34
+ * The text is translated once as a template and cached, whatever the
35
+ * values are.
36
+ */
37
+ v: PropType<Variables>;
16
38
  }>> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
17
39
  export declare const T: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
18
40
  lang: PropType<Languages>;
19
41
  formality: PropType<TranslateOptions["formality"]>;
42
+ /**
43
+ * Lets this use of the text be edited on its own in the webapp. Its
44
+ * edited translation stays when the text changes.
45
+ */
46
+ id: StringConstructor;
47
+ /**
48
+ * Values that are not translated, referenced as `$name` in the content.
49
+ * The text is translated once as a template and cached, whatever the
50
+ * values are.
51
+ */
52
+ v: PropType<Variables>;
20
53
  }>, () => VNode<import("vue").RendererNode, import("vue").RendererElement, {
21
54
  [key: string]: any;
22
55
  }>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
23
56
  lang: PropType<Languages>;
24
57
  formality: PropType<TranslateOptions["formality"]>;
58
+ /**
59
+ * Lets this use of the text be edited on its own in the webapp. Its
60
+ * edited translation stays when the text changes.
61
+ */
62
+ id: StringConstructor;
63
+ /**
64
+ * Values that are not translated, referenced as `$name` in the content.
65
+ * The text is translated once as a template and cached, whatever the
66
+ * values are.
67
+ */
68
+ v: PropType<Variables>;
25
69
  }>> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
26
70
  declare module 'vue' {
27
71
  interface GlobalComponents {
@@ -3,6 +3,9 @@ import { TranslateParams } from './magic-translate';
3
3
  * Returns a function that translates plain strings, e.g. for attributes:
4
4
  * `<input :placeholder="t('Search')" />`. It returns the original text until
5
5
  * the translation has loaded, and the component re-renders once it has.
6
+ *
7
+ * Values that should not be translated go in `v` and are referenced as `$name`:
8
+ * `t('Search $count products', { v: { count } })`.
6
9
  */
7
10
  export declare function useTranslate(): (text: string, params?: TranslateParams) => string;
8
11
  export declare const useT: typeof useTranslate;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magic-translate/vue",
3
- "version": "1.0.3",
3
+ "version": "1.2.0",
4
4
  "keywords": [
5
5
  "vue",
6
6
  "vue3",
@@ -48,7 +48,7 @@
48
48
  "vue": ">=3.3.0"
49
49
  },
50
50
  "dependencies": {
51
- "@magic-translate/core": "^1.4.2"
51
+ "@magic-translate/core": "^1.6.0"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@types/node": "^26.6.5",