diff --git a/package.json b/package.json index a3a39c33..12b2e74d 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "fluent-vue", "type": "module", "version": "3.6.0", - "packageManager": "pnpm@10.3.0", + "packageManager": "pnpm@10.3.0+sha512.ee592eda8815a8a293c206bb0917c4bb0ff274c50def7cbc17be05ec641fc2d1b02490ce660061356bd0d126a4d7eb2ec8830e6959fb8a447571c631d5a2442d", "description": "Internationalization plugin for Vue.js. Project Fluent bindings for Vue.js", "author": "Ivan Demchuk ", "license": "MIT", @@ -30,8 +30,18 @@ "sideEffects": false, "exports": { ".": { - "import": "./dist/index.js", - "require": "./dist/index.cjs" + "import": { + "types": "./dist/index.d.ts", + "production:": "./dist/prod/index.js", + "development:": "./dist/index.js", + "default": "./dist/index.js" + }, + "require": { + "types": "./dist/index.d.cts", + "production:": "./dist/prod/index.cjs", + "development:": "./dist/index.cjs", + "default": "./dist/index.cjs" + } } }, "main": "dist/index.js", @@ -67,6 +77,7 @@ }, "dependencies": { "@fluent/sequence": "^0.8.0", + "@vue/devtools-api": "^7.7.1", "cached-iterable": "^0.3.0", "vue-demi": "latest" }, @@ -87,7 +98,7 @@ "happy-dom": "^17.1.0", "husky": "^9.1.7", "lint-staged": "^15.4.3", - "release-it": "*", + "release-it": "^18.1.2", "rimraf": "^6.0.1", "tsup": "^8.3.6", "typescript": "^5.7.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 31259c50..a481e130 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@fluent/sequence': specifier: ^0.8.0 version: 0.8.0(@fluent/bundle@0.18.0) + '@vue/devtools-api': + specifier: ^7.7.1 + version: 7.7.1 cached-iterable: specifier: ^0.3.0 version: 0.3.0 @@ -67,7 +70,7 @@ importers: specifier: ^15.4.3 version: 15.4.3 release-it: - specifier: '*' + specifier: ^18.1.2 version: 18.1.2(@types/node@22.13.1)(typescript@5.7.3) rimraf: specifier: ^6.0.1 @@ -622,7 +625,6 @@ packages: '@ls-lint/ls-lint@2.2.3': resolution: {integrity: sha512-ekM12jNm/7O2I/hsRv9HvYkRdfrHpiV1epVuI2NP+eTIcEgdIdKkKCs9KgQydu/8R5YXTov9aHdOgplmCHLupw==} - cpu: [x64, arm64, s390x] os: [darwin, linux, win32] hasBin: true @@ -654,8 +656,8 @@ packages: resolution: {integrity: sha512-z+j7DixNnfpdToYsOutStDgeRzJSMnbj8T1C/oQjB6Aa+kRfNjs/Fn7W6c8bmlt6mfy3FkgeKBRnDjxQow5dow==} engines: {node: '>= 18'} - '@octokit/endpoint@10.1.2': - resolution: {integrity: sha512-XybpFv9Ms4hX5OCHMZqyODYqGTZ3H6K6Vva+M9LR7ib/xr1y1ZnlChYv9H680y77Vd/i/k+thXApeRASBQkzhA==} + '@octokit/endpoint@10.1.3': + resolution: {integrity: sha512-nBRBMpKPhQUxCsQQeW+rCJ/OPSMcj3g0nfHn01zGYZXuNDvvXudF/TYY6APj5THlurerpFN4a/dQAIAaM6BYhA==} engines: {node: '>= 18'} '@octokit/graphql@8.2.0': @@ -665,8 +667,8 @@ packages: '@octokit/openapi-types@23.0.1': resolution: {integrity: sha512-izFjMJ1sir0jn0ldEKhZ7xegCTj/ObmEDlEfpFrx4k/JyZSMRHbO3/rBwgE7f3m2DHt+RrNGIVw4wSmwnm3t/g==} - '@octokit/plugin-paginate-rest@11.4.0': - resolution: {integrity: sha512-ttpGck5AYWkwMkMazNCZMqxKqIq1fJBNxBfsFwwfyYKTf914jKkLF0POMS3YkPBwp5g1c2Y4L79gDz01GhSr1g==} + '@octokit/plugin-paginate-rest@11.4.2': + resolution: {integrity: sha512-BXJ7XPCTDXFF+wxcg/zscfgw2O/iDPtNSkwwR1W1W5c4Mb3zav/M2XvxQ23nVmKj7jpweB4g8viMeCQdm7LMVA==} engines: {node: '>= 18'} peerDependencies: '@octokit/core': '>=6' @@ -997,6 +999,15 @@ packages: '@vue/compiler-ssr@3.5.13': resolution: {integrity: sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==} + '@vue/devtools-api@7.7.1': + resolution: {integrity: sha512-Cexc8GimowoDkJ6eNelOPdYIzsu2mgNyp0scOQ3tiaYSb9iok6LOESSsJvHaI+ib3joRfqRJNLkHFjhNuWA5dg==} + + '@vue/devtools-kit@7.7.1': + resolution: {integrity: sha512-yhZ4NPnK/tmxGtLNQxmll90jIIXdb2jAhPF76anvn5M/UkZCiLJy28bYgPIACKZ7FCosyKoaope89/RsFJll1w==} + + '@vue/devtools-shared@7.7.1': + resolution: {integrity: sha512-BtgF7kHq4BHG23Lezc/3W2UhK2ga7a8ohAIAGJMBr4BkxUFzhqntQtCiuL1ijo2ztWnmusymkirgqUrXoQKumA==} + '@vue/reactivity@3.5.13': resolution: {integrity: sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==} @@ -1111,6 +1122,9 @@ packages: before-after-hook@3.0.2: resolution: {integrity: sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==} + birpc@0.2.19: + resolution: {integrity: sha512-5WeXXAvTmitV1RqJFppT5QtUiz2p1mRSYU000Jkft5ZUCLJIk4uQriYNO50HknxKwM6jd8utNc66K1qGIwwWBQ==} + boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} @@ -1293,6 +1307,10 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + copy-anything@3.0.5: + resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==} + engines: {node: '>=12.13'} + core-js-compat@3.40.0: resolution: {integrity: sha512-0XEDpr5y5mijvw8Lbc6E5AkjrHfp7eEoPlu36SWeAbcL8fn1G1ANe8DBlo2XoNN89oVpxWwOjYIPVzR4ZvsKCQ==} @@ -1878,6 +1896,9 @@ packages: highlight.js@10.7.3: resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} @@ -2078,6 +2099,10 @@ packages: resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} engines: {node: '>=18'} + is-what@4.1.16: + resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} + engines: {node: '>=12.13'} + is-wsl@3.1.0: resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} engines: {node: '>=16'} @@ -2581,6 +2606,9 @@ packages: resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} engines: {node: '>= 8'} + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + mkdirp@1.0.4: resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} engines: {node: '>=10'} @@ -2820,6 +2848,9 @@ packages: resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} engines: {node: '>= 14.16'} + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -3160,6 +3191,10 @@ packages: spdx-license-ids@3.0.21: resolution: {integrity: sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==} + speakingurl@14.0.1: + resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} + engines: {node: '>=0.10.0'} + sprintf-js@1.1.3: resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} @@ -3236,6 +3271,10 @@ packages: engines: {node: '>=16 || 14 >=14.17'} hasBin: true + superjson@2.2.2: + resolution: {integrity: sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==} + engines: {node: '>=16'} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -4207,7 +4246,7 @@ snapshots: before-after-hook: 3.0.2 universal-user-agent: 7.0.2 - '@octokit/endpoint@10.1.2': + '@octokit/endpoint@10.1.3': dependencies: '@octokit/types': 13.8.0 universal-user-agent: 7.0.2 @@ -4220,7 +4259,7 @@ snapshots: '@octokit/openapi-types@23.0.1': {} - '@octokit/plugin-paginate-rest@11.4.0(@octokit/core@6.1.3)': + '@octokit/plugin-paginate-rest@11.4.2(@octokit/core@6.1.3)': dependencies: '@octokit/core': 6.1.3 '@octokit/types': 13.8.0 @@ -4240,7 +4279,7 @@ snapshots: '@octokit/request@9.2.0': dependencies: - '@octokit/endpoint': 10.1.2 + '@octokit/endpoint': 10.1.3 '@octokit/request-error': 6.1.6 '@octokit/types': 13.8.0 fast-content-type-parse: 2.0.1 @@ -4249,7 +4288,7 @@ snapshots: '@octokit/rest@21.0.2': dependencies: '@octokit/core': 6.1.3 - '@octokit/plugin-paginate-rest': 11.4.0(@octokit/core@6.1.3) + '@octokit/plugin-paginate-rest': 11.4.2(@octokit/core@6.1.3) '@octokit/plugin-request-log': 5.3.1(@octokit/core@6.1.3) '@octokit/plugin-rest-endpoint-methods': 13.3.1(@octokit/core@6.1.3) @@ -4587,6 +4626,24 @@ snapshots: '@vue/compiler-dom': 3.5.13 '@vue/shared': 3.5.13 + '@vue/devtools-api@7.7.1': + dependencies: + '@vue/devtools-kit': 7.7.1 + + '@vue/devtools-kit@7.7.1': + dependencies: + '@vue/devtools-shared': 7.7.1 + birpc: 0.2.19 + hookable: 5.5.3 + mitt: 3.0.1 + perfect-debounce: 1.0.0 + speakingurl: 14.0.1 + superjson: 2.2.2 + + '@vue/devtools-shared@7.7.1': + dependencies: + rfdc: 1.4.1 + '@vue/reactivity@3.5.13': dependencies: '@vue/shared': 3.5.13 @@ -4697,6 +4754,8 @@ snapshots: before-after-hook@3.0.2: {} + birpc@0.2.19: {} + boolbase@1.0.0: {} boxen@8.0.1: @@ -4883,6 +4942,10 @@ snapshots: convert-source-map@2.0.0: {} + copy-anything@3.0.5: + dependencies: + is-what: 4.1.16 + core-js-compat@3.40.0: dependencies: browserslist: 4.24.4 @@ -5578,6 +5641,8 @@ snapshots: highlight.js@10.7.3: {} + hookable@5.5.3: {} + hosted-git-info@2.8.9: {} hosted-git-info@4.1.0: @@ -5743,6 +5808,8 @@ snapshots: is-unicode-supported@2.1.0: {} + is-what@4.1.16: {} + is-wsl@3.1.0: dependencies: is-inside-container: 1.0.0 @@ -6527,6 +6594,8 @@ snapshots: minipass: 3.3.6 yallist: 4.0.0 + mitt@3.0.1: {} + mkdirp@1.0.4: {} mlly@1.7.4: @@ -6769,6 +6838,8 @@ snapshots: pathval@2.0.0: {} + perfect-debounce@1.0.0: {} + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -7112,6 +7183,8 @@ snapshots: spdx-license-ids@3.0.21: {} + speakingurl@14.0.1: {} + sprintf-js@1.1.3: {} ssri@8.0.1: @@ -7180,6 +7253,10 @@ snapshots: pirates: 4.0.6 ts-interface-checker: 0.1.13 + superjson@2.2.2: + dependencies: + copy-anything: 3.0.5 + supports-color@7.2.0: dependencies: has-flag: 4.0.0 diff --git a/src/devtools/index.ts b/src/devtools/index.ts new file mode 100644 index 00000000..8b817907 --- /dev/null +++ b/src/devtools/index.ts @@ -0,0 +1 @@ +export { registerFluentVueDevtools } from './plugin' diff --git a/src/devtools/plugin.ts b/src/devtools/plugin.ts new file mode 100644 index 00000000..a7533d8e --- /dev/null +++ b/src/devtools/plugin.ts @@ -0,0 +1,314 @@ +import type { FluentResource } from '@fluent/bundle' +import type { FluentVue } from 'src' + +import type { App, ComponentInternalInstance } from 'vue-demi' +import type { ResolvedOptions } from '../types' +import { FluentBundle } from '@fluent/bundle' +import { setupDevToolsPlugin } from '@vue/devtools-api' +import { computed, getCurrentInstance, watchEffect } from 'vue-demi' +import pseudoLocalize from './pseudoLocalize' + +export function registerFluentVueDevtools(app: App, options: ResolvedOptions, fluent: FluentVue) { + let currentPseudoLocalize: ((str: string) => string) | undefined + const missingTranslations: Map> = new Map() + + // Hook into options and app + const oldWarnMissing = options.warnMissing + options.warnMissing = (key) => { + const instance = getCurrentInstance() + + oldWarnMissing(key) + + if (!instance) + return + + missingTranslations.set(instance, missingTranslations.get(instance) ?? new Set()) + missingTranslations.get(instance)!.add(key) + } + + // Hook into bundle tranform + const hookedBundles = new WeakSet() + + watchEffect(() => { + const bundles = fluent.bundles + + for (const bundle of bundles) { + if (hookedBundles.has(bundle)) + continue + + const userTransform = bundle._transform + bundle._transform = (str) => { + if (userTransform != null) + str = userTransform(str) + + if (currentPseudoLocalize != null) + str = currentPseudoLocalize(str) + + return str + } + + hookedBundles.add(bundle) + } + }, { flush: 'sync' }) + + const cleanBundles = computed(() => { + return [...fluent.bundles] + .map((bundle) => { + const newBundle = new FluentBundle(bundle.locales, { + functions: bundle._functions, + useIsolating: bundle._useIsolating, + }) + newBundle._terms = bundle._terms + newBundle._messages = bundle._messages + return newBundle + }) + }) + + setupDevToolsPlugin({ + id: 'fluent-vue', + label: 'fluent-vue', + packageName: 'fluent-vue', + homepage: 'https://fluent-vue.demivan.me', + logo: 'https://fluent-vue.demivan.me/assets/logo.svg', + componentStateTypes: ['fluent-vue'], + app, + settings: { + components: { + label: 'Components', + } as any, // Use option as a header + showLocalized: { + defaultValue: true, + label: 'Mark localized', + description: 'Mark localized components in component tree', + type: 'boolean', + }, + markMissing: { + defaultValue: true, + label: 'Mark missing', + description: 'Mark missing translations in component tree', + type: 'boolean', + }, + showI18n: { + defaultValue: true, + label: 'Mark i18n', + description: 'Mark i18n components in component tree', + type: 'boolean', + }, + pseudo: { + label: 'Pseudolocalization', + } as any, // Use option as a header + pseudoEnable: { + defaultValue: false, + label: 'Enable', + description: 'Enable pseudolocalization', + type: 'boolean', + }, + pseudoAccents: { + defaultValue: true, + label: 'Accents', + description: 'Enable pseudolocalization accents', + type: 'boolean', + }, + pseudoPrefix: { + label: 'Prefix', + type: 'text', + description: 'Prefix to wrap translation', + defaultValue: '[', + }, + pseudoSuffix: { + label: 'Suffix', + type: 'text', + description: 'Suffix to wrap translation', + defaultValue: ']', + }, + }, + }, (api) => { + api.on.visitComponentTree(({ treeNode, componentInstance }) => { + const settings = api.getSettings() + + if (settings.showI18n && treeNode.name === options.componentName) { + treeNode.tags.push({ + label: 'fluent-vue', + textColor: 0x000000, + backgroundColor: 0x41B883, + }) + } + + if (settings.showLocalized && componentInstance?.proxy?.$options.fluent != null) { + treeNode.tags.push({ + label: 'localized', + textColor: 0x000000, + backgroundColor: 0x41B883, + }) + } + + const missing = missingTranslations.get(componentInstance) + if (settings.markMissing && missing != null && missing.size > 0) { + treeNode.tags.push({ + label: 'missing translations', + textColor: 0xFFFFFF, + backgroundColor: 0xB00020, + }) + } + }) + + api.on.inspectComponent(({ componentInstance, instanceData }) => { + const missing = missingTranslations.get(componentInstance) + if (missing) { + for (const key of missing.values()) { + instanceData.state.push({ + type: 'Missing translations', + key, + editable: false, + value: { + _custom: { + type: 'custom', + display: 'Missing', + }, + }, + }) + } + } + + const componentFluent = componentInstance?.proxy?.$options.fluent as Record + if (!componentFluent) + return + + const bundles = cleanBundles.value + for (const [locale, messages] of Object.entries(componentFluent)) { + const bundle = bundles.find(bundle => bundle.locales.includes(locale)) + + if (bundle == null) + continue + + for (const message of messages.body) { + const overridesGlobal = bundle.hasMessage(message.id) + + instanceData.state.push({ + type: `Component translations (${locale})`, + key: message.id, + editable: false, + value: { + _custom: { + type: 'custom', + display: + (message.value ? bundle.formatPattern(message.value, {}, []) : '') + + (overridesGlobal ? ' Global' : ''), + }, + }, + }) + } + } + }) + + api.on.getInspectorTree((payload) => { + if (payload.inspectorId === 'fluent-vue-inspector') { + payload.rootNodes = [ + { + id: 'global', + label: 'Global translations', + }, + { + id: 'missing', + label: 'Missing translations', + }, + ] + } + }) + + const highlightedComponents = new Set() + + api.on.getInspectorState(async (payload) => { + if (payload.inspectorId === 'fluent-vue-inspector') { + if (payload.nodeId === 'global') { + payload.state = {} + for (const bundle of cleanBundles.value) { + payload.state[bundle.locales.join(',')] = [...bundle._messages.entries()].map(([_, message]) => ({ + key: message.id, + value: message.value ? bundle.formatPattern(message.value, {}, []) : '', + })) + } + } + + if (payload.nodeId === 'missing') { + payload.state = {} + for (const bundle of cleanBundles.value) { + const locale = bundle.locales.join(',') + + for (const [component, translations] of missingTranslations.entries()) { + for (const key of translations) { + payload.state[locale] = payload.state[locale] ?? [] + payload.state[locale].push({ + key, + editable: true, + value: { + _custom: { + type: 'component', + value: highlightedComponents.has(component), + display: await api.getComponentName(component), + }, + }, + }) + } + } + } + } + } + }) + + api.on.editInspectorState((payload) => { + if (payload.inspectorId === 'fluent-vue-inspector') { + if (payload.nodeId === 'missing') { + const key = payload.path[0] + const [component] = missingTranslations.entries() + .find(([_, translations]) => translations.has(key)) + ?? [] + + if (component != null) { + if (payload.state.value) { + highlightedComponents.add(component) + api.highlightElement(component) + } + else { + highlightedComponents.delete(component) + api.unhighlightElement() + } + } + } + } + }) + + function handleSettingsChange(settings: { + pseudoEnable: boolean + pseudoType: string + pseudoPrefix: string + pseudoSuffix: string + pseudoAccents: boolean + }) { + if (settings.pseudoEnable) { + currentPseudoLocalize = str => pseudoLocalize(str, { + prefix: settings.pseudoPrefix ?? undefined, + suffix: settings.pseudoSuffix ?? undefined, + accents: settings.pseudoAccents ?? false, + }) + } + else { + currentPseudoLocalize = undefined + } + + // Force update app as if bundles changed + fluent.bundles = [...fluent.bundles] + } + + api.on.setPluginSettings((payload) => { + handleSettingsChange(payload.settings) + }) + + handleSettingsChange(api.getSettings()) + + api.addInspector({ + id: 'fluent-vue-inspector', + label: 'fluent-vue', + }) + }) +} diff --git a/src/devtools/pseudoLocalize.ts b/src/devtools/pseudoLocalize.ts new file mode 100644 index 00000000..7b8ecc9d --- /dev/null +++ b/src/devtools/pseudoLocalize.ts @@ -0,0 +1,29 @@ +const ASCII = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' +const ACCENTED_ASCII = 'âḃćḋèḟĝḫíĵǩĺṁńŏṗɋŕśṭůṿẘẋẏẓḀḂḈḊḔḞḠḢḬĴḴĻḾŊÕṔɊŔṠṮŨṼẄẌŸƵ' + +interface PseudolocalizationOptions { + prefix: string + suffix: string + accents: boolean +} + +export default function pseudoLocalize(str: string, options: PseudolocalizationOptions): string { + let finalString = '' + + if (options.prefix) + finalString += options.prefix + + for (let i = 0; i < str.length; i++) { + const character = str[i] + const convertedCharacter = options.accents + ? ACCENTED_ASCII[ASCII.indexOf(character)] ?? character + : character + + finalString += convertedCharacter + } + + if (options.suffix) + finalString += options.suffix + + return finalString +} diff --git a/src/index.ts b/src/index.ts index 2936d87a..6dca610f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,8 +2,10 @@ import type { FluentBundle, FluentResource, FluentVariable } from '@fluent/bundl import type { TranslationWithAttrs } from './TranslationContext' import type { FluentVueOptions } from './types' import type { InstallFunction, Vue2, Vue3, Vue3Component } from './types/typesCompat' - import { isVue3, shallowRef } from 'vue-demi' + +import { registerFluentVueDevtools } from './devtools' + import { getContext, getMergedContext } from './getContext' import { RootContextSymbol } from './symbols' import { TranslationContext } from './TranslationContext' @@ -67,6 +69,10 @@ export function createFluentVue(options: FluentVueOptions): FluentVue { if (isVue3) { const vue3 = vue as Vue3 + // eslint-disable-next-line node/prefer-global/process + if (process.env.NODE_ENV !== 'production') + registerFluentVueDevtools(vue3, resolvedOptions, this) + vue3.provide(RootContextSymbol, rootContext) vue3.config.globalProperties[resolvedOptions.globalFormatName] = function ( diff --git a/tsup.config.ts b/tsup.config.ts index 46701e65..ff0f2f46 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -1,19 +1,37 @@ +import type { Options } from 'tsup' import GlobalsPlugin from 'esbuild-plugin-globals' import { defineConfig } from 'tsup' -export default defineConfig({ - target: 'node12', - globalName: 'FluentVue', - splitting: true, - esbuildPlugins: [ - GlobalsPlugin({ - 'vue-demi': 'VueDemi', - '@fluent/bundle': 'FluentBundle', - }), - ], - external: ['vue-demi', '@fluent/bundle'], - entry: ['src/index.ts', 'src/composition.ts'], - format: ['esm', 'cjs', 'iife'], - dts: true, - clean: true, -}) +function getConfig(overrides: Partial = {}): Options { + return { + target: 'node16', + globalName: 'FluentVue', + splitting: true, + esbuildPlugins: [ + GlobalsPlugin({ + 'vue-demi': 'VueDemi', + '@fluent/bundle': 'FluentBundle', + }), + ], + external: ['vue-demi', '@fluent/bundle', '@vue/devtools-api'], + entry: ['src/index.ts', 'src/composition.ts'], + format: ['esm', 'cjs', 'iife'], + ...overrides, + } +} + +export default defineConfig([ + getConfig({ + clean: true, + dts: true, + outDir: 'dist', + }), + getConfig({ + clean: false, + dts: false, + env: { + NODE_ENV: 'production', + }, + outDir: 'dist/prod', + }), +])