@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 +42 -1
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +89 -68
- package/dist/index.js.map +1 -1
- package/dist/magic-translate.d.ts +9 -3
- package/dist/translate.d.ts +45 -1
- package/dist/use-translate.d.ts +3 -0
- package/package.json +2 -2
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(
|
|
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
|
package/dist/index.cjs.map
CHANGED
|
@@ -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":"A,2HAoCA,SAASuB,EAAW,EAA8B,CAChD,IAAMC,EAAQC,EAAQ,UAAU,EAAI,EAC9BC,EAAmB,CAAC,EACtBC,EAEEC,EAAS,SAAS,iBACtBJ,EACA,WAAW,aAAe,WAAW,YACvC,EACA,IAAK,IAAIK,EAAOD,EAAO,SAAS,EAAGC,EAAM,EAAOD,EAAO,SAAS,EAAG,CACjE,GAAIC,EAAK,WAAa,KAAK,aAAc,CACvCH,EAAS,KAAKG,CAAI,EAClB,QACF,CAEA,IAAMC,EAAKD,EACX,IAAK,GAAM,CAAE,UAAU,MAAM,KAAKC,EAAG,UAAU,EACzCC,EAAK,WAAW,SAAS,IAC3B,EAAiBA,EACjBD,EAAG,gBAAgBC,CAAI,EAG7B,CACAL,EAAS,QAAS,GAAYM,EAAQ,YAAY,YAAYA,CAAO,CAAC,EAEtE,GAAM,EAAGC,EAAmBC,EAAMC,GAChC,yBAAyB,KAAKX,EAAM,SAAS,EAE/C,MAAO,CAAE,OAAM,oBAAmB,qBAAoB,gBAAe,CACvE,CAEA,SAASY,EAAmB,EAAmB,CAC7C,GAAK,MAAM,QAAQC,CAAQ,EAI3B,IAAK,IAAMC,KAASD,EAAU,CAC5B,GAAI,EAAA,EAACrC,EAAAA,QAAAA,CAAQsC,CAAK,EAAG,CACnBF,EAAmBE,CAAK,EACxB,QACF,CAEA,IAAMC,EAAQD,EACd,GAAIC,EAAM,OAAStC,EAAAA,MAAQsC,EAAM,OAASrC,EAAAA,QAG1C,IAAI,OAAOqC,EAAM,MAAS,UAAY,OAAOA,EAAM,MAAS,WAAY,CACtE,IAAMR,EACHQ,EAAM,KAA4C,MAClDA,EAAM,KAA6B,OACtC,MAAU,MACR,+DACER,EAAO,kDAAkDA,EAAK,GAAK,IAEvE,CACF,CAEAK,EAAmBG,EAAM,QAAQ,CAFjC,CAGF,CACF,CAMA,IAAaC,GAAA,EAAYrC,EAAAA,gBAAAA,CAAgB,CACvC,KAAM,IACN,MAAO,CACL,KAAM,OACN,UAAW,OAKX,GAAI,OAMJ,EAAG,MACL,EACA,MAAM,EAAO,CAAE,SAAS,CACtB,IAAMsC,EAAiBC,EAAkB,EACnCC,GAAA,EAAYvC,EAAAA,IAAAA,CAAiB,EAC7BwC,GAAA,EAAYxC,EAAAA,IAAAA,CAAiB,EAC7ByC,GAAA,EAASxC,EAAAA,WAAAA,CAA0B,IAAI,EACvCyC,GAAA,EAAa1C,EAAAA,IAAAA,CAAI,EAAK,EACxB2C,EAAgB,EAChBC,EACAC,EAEEC,GACJ,EACA,EACA,EAA4B,CAAC,IAC1B,CACH,IAAMC,EAASP,EAAU,MACpBO,IAIL,EAAO,UAAY,GAAGN,EAAO,oBAAoBO,IAAcP,EAAO,qBAClEQ,EAAO,OAAS,IAClB,EAAAhC,EAAA,YAAA,CAAY8B,EAAQE,CAAM,EAExBR,EAAO,gBACTM,EACG,iBAAiB,GAAG,CAAC,CACrB,QAAS,GAAOrB,EAAG,aAAae,EAAO,eAAiB,EAAE,CAAC,EAEhE,EAAW,MAAQ,GACrB,EAoEA,OAlEA,EAAAvB,EAAA,UAAA,KAAgB,CACd,IAAMG,EAAUkB,EAAU,MACpBW,MAAa,CACjB,IAAMC,EAAOhC,EAAWE,CAAO,EACzB+B,EAAUX,EAAO,OAErBU,EAAK,OAASC,GAAS,MACvBD,EAAK,oBAAsBC,EAAQ,mBACnCD,EAAK,qBAAuBC,EAAQ,sBAEpC,EAAO,MAAQD,EAEnB,EACAD,EAAK,EAGL,EAAW,IAAI,iBAAiBA,CAAI,EACpCL,EAAS,QAAQxB,EAAS,CACxB,QAAS,GACT,UAAW,GACX,cAAe,GACf,WAAY,EACd,CAAC,CACH,CAAC,GAED,EAAAH,EAAA,gBAAA,KAAsB2B,GAAU,WAAW,CAAC,GAE5C,EAAA3B,EAAA,MAAA,CACE,CACEuB,MACMY,EAAM,SACNA,EAAM,cACNA,EAAM,OACNA,EAAM,EAEZhB,EAAe,QACjB,GACC,CAACI,EAAQa,EAAMC,EAAWC,EAAIC,KAAe,CAC5C,GAAI,CAAChB,GAAQ,KAAM,CACjB,EAAW,MAAQ,GACnB,MACF,CAEA,GAAM,CAAE,WAAU,SAAQ,UAAWgB,GAAA,EACjCvD,EAAAA,eAAAA,CAAeuC,EAAO,KAAMgB,CAAS,EACrC,CAAE,SAAUhB,EAAO,KAAM,OAAQ,CAAC,EAAG,OAAQ,CAAC,CAAE,EAChDiB,IAAad,IACf,EAAec,GACf,EAAAzC,EAAA,gBAAA,CAAgB0C,CAAM,GAEpBF,GAAa,CAACf,EAAW,OAC3BI,EAAOL,EAAQiB,EAAUT,CAAM,EAIjC,IAAMW,EAAY,EAAEjB,EACpBN,EACG,UAAUqB,EAAU,CAAE,OAAM,YAAW,IAAG,CAAC,CAAC,CAC5C,KAAM,GAAgB,CACjBE,IAAcjB,GAChBG,EAAOL,EAAQO,EAAaC,CAAM,CAEtC,CAAC,CACL,CACF,MAEa,CACX,IAAMhB,EAAW4B,EAAM,UAAU,EAGjC,OAFA7B,EAAmBC,CAAQ,GAE3B,EAAO9B,EAAAA,EAAAA,CAAE,OAAQ,CAAE,MAAO,mBAAoB,EAAG,EAAA,EAC/CA,EAAAA,EAAAA,CACE,OACA,CACE,IAAKoC,EAIL,MAAO,CACL,QAASG,EAAW,OAASW,EAAM,EAAI,OAAS,UAClD,CACF,EACApB,CACF,GAAA,EACA9B,EAAAA,EAAAA,CAAE,OAAQ,CACR,IAAKqC,EACL,MAAO,CAAE,QAASE,EAAW,MAAQ,WAAa,MAAO,CAC3D,CAAC,CACH,CAAC,CACH,CACF,CACF,CAAC,EAEYoB,EAAI1B,EC1LJ2B,EACX,OAAO,iBAAiB,EAM1B,SAAgBC,EACd,EACgB,CAChB,GAAI,CAACC,EAAO,OACV,MAAU,MAAM,sCAAsC7D,EAAAA,gBAAgB,EAGxE,GAAM,CAAE,QAAO,SAAQ,iBAAgB,UAAS,aAAc6D,EACxDC,EAAgBD,EAAO,gBAAkB5D,EAAAA,qBACzC8D,GAAA,EAAW7D,EAAAA,IAAAA,CAAe2D,EAAO,QAAQ,EAEzCG,GAAA,EAAa7D,EAAAA,SAAAA,KAEf,IAAIC,EAAAA,WACF0D,EAAc,CACZ,MAAO,CAAC,CAACG,EACT,SACA,KAAMF,EAAS,MACf,WAAYG,EACZ,OAAQC,EACR,WACF,CAAC,EACD,CAAE,YAAA,EAAY9D,EAAAA,WAAAA,CAAW+D,EAAQF,EAAgBH,EAAS,KAAK,CAAE,CACnE,CACJ,EAEM9B,EAAiC,CACrC,UAAA,EAAU3B,EAAAA,SAAAA,CAASyD,CAAQ,EAC3B,SACA,iBACA,aACA,YAAY,EAAM,CAChB,EAAS,MAAQb,CACnB,EACA,UAAU,EAAM,CAAE,IAAG,GAAGmB,GAAW,CAAC,EAAG,CACrC,GAAI,CAACC,EACH,OAAON,EAAW,MAAM,KAAK,CAAE,OAAM,GAAGK,CAAO,CAAC,EAElD,GAAM,CAAE,WAAU,SAAQ,WAAA,EAAW9D,EAAAA,WAAAA,CAAWgE,EAAMD,CAAC,EAEvD,OADA,EAAAzD,EAAA,gBAAA,CAAgB0C,CAAM,EACfS,EAAW,MACf,KAAK,CAAE,KAAMV,EAAU,GAAGe,CAAO,CAAC,CAAC,CACnC,KAAM,IAAA,EAAgB7D,EAAAA,iBAAAA,CAAiBoC,EAAaC,CAAM,CAAC,CAChE,EACA,SAAS,EAAM,EAAS,CAAC,EAAG,CAC1B,OAAA,EAAOxC,EAAAA,WAAAA,CACL+D,EACAF,EACAH,EAAS,KACX,CAAC,CAAC,CACA,OACA,GAAGM,CACL,CAAC,CACH,EACA,QAAQ,EAAK,CACXG,EAAI,QAAQb,EAAiB1B,CAAc,EAC3CuC,EAAI,UAAU,IAAKxC,CAAS,CAC9B,CACF,EAEA,OAAOC,CACT,CAEA,SAAgBC,GAAoC,CAClD,IAAMD,GAAA,EAAiBxB,EAAAA,OAAAA,CAAOkD,EAAiB,IAAI,EAEnD,GAAI1B,IAAmB,KACrB,MAAU,MACR,gFACF,EAGF,OAAOA,CACT,CAEA,SAAgBwC,GAGd,CACA,GAAM,CAAE,cAAa,YAAavC,EAAkB,EAEpD,MAAO,CAAE,cAAa,gBAAiB6B,CAAS,CAClD,CClIA,SAAgBW,GAGJ,CACV,IAAMzC,EAAiBC,EAAkB,EACnCyC,GAAA,EAAejE,EAAAA,SAAAA,CAAS,IAAI,GAAqB,EACjDkE,EAAU,IAAI,IAEpB,OAAQ,EAAM,CAAE,IAAG,GAAGP,GAAW,CAAC,IAAM,CAEtC,GAAM,CAAE,WAAU,SAAQ,UAAWC,GAAA,EACjC3D,EAAAA,WAAAA,CAAW4D,EAAMD,CAAC,EAClB,CAAE,SAAUC,EAAM,OAAQ,CAAC,EAAG,OAAQ,CAAC,CAAE,EAG7C,GAAI,CAACA,GAAQ,OAAO,OAAW,IAC7B,OAAA,EAAO3D,EAAAA,iBAAAA,CAAiB0C,EAAUT,CAAM,EAI1C,IAAMgC,EAAM5C,EAAe,SAASqB,EAAUe,CAAM,EAC9CzB,EAAc+B,EAAa,IAAIE,CAAG,EAWxC,OATIjC,IAAgB,IAAA,IAAa,CAACgC,EAAQ,IAAIC,CAAG,KAC/C,EAAAhE,EAAA,gBAAA,CAAgB0C,CAAM,EACtBqB,EAAQ,IAAIC,CAAG,EACf5C,EAAe,UAAUqB,EAAUe,CAAM,CAAC,CAAC,KAAM,GAAe,CAC9DM,EAAa,IAAIE,EAAKvC,CAAU,EAChCsC,EAAQ,OAAOC,CAAG,CACpB,CAAC,IAGH,EAAOjE,EAAAA,iBAAAA,CAAiBgC,GAAeU,EAAUT,CAAM,CACzD,CACF,CAEA,IAAaiC,EAAOJ,E"}
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { DataLoader as e, cacheKeyFn as t, defaultLoaderFactory as n,
|
|
2
|
-
import { Comment as
|
|
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
|
|
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
|
|
24
|
+
function w(e) {
|
|
25
25
|
if (Array.isArray(e)) for (let t of e) {
|
|
26
|
-
if (!
|
|
27
|
-
|
|
26
|
+
if (!h(t)) {
|
|
27
|
+
w(t);
|
|
28
28
|
continue;
|
|
29
29
|
}
|
|
30
30
|
let e = t;
|
|
31
|
-
if (e.type !==
|
|
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
|
-
|
|
36
|
+
w(e.children);
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
-
var
|
|
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 =
|
|
48
|
-
let
|
|
49
|
-
|
|
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
|
|
52
|
-
let e =
|
|
53
|
-
let t =
|
|
54
|
-
(t.html !== n?.html || t.leadingWhitespace !== n.leadingWhitespace || t.trailingWhitespace !== n.trailingWhitespace) && (
|
|
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(),
|
|
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
|
-
}),
|
|
63
|
-
|
|
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
|
-
|
|
73
|
+
l.value = !1;
|
|
70
74
|
return;
|
|
71
75
|
}
|
|
72
|
-
let
|
|
73
|
-
|
|
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
|
-
|
|
88
|
+
h === u && m(e, t, f);
|
|
78
89
|
});
|
|
79
90
|
}), () => {
|
|
80
|
-
let
|
|
81
|
-
return
|
|
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:
|
|
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
|
-
}),
|
|
91
|
-
function
|
|
92
|
-
if (!
|
|
93
|
-
let { debug:
|
|
94
|
-
debug: !!
|
|
95
|
-
apiKey:
|
|
96
|
-
lang:
|
|
97
|
-
sourceLang:
|
|
98
|
-
apiUrl:
|
|
99
|
-
formality:
|
|
100
|
-
}), { cacheKeyFn: t(
|
|
101
|
-
language:
|
|
102
|
-
apiKey:
|
|
103
|
-
sourceLanguage:
|
|
104
|
-
dataLoader:
|
|
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
|
-
|
|
117
|
+
h.value = e;
|
|
107
118
|
},
|
|
108
|
-
translate(e, t = {}) {
|
|
109
|
-
return
|
|
119
|
+
translate(e, { v: t, ...n } = {}) {
|
|
120
|
+
if (!t) return g.value.load({
|
|
110
121
|
text: e,
|
|
111
|
-
...
|
|
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(
|
|
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(
|
|
137
|
+
e.provide(D, _), e.component("T", T);
|
|
122
138
|
}
|
|
123
139
|
};
|
|
124
|
-
return
|
|
140
|
+
return _;
|
|
125
141
|
}
|
|
126
|
-
function
|
|
127
|
-
let e =
|
|
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
|
|
132
|
-
let { setLanguage: e, language: t } =
|
|
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
|
|
140
|
-
let e =
|
|
141
|
-
return (r,
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
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
|
|
150
|
-
export {
|
|
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
|
|
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>;
|
package/dist/translate.d.ts
CHANGED
|
@@ -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 {
|
package/dist/use-translate.d.ts
CHANGED
|
@@ -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
|
+
"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.
|
|
51
|
+
"@magic-translate/core": "^1.6.0"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@types/node": "^26.6.5",
|