@magic-translate/vue 1.1.0 → 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
@@ -143,6 +143,17 @@ Both the component and `t` can override the target language and the formality:
143
143
  <input :placeholder="t('Your name', { lang: 'fr', formality: 'formal' })" />
144
144
  ```
145
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
+
146
157
  ### Switching languages
147
158
 
148
159
  `useLanguageSwitcher` reads and changes the current language. Every `<T>`
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,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.v,o.language],([t,n,r,i])=>{if(!t?.html){d.value=!1;return}let{template:a,values:s,unused:c}=i?(0,e.toHtmlTemplate)(t.html,i):{template:t.html,values:[],unused:[]};a!==p&&(p=a,(0,e.warnAboutUnused)(c)),i&&!d.value&&h(t,a,s);let l=++f;o.translate(a,{lang:n,formality:r}).then(e=>{l===f&&h(t,e,s)})}),()=>{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]}})});
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","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","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 * 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.v,\n // a language switch has to re-translate the content\n magicTranslate.language,\n ],\n ([source, lang, formality, 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 })\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
+ {"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
@@ -42,6 +42,7 @@ var T = f({
42
42
  props: {
43
43
  lang: String,
44
44
  formality: String,
45
+ id: String,
45
46
  v: Object
46
47
  },
47
48
  setup(e, { slots: t }) {
@@ -64,25 +65,27 @@ var T = f({
64
65
  s,
65
66
  () => e.lang,
66
67
  () => e.formality,
68
+ () => e.id,
67
69
  () => e.v,
68
70
  n.language
69
- ], ([e, t, r, i]) => {
71
+ ], ([e, t, r, i, a]) => {
70
72
  if (!e?.html) {
71
73
  l.value = !1;
72
74
  return;
73
75
  }
74
- let { template: a, values: s, unused: f } = i ? o(e.html, i) : {
76
+ let { template: s, values: f, unused: p } = a ? o(e.html, a) : {
75
77
  template: e.html,
76
78
  values: [],
77
79
  unused: []
78
80
  };
79
- a !== d && (d = a, c(f)), i && !l.value && m(e, a, s);
80
- let p = ++u;
81
- n.translate(a, {
81
+ s !== d && (d = s, c(p)), a && !l.value && m(e, s, f);
82
+ let h = ++u;
83
+ n.translate(s, {
82
84
  lang: t,
83
- formality: r
85
+ formality: r,
86
+ id: i
84
87
  }).then((t) => {
85
- p === u && m(e, t, s);
88
+ h === u && m(e, t, f);
86
89
  });
87
90
  }), () => {
88
91
  let n = t.default?.();
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","lastTemplate","observer","render","target","translation","values","fillElement","onMounted","read","next","current","onBeforeUnmount","watch","props","lang","formality","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 * 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.v,\n // a language switch has to re-translate the content\n magicTranslate.language,\n ],\n ([source, lang, formality, 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 })\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
+ {"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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"}
@@ -8,6 +8,11 @@ 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;
11
16
  /**
12
17
  * Values that are not translated, referenced as `$name` in the content.
13
18
  * The text is translated once as a template and cached, whatever the
@@ -19,6 +24,11 @@ export declare const Translate: import("vue").DefineComponent<import("vue").Extr
19
24
  }>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
20
25
  lang: PropType<Languages>;
21
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;
22
32
  /**
23
33
  * Values that are not translated, referenced as `$name` in the content.
24
34
  * The text is translated once as a template and cached, whatever the
@@ -29,6 +39,11 @@ export declare const Translate: import("vue").DefineComponent<import("vue").Extr
29
39
  export declare const T: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
30
40
  lang: PropType<Languages>;
31
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;
32
47
  /**
33
48
  * Values that are not translated, referenced as `$name` in the content.
34
49
  * The text is translated once as a template and cached, whatever the
@@ -40,6 +55,11 @@ export declare const T: import("vue").DefineComponent<import("vue").ExtractPropT
40
55
  }>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
41
56
  lang: PropType<Languages>;
42
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;
43
63
  /**
44
64
  * Values that are not translated, referenced as `$name` in the content.
45
65
  * The text is translated once as a template and cached, whatever the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magic-translate/vue",
3
- "version": "1.1.0",
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.5.0"
51
+ "@magic-translate/core": "^1.6.0"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@types/node": "^26.6.5",