From 0aca8fed165d0d053fb9c2f2e75be45393588840 Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Wed, 21 Feb 2024 16:50:55 +0100 Subject: [PATCH 01/23] 10.0.0 --- package-lock.json | 4 +- package.json | 331 ++++++++++++++++++++++++++-------------------- 2 files changed, 186 insertions(+), 149 deletions(-) diff --git a/package-lock.json b/package-lock.json index 70055ee9..998b49dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "vscode-front-matter-beta", - "version": "9.5.0", + "version": "10.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "vscode-front-matter-beta", - "version": "9.5.0", + "version": "10.0.0", "license": "MIT", "dependencies": { "@radix-ui/react-dropdown-menu": "^2.0.6" diff --git a/package.json b/package.json index 2e628580..55fea796 100644 --- a/package.json +++ b/package.json @@ -3,14 +3,15 @@ "displayName": "Front Matter CMS", "description": "Front Matter is a CMS that runs within Visual Studio Code. It gives you the power and control of a full-blown CMS while also providing you the flexibility and speed of the static site generator of your choice like: Hugo, Jekyll, Docusaurus, NextJs, Gatsby, and many more...", "icon": "assets/frontmatter-teal-128x128.png", - "version": "9.5.0", + "version": "10.0.0", "preview": false, "publisher": "eliostruyf", "galleryBanner": { "color": "#0e131f", "theme": "dark" }, - "badges": [{ + "badges": [ + { "description": "version", "url": "https://img.shields.io/github/package-json/v/estruyf/vscode-front-matter?color=green&label=vscode-front-matter&style=flat-square", "href": "https://github.com/estruyf/vscode-front-matter" @@ -70,7 +71,8 @@ "**/.frontmatter/config/*.json": "jsonc" } }, - "keybindings": [{ + "keybindings": [ + { "command": "frontMatter.dashboard", "key": "alt+d" }, @@ -88,19 +90,23 @@ } ], "viewsContainers": { - "activitybar": [{ - "id": "frontmatter-explorer", - "title": "FM", - "icon": "$(fm-logo)" - }] + "activitybar": [ + { + "id": "frontmatter-explorer", + "title": "FM", + "icon": "$(fm-logo)" + } + ] }, "views": { - "frontmatter-explorer": [{ - "id": "frontMatter.explorer", - "name": "Front Matter", - "icon": "$(fm-logo)", - "type": "webview" - }] + "frontmatter-explorer": [ + { + "id": "frontMatter.explorer", + "name": "Front Matter", + "icon": "$(fm-logo)", + "type": "webview" + } + ] }, "configuration": { "title": "%settings.configuration.title%", @@ -168,7 +174,8 @@ "frontMatter.content.defaultFileType": { "type": "string", "default": "md", - "oneOf": [{ + "oneOf": [ + { "enum": [ "md", "mdx" @@ -184,7 +191,8 @@ "frontMatter.content.defaultSorting": { "type": "string", "default": "", - "oneOf": [{ + "oneOf": [ + { "enum": [ "LastModifiedAsc", "LastModifiedDesc", @@ -531,7 +539,8 @@ "categories" ], "markdownDescription": "%setting.frontMatter.content.filters.markdownDescription%", - "items": [{ + "items": [ + { "type": "string" }, { @@ -599,7 +608,8 @@ "command": { "$id": "#scriptCommand", "type": "string", - "anyOf": [{ + "anyOf": [ + { "enum": [ "node", "bash", @@ -806,7 +816,8 @@ "title", "file" ], - "anyOf": [{ + "anyOf": [ + { "required": [ "schema" ] @@ -860,7 +871,8 @@ "id", "path" ], - "anyOf": [{ + "anyOf": [ + { "required": [ "schema" ] @@ -1101,26 +1113,29 @@ } } }, - "default": [{ - "name": "default", - "fileTypes": null, - "fields": [{ - "title": "Title", - "name": "title", - "type": "string" - }, - { - "title": "Caption", - "name": "caption", - "type": "string" - }, - { - "title": "Alt text", - "name": "alt", - "type": "string" - } - ] - }], + "default": [ + { + "name": "default", + "fileTypes": null, + "fields": [ + { + "title": "Title", + "name": "title", + "type": "string" + }, + { + "title": "Caption", + "name": "caption", + "type": "string" + }, + { + "title": "Alt text", + "name": "alt", + "type": "string" + } + ] + } + ], "scope": "Media" }, "frontMatter.media.supportedMimeTypes": { @@ -1350,7 +1365,8 @@ "default": "", "description": "%setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.taxonomyId.description%", "not": { - "anyOf": [{ + "anyOf": [ + { "const": "" }, { @@ -1544,7 +1560,8 @@ "type", "name" ], - "allOf": [{ + "allOf": [ + { "if": { "properties": { "type": { @@ -1752,48 +1769,51 @@ "fields" ] }, - "default": [{ - "name": "default", - "pageBundle": false, - "fields": [{ - "title": "Title", - "name": "title", - "type": "string" - }, - { - "title": "Description", - "name": "description", - "type": "string" - }, - { - "title": "Publishing date", - "name": "date", - "type": "datetime", - "default": "{{now}}", - "isPublishDate": true - }, - { - "title": "Content preview", - "name": "preview", - "type": "image" - }, - { - "title": "Is in draft", - "name": "draft", - "type": "boolean" - }, - { - "title": "Tags", - "name": "tags", - "type": "tags" - }, - { - "title": "Categories", - "name": "categories", - "type": "categories" - } - ] - }], + "default": [ + { + "name": "default", + "pageBundle": false, + "fields": [ + { + "title": "Title", + "name": "title", + "type": "string" + }, + { + "title": "Description", + "name": "description", + "type": "string" + }, + { + "title": "Publishing date", + "name": "date", + "type": "datetime", + "default": "{{now}}", + "isPublishDate": true + }, + { + "title": "Content preview", + "name": "preview", + "type": "image" + }, + { + "title": "Is in draft", + "name": "draft", + "type": "boolean" + }, + { + "title": "Tags", + "name": "tags", + "type": "tags" + }, + { + "title": "Categories", + "name": "categories", + "type": "categories" + } + ] + } + ], "scope": "Taxonomy" }, "frontMatter.taxonomy.customTaxonomy": { @@ -1806,7 +1826,8 @@ "type": "string", "description": "%setting.frontMatter.taxonomy.customTaxonomy.items.properties.id.description%", "not": { - "anyOf": [{ + "anyOf": [ + { "const": "" }, { @@ -2003,7 +2024,8 @@ } } }, - "commands": [{ + "commands": [ + { "command": "frontMatter.project.switch", "title": "%command.frontMatter.project.switch%", "category": "Front Matter", @@ -2329,16 +2351,21 @@ } } ], - "submenus": [{ - "id": "frontmatter.submenu", - "label": "Front Matter" - }], + "submenus": [ + { + "id": "frontmatter.submenu", + "label": "Front Matter" + } + ], "menus": { - "webview/context": [{ - "command": "workbench.action.webview.openDeveloperTools", - "when": "frontMatter:isDevelopment" - }], - "editor/title": [{ + "webview/context": [ + { + "command": "workbench.action.webview.openDeveloperTools", + "when": "frontMatter:isDevelopment" + } + ], + "editor/title": [ + { "command": "frontMatter.markup.heading", "group": "navigation@-133", "when": "frontMatter:file:isValid == true && frontMatter:markdown:wysiwyg" @@ -2424,11 +2451,14 @@ "when": "resourceFilename == 'frontmatter.json'" } ], - "explorer/context": [{ - "submenu": "frontmatter.submenu", - "group": "frontmatter@1" - }], - "frontmatter.submenu": [{ + "explorer/context": [ + { + "submenu": "frontmatter.submenu", + "group": "frontmatter@1" + } + ], + "frontmatter.submenu": [ + { "command": "frontMatter.createFromTemplate", "when": "explorerResourceIsFolder", "group": "frontmatter@1" @@ -2444,7 +2474,8 @@ "group": "frontmatter@3" } ], - "commandPalette": [{ + "commandPalette": [ + { "command": "frontMatter.init", "when": "frontMatterCanInit" }, @@ -2597,7 +2628,8 @@ "when": "frontMatter:file:isValid == true" } ], - "view/title": [{ + "view/title": [ + { "command": "frontMatter.chatbot", "group": "navigation@0", "when": "view == frontMatter.explorer" @@ -2629,52 +2661,57 @@ } ] }, - "grammars": [{ - "path": "./syntaxes/hugo.tmLanguage.json", - "scopeName": "frontmatter.markdown.hugo", - "injectTo": [ - "text.html.markdown" - ] - }], - "walkthroughs": [{ - "id": "frontmatter.welcome", - "title": "Get started with Front Matter", - "description": "Discover the features of Front Matter and learn how to use the CMS for your SSG or static site.", - "steps": [{ - "id": "frontmatter.welcome.init", - "title": "Get started", - "description": "Initial steps to get started.\n[Open dashboard](command:frontMatter.dashboard)", - "media": { - "markdown": "assets/walkthrough/get-started.md" + "grammars": [ + { + "path": "./syntaxes/hugo.tmLanguage.json", + "scopeName": "frontmatter.markdown.hugo", + "injectTo": [ + "text.html.markdown" + ] + } + ], + "walkthroughs": [ + { + "id": "frontmatter.welcome", + "title": "Get started with Front Matter", + "description": "Discover the features of Front Matter and learn how to use the CMS for your SSG or static site.", + "steps": [ + { + "id": "frontmatter.welcome.init", + "title": "Get started", + "description": "Initial steps to get started.\n[Open dashboard](command:frontMatter.dashboard)", + "media": { + "markdown": "assets/walkthrough/get-started.md" + }, + "completionEvents": [ + "onContext:frontMatterInitialized" + ] }, - "completionEvents": [ - "onContext:frontMatterInitialized" - ] - }, - { - "id": "frontmatter.welcome.documentation", - "title": "Documentation", - "description": "Check out the documentation for Front Matter.\n[View our documentation](https://frontmatter.codes/docs)", - "media": { - "markdown": "assets/walkthrough/documentation.md" + { + "id": "frontmatter.welcome.documentation", + "title": "Documentation", + "description": "Check out the documentation for Front Matter.\n[View our documentation](https://frontmatter.codes/docs)", + "media": { + "markdown": "assets/walkthrough/documentation.md" + }, + "completionEvents": [ + "onLink:https://frontmatter.codes/docs" + ] }, - "completionEvents": [ - "onLink:https://frontmatter.codes/docs" - ] - }, - { - "id": "frontmatter.welcome.supporter", - "title": "Support the project", - "description": "Become a supporter.\n[Support the project](https://github.com/sponsors/estruyf)", - "media": { - "markdown": "assets/walkthrough/support-the-project.md" - }, - "completionEvents": [ - "onLink:https://github.com/sponsors/estruyf" - ] - } - ] - }] + { + "id": "frontmatter.welcome.supporter", + "title": "Support the project", + "description": "Become a supporter.\n[Support the project](https://github.com/sponsors/estruyf)", + "media": { + "markdown": "assets/walkthrough/support-the-project.md" + }, + "completionEvents": [ + "onLink:https://github.com/sponsors/estruyf" + ] + } + ] + } + ] }, "scripts": { "dev:ext": "npm run clean && npm run localization:generate && npm-run-all --parallel watch:*", @@ -2803,4 +2840,4 @@ "dependencies": { "@radix-ui/react-dropdown-menu": "^2.0.6" } -} \ No newline at end of file +} From 03bc7e72fdb6d3a338cd03d6108605ac5ae2dcf3 Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Wed, 21 Feb 2024 16:57:53 +0100 Subject: [PATCH 02/23] Update version --- CHANGELOG.md | 2 +- README.beta.md | 6 + README.md | 6 + package.json | 329 ++++++++++++++++++++++--------------------------- 4 files changed, 159 insertions(+), 184 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ec33d09..72662878 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Change Log -## [9.5.0] - 2024-xx-xx +## [10.0.0] - 2024-xx-xx ### ✨ New features diff --git a/README.beta.md b/README.beta.md index ec78691a..5b6ace70 100644 --- a/README.beta.md +++ b/README.beta.md @@ -54,6 +54,12 @@ A couple of our extension highlights that hopefully get you interested in giving > If you see something missing in your article creation flow, please feel free to reach out. +**Version 10** + +In version 10, we introduced the new i18n/multilingual support for your content. You can now manage your content in multiple languages, more information can be found in the [multilingual](https://frontmatter.codes/docs/content-creation/multilingual) section of our documentation. + +![Multilingual support](https://beta.frontmatter.codes/releases/v10.0.0/multilingual-content.png) + **Version 9** The extension is now available in multiple languages: English, German, and Japanese. Want to add your language? Check out the [localization the extension](https://frontmatter.codes/docs/contributing#translating-the-extension). diff --git a/README.md b/README.md index 151bde79..73ce1070 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,12 @@ A couple of our extension highlights that hopefully get you interested in giving > If you see something missing in your article creation flow, please feel free to reach out. +**Version 10** + +In version 10, we introduced the new i18n/multilingual support for your content. You can now manage your content in multiple languages, more information can be found in the [multilingual](https://frontmatter.codes/docs/content-creation/multilingual) section of our documentation. + +![Multilingual support](https://beta.frontmatter.codes/releases/v10.0.0/multilingual-content.png) + **Version 9** The extension is now available in multiple languages: English, German, and Japanese. Want to add your language? Check out the [localization the extension](https://frontmatter.codes/docs/contributing#translating-the-extension). diff --git a/package.json b/package.json index 55fea796..1139039b 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,7 @@ "color": "#0e131f", "theme": "dark" }, - "badges": [ - { + "badges": [{ "description": "version", "url": "https://img.shields.io/github/package-json/v/estruyf/vscode-front-matter?color=green&label=vscode-front-matter&style=flat-square", "href": "https://github.com/estruyf/vscode-front-matter" @@ -71,8 +70,7 @@ "**/.frontmatter/config/*.json": "jsonc" } }, - "keybindings": [ - { + "keybindings": [{ "command": "frontMatter.dashboard", "key": "alt+d" }, @@ -90,23 +88,19 @@ } ], "viewsContainers": { - "activitybar": [ - { - "id": "frontmatter-explorer", - "title": "FM", - "icon": "$(fm-logo)" - } - ] + "activitybar": [{ + "id": "frontmatter-explorer", + "title": "FM", + "icon": "$(fm-logo)" + }] }, "views": { - "frontmatter-explorer": [ - { - "id": "frontMatter.explorer", - "name": "Front Matter", - "icon": "$(fm-logo)", - "type": "webview" - } - ] + "frontmatter-explorer": [{ + "id": "frontMatter.explorer", + "name": "Front Matter", + "icon": "$(fm-logo)", + "type": "webview" + }] }, "configuration": { "title": "%settings.configuration.title%", @@ -174,8 +168,7 @@ "frontMatter.content.defaultFileType": { "type": "string", "default": "md", - "oneOf": [ - { + "oneOf": [{ "enum": [ "md", "mdx" @@ -191,8 +184,7 @@ "frontMatter.content.defaultSorting": { "type": "string", "default": "", - "oneOf": [ - { + "oneOf": [{ "enum": [ "LastModifiedAsc", "LastModifiedDesc", @@ -539,8 +531,7 @@ "categories" ], "markdownDescription": "%setting.frontMatter.content.filters.markdownDescription%", - "items": [ - { + "items": [{ "type": "string" }, { @@ -608,8 +599,7 @@ "command": { "$id": "#scriptCommand", "type": "string", - "anyOf": [ - { + "anyOf": [{ "enum": [ "node", "bash", @@ -816,8 +806,7 @@ "title", "file" ], - "anyOf": [ - { + "anyOf": [{ "required": [ "schema" ] @@ -871,8 +860,7 @@ "id", "path" ], - "anyOf": [ - { + "anyOf": [{ "required": [ "schema" ] @@ -1113,29 +1101,26 @@ } } }, - "default": [ - { - "name": "default", - "fileTypes": null, - "fields": [ - { - "title": "Title", - "name": "title", - "type": "string" - }, - { - "title": "Caption", - "name": "caption", - "type": "string" - }, - { - "title": "Alt text", - "name": "alt", - "type": "string" - } - ] - } - ], + "default": [{ + "name": "default", + "fileTypes": null, + "fields": [{ + "title": "Title", + "name": "title", + "type": "string" + }, + { + "title": "Caption", + "name": "caption", + "type": "string" + }, + { + "title": "Alt text", + "name": "alt", + "type": "string" + } + ] + }], "scope": "Media" }, "frontMatter.media.supportedMimeTypes": { @@ -1365,8 +1350,7 @@ "default": "", "description": "%setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.taxonomyId.description%", "not": { - "anyOf": [ - { + "anyOf": [{ "const": "" }, { @@ -1560,8 +1544,7 @@ "type", "name" ], - "allOf": [ - { + "allOf": [{ "if": { "properties": { "type": { @@ -1769,51 +1752,48 @@ "fields" ] }, - "default": [ - { - "name": "default", - "pageBundle": false, - "fields": [ - { - "title": "Title", - "name": "title", - "type": "string" - }, - { - "title": "Description", - "name": "description", - "type": "string" - }, - { - "title": "Publishing date", - "name": "date", - "type": "datetime", - "default": "{{now}}", - "isPublishDate": true - }, - { - "title": "Content preview", - "name": "preview", - "type": "image" - }, - { - "title": "Is in draft", - "name": "draft", - "type": "boolean" - }, - { - "title": "Tags", - "name": "tags", - "type": "tags" - }, - { - "title": "Categories", - "name": "categories", - "type": "categories" - } - ] - } - ], + "default": [{ + "name": "default", + "pageBundle": false, + "fields": [{ + "title": "Title", + "name": "title", + "type": "string" + }, + { + "title": "Description", + "name": "description", + "type": "string" + }, + { + "title": "Publishing date", + "name": "date", + "type": "datetime", + "default": "{{now}}", + "isPublishDate": true + }, + { + "title": "Content preview", + "name": "preview", + "type": "image" + }, + { + "title": "Is in draft", + "name": "draft", + "type": "boolean" + }, + { + "title": "Tags", + "name": "tags", + "type": "tags" + }, + { + "title": "Categories", + "name": "categories", + "type": "categories" + } + ] + }], "scope": "Taxonomy" }, "frontMatter.taxonomy.customTaxonomy": { @@ -1826,8 +1806,7 @@ "type": "string", "description": "%setting.frontMatter.taxonomy.customTaxonomy.items.properties.id.description%", "not": { - "anyOf": [ - { + "anyOf": [{ "const": "" }, { @@ -2024,8 +2003,7 @@ } } }, - "commands": [ - { + "commands": [{ "command": "frontMatter.project.switch", "title": "%command.frontMatter.project.switch%", "category": "Front Matter", @@ -2351,21 +2329,16 @@ } } ], - "submenus": [ - { - "id": "frontmatter.submenu", - "label": "Front Matter" - } - ], + "submenus": [{ + "id": "frontmatter.submenu", + "label": "Front Matter" + }], "menus": { - "webview/context": [ - { - "command": "workbench.action.webview.openDeveloperTools", - "when": "frontMatter:isDevelopment" - } - ], - "editor/title": [ - { + "webview/context": [{ + "command": "workbench.action.webview.openDeveloperTools", + "when": "frontMatter:isDevelopment" + }], + "editor/title": [{ "command": "frontMatter.markup.heading", "group": "navigation@-133", "when": "frontMatter:file:isValid == true && frontMatter:markdown:wysiwyg" @@ -2451,14 +2424,11 @@ "when": "resourceFilename == 'frontmatter.json'" } ], - "explorer/context": [ - { - "submenu": "frontmatter.submenu", - "group": "frontmatter@1" - } - ], - "frontmatter.submenu": [ - { + "explorer/context": [{ + "submenu": "frontmatter.submenu", + "group": "frontmatter@1" + }], + "frontmatter.submenu": [{ "command": "frontMatter.createFromTemplate", "when": "explorerResourceIsFolder", "group": "frontmatter@1" @@ -2474,8 +2444,7 @@ "group": "frontmatter@3" } ], - "commandPalette": [ - { + "commandPalette": [{ "command": "frontMatter.init", "when": "frontMatterCanInit" }, @@ -2628,8 +2597,7 @@ "when": "frontMatter:file:isValid == true" } ], - "view/title": [ - { + "view/title": [{ "command": "frontMatter.chatbot", "group": "navigation@0", "when": "view == frontMatter.explorer" @@ -2661,57 +2629,52 @@ } ] }, - "grammars": [ - { - "path": "./syntaxes/hugo.tmLanguage.json", - "scopeName": "frontmatter.markdown.hugo", - "injectTo": [ - "text.html.markdown" - ] - } - ], - "walkthroughs": [ - { - "id": "frontmatter.welcome", - "title": "Get started with Front Matter", - "description": "Discover the features of Front Matter and learn how to use the CMS for your SSG or static site.", - "steps": [ - { - "id": "frontmatter.welcome.init", - "title": "Get started", - "description": "Initial steps to get started.\n[Open dashboard](command:frontMatter.dashboard)", - "media": { - "markdown": "assets/walkthrough/get-started.md" - }, - "completionEvents": [ - "onContext:frontMatterInitialized" - ] + "grammars": [{ + "path": "./syntaxes/hugo.tmLanguage.json", + "scopeName": "frontmatter.markdown.hugo", + "injectTo": [ + "text.html.markdown" + ] + }], + "walkthroughs": [{ + "id": "frontmatter.welcome", + "title": "Get started with Front Matter", + "description": "Discover the features of Front Matter and learn how to use the CMS for your SSG or static site.", + "steps": [{ + "id": "frontmatter.welcome.init", + "title": "Get started", + "description": "Initial steps to get started.\n[Open dashboard](command:frontMatter.dashboard)", + "media": { + "markdown": "assets/walkthrough/get-started.md" }, - { - "id": "frontmatter.welcome.documentation", - "title": "Documentation", - "description": "Check out the documentation for Front Matter.\n[View our documentation](https://frontmatter.codes/docs)", - "media": { - "markdown": "assets/walkthrough/documentation.md" - }, - "completionEvents": [ - "onLink:https://frontmatter.codes/docs" - ] + "completionEvents": [ + "onContext:frontMatterInitialized" + ] + }, + { + "id": "frontmatter.welcome.documentation", + "title": "Documentation", + "description": "Check out the documentation for Front Matter.\n[View our documentation](https://frontmatter.codes/docs)", + "media": { + "markdown": "assets/walkthrough/documentation.md" }, - { - "id": "frontmatter.welcome.supporter", - "title": "Support the project", - "description": "Become a supporter.\n[Support the project](https://github.com/sponsors/estruyf)", - "media": { - "markdown": "assets/walkthrough/support-the-project.md" - }, - "completionEvents": [ - "onLink:https://github.com/sponsors/estruyf" - ] - } - ] - } - ] + "completionEvents": [ + "onLink:https://frontmatter.codes/docs" + ] + }, + { + "id": "frontmatter.welcome.supporter", + "title": "Support the project", + "description": "Become a supporter.\n[Support the project](https://github.com/sponsors/estruyf)", + "media": { + "markdown": "assets/walkthrough/support-the-project.md" + }, + "completionEvents": [ + "onLink:https://github.com/sponsors/estruyf" + ] + } + ] + }] }, "scripts": { "dev:ext": "npm run clean && npm run localization:generate && npm-run-all --parallel watch:*", @@ -2840,4 +2803,4 @@ "dependencies": { "@radix-ui/react-dropdown-menu": "^2.0.6" } -} +} \ No newline at end of file From 241e660694139ffbc56f16098cdd12cfae9338c1 Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Thu, 22 Feb 2024 09:52:59 +0100 Subject: [PATCH 03/23] #756: Support for creating translation from any page + change path logic --- CHANGELOG.md | 1 + l10n/bundle.l10n.json | 4 +- package.json | 7 +- src/commands/Folders.ts | 81 ++++++++++------- src/commands/StatusListener.ts | 6 +- src/commands/i18n.ts | 91 ++++++++++++++----- src/constants/context.ts | 2 +- .../components/Contents/ContentActions.tsx | 2 +- src/localization/localization.enum.ts | 12 ++- src/models/ContentFolder.ts | 3 + 10 files changed, 144 insertions(+), 65 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72662878..05531c40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - [#731](https://github.com/estruyf/vscode-front-matter/issues/731): Added the ability to map/unmap taxonomy to multiple pages at once - [#746](https://github.com/estruyf/vscode-front-matter/issues/746): Placeholder support added to to the `slug` field - [#749](https://github.com/estruyf/vscode-front-matter/issues/749): Ability to set your own filters on the content dashboard with the `frontMatter.content.filters` setting +- [[#756](https://github.com/estruyf/vscode-front-matter/issues/756): i18n/multilingual content support ### 🎨 Enhancements diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 6ddfaeab..ef4467aa 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -539,7 +539,9 @@ "commands.i18n.create.warning.noFile": "The file could not be retrieved.", "commands.i18n.create.warning.noContentType": "Content type could not be retrieved for the current file.", "commands.i18n.create.warning.noConfig": "No i18n configuration found.", - "commands.i18n.create.warning.notDefaultLocale": "The current file cannot be used for i18n content creation.", + "commands.i18n.create.error.noLocaleDefinition": "Could not retrieve the locale for the current file.", + "commands.i18n.create.error.noLocales": "Current file has been translated to all available languages.", + "commands.i18n.create.error.noContentFolder": "Could not define a content folder for the current file.", "commands.i18n.create.error.fileExists": "The i18n translation already exists.", "commands.i18n.create.success.created": "Created \"{0}\" i18n content file.", "commands.i18n.create.quickPick.title": "Create content for locale", diff --git a/package.json b/package.json index 1139039b..e1434546 100644 --- a/package.json +++ b/package.json @@ -347,7 +347,8 @@ }, "additionalProperties": false, "required": [ - "locale" + "locale", + "path" ] }, "scope": "Content" @@ -2371,7 +2372,7 @@ { "command": "frontMatter.i18n.create", "group": "navigation@-127", - "when": "frontMatter:file:isValid && frontMatter:i18n:default" + "when": "frontMatter:file:isValid && frontMatter:i18n:enabled" }, { "command": "frontMatter.markup.options", @@ -2474,7 +2475,7 @@ }, { "command": "frontMatter.i18n.create", - "when": "frontMatter:i18n:default" + "when": "frontMatter:i18n:enabled" }, { "command": "frontMatter.collapseSections", diff --git a/src/commands/Folders.ts b/src/commands/Folders.ts index 935e6cb3..2f64e188 100644 --- a/src/commands/Folders.ts +++ b/src/commands/Folders.ts @@ -293,31 +293,6 @@ export class Folders { if (crntFolderInfo) { folderInfo.push(crntFolderInfo); } - - // Process localization folders - if (folder.defaultLocale) { - const i18nConfig = folder.locales || Settings.get(SETTING_CONTENT_I18N); - if (i18nConfig) { - for (const i18n of i18nConfig) { - if (i18n.locale !== folder.defaultLocale && i18n.path) { - const i18nFolder = { - ...folder, - path: join(folder.path, i18n.path), - title: `${folder.title} (${i18n.title})` - } as ContentFolder; - - const crntFolderInfo = await Folders.getFilesByFolder( - i18nFolder, - supportedFiles, - limit - ); - if (crntFolderInfo) { - folderInfo.push(crntFolderInfo); - } - } - } - } - } } return folderInfo; @@ -333,11 +308,14 @@ export class Folders { public static get(): ContentFolder[] { const wsFolder = Folders.getWorkspaceFolder(); let folders: ContentFolder[] = Settings.get(SETTING_CONTENT_PAGE_FOLDERS) as ContentFolder[]; + const i18nSettings = Settings.get(SETTING_CONTENT_I18N); // Filter out folders without a path folders = folders.filter((f) => f.path); - const contentFolders = folders.map((folder) => { + const contentFolders: ContentFolder[] = []; + + folders.forEach((folder) => { if (!folder.title) { folder.title = basename(folder.path); } @@ -371,11 +349,52 @@ export class Folders { } } - return { - ...folder, - originalPath: folder.path, - path: folderPath - }; + // Check i18n + if (folder.defaultLocale && (folder.locales || i18nSettings)) { + const i18nConfig = + folder.locales && folder.locales.length > 0 ? folder.locales : i18nSettings; + + let defaultLocale; + let sourcePath = folderPath; + let localeFolders: ContentFolder[] = []; + + if (i18nConfig && i18nConfig.length > 0) { + for (const i18n of i18nConfig) { + if (i18n.locale === folder.defaultLocale) { + defaultLocale = i18n; + } else if (i18n.locale !== folder.defaultLocale && i18n.path) { + localeFolders.push({ + ...folder, + title: `${folder.title} (${i18n.title})`, + locale: i18n.locale, + localeSourcePath: sourcePath, + path: join(folderPath, i18n.path) + }); + } + } + } + + const defaultTitle = defaultLocale?.title + ? `${folder.title} (${defaultLocale.title})` + : folder.title; + contentFolders.push({ + ...folder, + title: defaultTitle, + locale: folder.defaultLocale, + originalPath: folder.path, + localeSourcePath: sourcePath, + path: join(folderPath, defaultLocale?.path || '') + }); + + contentFolders.push(...localeFolders); + } else { + contentFolders.push({ + ...folder, + locale: folder.defaultLocale, + originalPath: folder.path, + path: folderPath + }); + } }); return contentFolders.filter((folder) => folder !== null) as ContentFolder[]; diff --git a/src/commands/StatusListener.ts b/src/commands/StatusListener.ts index 50effdc5..4968d044 100644 --- a/src/commands/StatusListener.ts +++ b/src/commands/StatusListener.ts @@ -44,8 +44,8 @@ export class StatusListener { commands.executeCommand('setContext', CONTEXT.isValidFile, true); // Check i18n - const isI18nDefault = await i18n.isDefaultLanguage(document.uri.fsPath); - commands.executeCommand('setContext', CONTEXT.isI18nDefault, isI18nDefault); + const isI18nEnabled = await i18n.isLocaleEnabled(document.uri.fsPath); + commands.executeCommand('setContext', CONTEXT.isI18nEnabled, isI18nEnabled); const article = editor ? ArticleHelper.getFrontMatter(editor) @@ -88,7 +88,7 @@ export class StatusListener { } } else { commands.executeCommand('setContext', CONTEXT.isValidFile, false); - commands.executeCommand('setContext', CONTEXT.isI18nDefault, false); + commands.executeCommand('setContext', CONTEXT.isI18nEnabled, false); const panel = PanelProvider.getInstance(); if (panel && panel.visible) { diff --git a/src/commands/i18n.ts b/src/commands/i18n.ts index 2dfd6589..760be23d 100644 --- a/src/commands/i18n.ts +++ b/src/commands/i18n.ts @@ -65,6 +65,25 @@ export class i18n { return pageFolder.locales; } + /** + * Checks if the locale is enabled for the given file path. + * @param filePath - The file path to check. + * @returns A promise that resolves to a boolean indicating whether the locale is enabled or not. + */ + public static async isLocaleEnabled(filePath: string): Promise { + const i18nSettings = await i18n.getSettings(filePath); + if (!i18nSettings) { + return false; + } + + const pageFolder = Folders.getPageFolderByFilePath(filePath); + if (!pageFolder || !pageFolder.locale) { + return false; + } + + return i18nSettings.some((i18n) => i18n.locale === pageFolder.locale); + } + /** * Checks if the given file path corresponds to the default language. * @param filePath - The file path to check. @@ -84,6 +103,10 @@ export class i18n { const fileInfo = await i18n.getFileInfo(filePath); if (pageFolder.path) { + if (pageFolder.locale) { + return pageFolder.locale === pageFolder.defaultLocale; + } + let pageFolderPath = parseWinPath(pageFolder.path); if (!pageFolderPath.endsWith('/')) { pageFolderPath += '/'; @@ -120,9 +143,10 @@ export class i18n { if ( pageFolder.path && + pageFolder.locale && parseWinPath(fileInfo.dir).toLowerCase() === parseWinPath(pageFolderPath).toLowerCase() ) { - return i18nSettings.find((i18n) => i18n.locale === pageFolder?.defaultLocale); + return i18nSettings.find((i18n) => i18n.locale === pageFolder?.locale); } } @@ -172,9 +196,9 @@ export class i18n { let pageFolder = Folders.getPageFolderByFilePath(filePath); const fileInfo = await i18n.getFileInfo(filePath); - if (pageFolder && pageFolder.defaultLocale) { + if (pageFolder && pageFolder.defaultLocale && pageFolder.localeSourcePath) { for (const i18n of i18nSettings) { - const translation = join(pageFolder.path, i18n.path || '', fileInfo.filename); + const translation = join(pageFolder.localeSourcePath, i18n.path || '', fileInfo.filename); if (await existsAsync(translation)) { translations[i18n.locale] = { locale: i18n, @@ -224,20 +248,40 @@ export class i18n { fileUri = Uri.file(fileUri); } + const pageFolder = Folders.getPageFolderByFilePath(fileUri.fsPath); + if (!pageFolder || !pageFolder.localeSourcePath) { + Notifications.error(l10n.t(LocalizationKey.commandsI18nCreateErrorNoContentFolder)); + return; + } + const i18nSettings = await i18n.getSettings(fileUri.fsPath); if (!i18nSettings) { Notifications.warning(l10n.t(LocalizationKey.commandsI18nCreateWarningNoConfig)); return; } - const isDefaultLanguage = await i18n.isDefaultLanguage(fileUri.fsPath); - if (!isDefaultLanguage) { - Notifications.warning(l10n.t(LocalizationKey.commandsI18nCreateWarningNotDefaultLocale)); + const sourceLocale = await i18n.getLocale(fileUri.fsPath); + if (!sourceLocale || !sourceLocale.locale) { + Notifications.warning(l10n.t(LocalizationKey.commandsI18nCreateErrorNoLocaleDefinition)); + return; + } + + const translations = (await i18n.getTranslations(fileUri.fsPath)) || {}; + const targetLocales = i18nSettings.filter((i18nSetting) => { + return ( + i18nSetting.path && + i18nSetting.locale !== sourceLocale.locale && + !translations[i18nSetting.locale] + ); + }); + + if (targetLocales.length === 0) { + Notifications.warning(l10n.t(LocalizationKey.commandsI18nCreateErrorNoLocales)); return; } const locale = await window.showQuickPick( - i18nSettings.filter((i18n) => i18n.path).map((i18n) => i18n.title || i18n.locale), + targetLocales.map((i18n) => i18n.title || i18n.locale), { title: l10n.t(LocalizationKey.commandsI18nCreateQuickPickTitle), placeHolder: l10n.t(LocalizationKey.commandsI18nCreateQuickPickPlaceHolder), @@ -249,10 +293,10 @@ export class i18n { return; } - const selectedI18n = i18nSettings.find( + const targetLocale = i18nSettings.find( (i18n) => i18n.title === locale || i18n.locale === locale ); - if (!selectedI18n || !selectedI18n.path) { + if (!targetLocale || !targetLocale.path) { Notifications.warning(l10n.t(LocalizationKey.commandsI18nCreateWarningNoConfig)); return; } @@ -280,7 +324,7 @@ export class i18n { pageBundleDir = join(parse(pageBundleDir).dir); } - const i18nDir = join(dir, selectedI18n.path, pageBundleDir); + const i18nDir = join(pageFolder.localeSourcePath, targetLocale.path, pageBundleDir); if (!(await existsAsync(i18nDir))) { await workspace.fs.createDirectory(Uri.file(i18nDir)); @@ -290,7 +334,8 @@ export class i18n { article, fileUri.fsPath, contentType, - selectedI18n, + sourceLocale, + targetLocale, i18nDir ); @@ -300,9 +345,8 @@ export class i18n { return; } - const sourceLocale = await i18n.getLocale(fileUri.fsPath); if (sourceLocale?.locale) { - article = await i18n.translate(article, sourceLocale, selectedI18n); + article = await i18n.translate(article, sourceLocale, targetLocale); } const newFileUri = Uri.file(newFilePath); @@ -318,7 +362,7 @@ export class i18n { Notifications.info( l10n.t( LocalizationKey.commandsI18nCreateSuccessCreated, - selectedI18n.title || selectedI18n.locale + sourceLocale.title || sourceLocale.locale ) ); } @@ -349,9 +393,9 @@ export class i18n { cancellable: false }, async () => { - const title = article.data.title; - const description = article.data.description; - const content = article.content; + const title = article.data.title || ''; + const description = article.data.description || ''; + const content = article.content || ''; try { const body = JSON.stringify({ @@ -368,7 +412,6 @@ export class i18n { Authorization: `DeepL-Auth-Key ${authKey}`, 'User-Agent': `FrontMatterCMS/${Extension.getInstance().version}`, 'Content-Type': 'application/json', - 'content-length': body.length.toString(), Accept: 'application/json' }, body @@ -383,9 +426,9 @@ export class i18n { throw new Error('DeepL: Invalid response'); } - article.data.title = data.translations[0].text; - article.data.description = data.translations[1].text; - article.content = data.translations[2].text; + article.data.title = article.data.title ? data.translations[0].text : ''; + article.data.description = article.data.description ? data.translations[1].text : ''; + article.content = article.content ? data.translations[2].text : ''; } catch (error) { Notifications.error(`${(error as Error).message}`); } @@ -460,7 +503,8 @@ export class i18n { * @param article - The parsed front matter of the article. * @param filePath - The path of the file containing the front matter. * @param contentType - The content type of the article. - * @param i18nConfig - The configuration for internationalization. + * @param sourceLocale - The source locale. + * @param targetLocale - The target locale. * @param i18nDir - The directory where the i18n files are located. * @returns A Promise that resolves to the updated parsed front matter. */ @@ -468,7 +512,8 @@ export class i18n { article: ParsedFrontMatter, filePath: string, contentType: IContentType, - i18nConfig: I18nConfig, + sourceLocale: I18nConfig, + targetLocale: I18nConfig, i18nDir: string ): Promise { const imageFields = ContentType.findFieldsByTypeDeep(contentType.fields, 'image'); diff --git a/src/constants/context.ts b/src/constants/context.ts index d169dcc7..e71ac57d 100644 --- a/src/constants/context.ts +++ b/src/constants/context.ts @@ -8,7 +8,7 @@ export const CONTEXT = { isValidFile: 'frontMatter:file:isValid', isDevelopment: 'frontMatter:isDevelopment', - isI18nDefault: 'frontMatter:i18n:default', + isI18nEnabled: 'frontMatter:i18n:enabled', hasViewModes: 'frontMatter:has:modes', diff --git a/src/dashboardWebView/components/Contents/ContentActions.tsx b/src/dashboardWebView/components/Contents/ContentActions.tsx index b4ae10e9..85bfdc06 100644 --- a/src/dashboardWebView/components/Contents/ContentActions.tsx +++ b/src/dashboardWebView/components/Contents/ContentActions.tsx @@ -230,7 +230,7 @@ export const ContentActions: React.FunctionComponent = ({ } { - locale && isDefaultLocale && ( + locale && ( runCommand(COMMAND_NAME.i18n.create)}> {l10n.t(LocalizationKey.dashboardContentsContentActionsTranslationsCreate)} diff --git a/src/localization/localization.enum.ts b/src/localization/localization.enum.ts index 28cc2d66..dcd9c9da 100644 --- a/src/localization/localization.enum.ts +++ b/src/localization/localization.enum.ts @@ -1725,9 +1725,17 @@ export enum LocalizationKey { */ commandsI18nCreateWarningNoConfig = 'commands.i18n.create.warning.noConfig', /** - * The current file cannot be used for i18n content creation. + * Could not retrieve the locale for the current file. */ - commandsI18nCreateWarningNotDefaultLocale = 'commands.i18n.create.warning.notDefaultLocale', + commandsI18nCreateErrorNoLocaleDefinition = 'commands.i18n.create.error.noLocaleDefinition', + /** + * Current file has been translated to all available languages. + */ + commandsI18nCreateErrorNoLocales = 'commands.i18n.create.error.noLocales', + /** + * Could not define a content folder for the current file. + */ + commandsI18nCreateErrorNoContentFolder = 'commands.i18n.create.error.noContentFolder', /** * The i18n translation already exists. */ diff --git a/src/models/ContentFolder.ts b/src/models/ContentFolder.ts index 88f00a1d..a39662e6 100644 --- a/src/models/ContentFolder.ts +++ b/src/models/ContentFolder.ts @@ -12,6 +12,9 @@ export interface ContentFolder { originalPath?: string; $schema?: string; extended?: boolean; + + locale?: string; + localeSourcePath?: string; defaultLocale?: string; locales: I18nConfig[]; } From b1e87d4f579feaecbfc624d99a008524b8e03992 Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Thu, 22 Feb 2024 10:29:51 +0100 Subject: [PATCH 04/23] #760 - Fix to keep locale filter --- src/dashboardWebView/hooks/usePages.tsx | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/dashboardWebView/hooks/usePages.tsx b/src/dashboardWebView/hooks/usePages.tsx index d6ef66b8..a43a585b 100644 --- a/src/dashboardWebView/hooks/usePages.tsx +++ b/src/dashboardWebView/hooks/usePages.tsx @@ -220,16 +220,6 @@ export default function usePages(pages: Page[]) { } } - if (tabPrevious !== tab || !locales || locales.length === 0) { - // Store the locale information - const config: I18nConfig[] = []; - crntPages.forEach((page) => { - if (page.fmLocale && !config.some(locale => locale.locale === page.fmLocale?.locale)) { - config.push(page.fmLocale); - } - }); - setLocales(config); - } // Set the pages setPageItems(crntPages); @@ -276,6 +266,17 @@ export default function usePages(pages: Page[]) { } else { startPageProcessing(); } + + if (pages && pages.length > 0) { + // Store the locale information + const config: I18nConfig[] = []; + pages.forEach((page) => { + if (page.fmLocale && !config.some(locale => locale.locale === page.fmLocale?.locale)) { + config.push(page.fmLocale); + } + }); + setLocales(config); + } }, [settings?.draftField, pages, sorting, search, tag, category, locale, filters, folder]); useEffect(() => { From b4d2e4ea8bfc5f546b9ff63ee04d9528e10f8d8a Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Thu, 22 Feb 2024 11:14:30 +0100 Subject: [PATCH 05/23] #749 - update name --- src/dashboardWebView/components/Header/Filters.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dashboardWebView/components/Header/Filters.tsx b/src/dashboardWebView/components/Header/Filters.tsx index e2dfd4a5..3733e605 100644 --- a/src/dashboardWebView/components/Header/Filters.tsx +++ b/src/dashboardWebView/components/Header/Filters.tsx @@ -77,7 +77,7 @@ export const Filters: React.FunctionComponent = (_: React.PropsWi { - settings?.filters?.includes("pageFolders") && ( + settings?.filters?.includes("contentFolders") && ( ) } From bc3b2c403db4ea2acccf3969847a40f5a513e286 Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Thu, 22 Feb 2024 11:15:15 +0100 Subject: [PATCH 06/23] Update default --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e1434546..3145e98f 100644 --- a/package.json +++ b/package.json @@ -527,7 +527,7 @@ "frontMatter.content.filters": { "type": "array", "default": [ - "pageFolders", + "contentFolders", "tags", "categories" ], From a641aabc2a3895d81f516037b3fa6d9d9cb8ab74 Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Thu, 22 Feb 2024 11:44:23 +0100 Subject: [PATCH 07/23] #749 - Support in extended configs --- package.json | 7 ++++++- src/dashboardWebView/components/Header/Filters.tsx | 2 +- src/dashboardWebView/hooks/usePages.tsx | 2 +- src/dashboardWebView/models/Settings.ts | 3 ++- src/helpers/DashboardSettings.ts | 4 +++- src/helpers/SettingsHelper.ts | 6 ++++-- src/models/FilterType.ts | 1 + src/models/index.ts | 1 + 8 files changed, 19 insertions(+), 7 deletions(-) create mode 100644 src/models/FilterType.ts diff --git a/package.json b/package.json index 3145e98f..b1b8fd78 100644 --- a/package.json +++ b/package.json @@ -533,7 +533,12 @@ ], "markdownDescription": "%setting.frontMatter.content.filters.markdownDescription%", "items": [{ - "type": "string" + "type": "string", + "enum": [ + "contentFolders", + "tags", + "categories" + ] }, { "type": "object", diff --git a/src/dashboardWebView/components/Header/Filters.tsx b/src/dashboardWebView/components/Header/Filters.tsx index 3733e605..a0c4d626 100644 --- a/src/dashboardWebView/components/Header/Filters.tsx +++ b/src/dashboardWebView/components/Header/Filters.tsx @@ -18,7 +18,7 @@ export const Filters: React.FunctionComponent = (_: React.PropsWi const settings = useRecoilValue(SettingsSelector); const location = useLocation(); - const otherFilters = useMemo(() => settings?.filters?.filter((filter) => filter !== "pageFolders" && filter !== "tags" && filter !== "categories"), [settings?.filters]); + const otherFilters = useMemo(() => settings?.filters?.filter((filter) => filter !== "contentFolders" && filter !== "tags" && filter !== "categories"), [settings?.filters]); const otherFilterValues = useMemo(() => { return otherFilters?.map((filter) => { diff --git a/src/dashboardWebView/hooks/usePages.tsx b/src/dashboardWebView/hooks/usePages.tsx index a43a585b..76cf0612 100644 --- a/src/dashboardWebView/hooks/usePages.tsx +++ b/src/dashboardWebView/hooks/usePages.tsx @@ -204,7 +204,7 @@ export default function usePages(pages: Page[]) { setTabInfo(draftTypes); if (Object.keys(filters).length === 0) { - const availableFilters = (settings?.filters || []).filter((f) => f !== 'pageFolders' && f !== 'tags' && f !== 'categories'); + const availableFilters = (settings?.filters || []).filter((f) => f !== 'contentFolders' && f !== 'tags' && f !== 'categories'); if (availableFilters.length > 0) { const allFilters: { [filter: string]: string[]; } = {}; for (const filter of availableFilters) { diff --git a/src/dashboardWebView/models/Settings.ts b/src/dashboardWebView/models/Settings.ts index 2b677a71..f96f6a9d 100644 --- a/src/dashboardWebView/models/Settings.ts +++ b/src/dashboardWebView/models/Settings.ts @@ -6,6 +6,7 @@ import { CustomScript, CustomTaxonomy, DraftField, + FilterType, Framework, GitSettings, MediaContentType, @@ -38,7 +39,7 @@ export interface Settings { framework: Framework | null | undefined; draftField: DraftField | null | undefined; customSorting: SortingSetting[] | undefined; - filters: (string | { title: string; name: string })[] | undefined; + filters: (FilterType | { title: string; name: string })[] | undefined; dashboardState: DashboardState; scripts: CustomScript[]; dataFiles: DataFile[] | undefined; diff --git a/src/helpers/DashboardSettings.ts b/src/helpers/DashboardSettings.ts index 407f22d2..e8bc15d3 100644 --- a/src/helpers/DashboardSettings.ts +++ b/src/helpers/DashboardSettings.ts @@ -42,6 +42,7 @@ import { CustomScript, DEFAULT_MEDIA_CONTENT_TYPE, DraftField, + FilterType, MediaContentType, Snippets, SortingSetting, @@ -111,7 +112,8 @@ export class DashboardSettings { draftField: Settings.get(SETTING_CONTENT_DRAFT_FIELD), customSorting: Settings.get(SETTING_CONTENT_SORTING), contentFolders: Folders.get(), - filters: Settings.get(SETTING_CONTENT_FILTERS), + filters: + Settings.get<(FilterType | { title: string; name: string })[]>(SETTING_CONTENT_FILTERS), crntFramework: Settings.get(SETTING_FRAMEWORK_ID), framework: !isInitialized && wsFolder ? await FrameworkDetector.get(wsFolder.fsPath) : null, scripts: Settings.get(SETTING_CUSTOM_SCRIPTS) || [], diff --git a/src/helpers/SettingsHelper.ts b/src/helpers/SettingsHelper.ts index 92ab8d5b..19139bf4 100644 --- a/src/helpers/SettingsHelper.ts +++ b/src/helpers/SettingsHelper.ts @@ -43,7 +43,8 @@ import { SETTING_CONFIG_DYNAMIC_FILE_PATH, SETTING_PROJECTS, SETTING_TAXONOMY_TAGS, - SETTING_TAXONOMY_CATEGORIES + SETTING_TAXONOMY_CATEGORIES, + SETTING_CONTENT_FILTERS } from '../constants'; import { Folders } from '../commands/Folders'; import { join, basename, dirname, parse } from 'path'; @@ -804,7 +805,8 @@ export class Settings { settingName === SETTING_GLOBAL_NOTIFICATIONS || settingName === SETTING_GLOBAL_NOTIFICATIONS_DISABLED || settingName === SETTING_MEDIA_SUPPORTED_MIMETYPES || - settingName === SETTING_COMMA_SEPARATED_FIELDS + settingName === SETTING_COMMA_SEPARATED_FIELDS || + settingName === SETTING_CONTENT_FILTERS ) { if (typeof originalConfig[key] === 'undefined') { Settings.globalConfig[key] = value; diff --git a/src/models/FilterType.ts b/src/models/FilterType.ts new file mode 100644 index 00000000..4ff3a294 --- /dev/null +++ b/src/models/FilterType.ts @@ -0,0 +1 @@ +export type FilterType = 'contentFolders' | 'tags' | 'categories'; diff --git a/src/models/index.ts b/src/models/index.ts index a118d63b..82b09148 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -11,6 +11,7 @@ export * from './DataFile'; export * from './DataFolder'; export * from './DataType'; export * from './DraftField'; +export * from './FilterType'; export * from './Framework'; export * from './GitRepository'; export * from './GitSettings'; From 41a5e9ab7a5f292d63a7917946f158e5d33ac979 Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Thu, 22 Feb 2024 13:09:36 +0100 Subject: [PATCH 08/23] Remove deprecated settings --- l10n/bundle.l10n.json | 3 - package.json | 23 ----- src/constants/settings.ts | 10 --- .../components/Media/DetailsSlideOver.tsx | 2 +- src/helpers/ArticleHelper.ts | 7 +- src/helpers/Extension.ts | 87 +------------------ 6 files changed, 3 insertions(+), 129 deletions(-) diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index ef4467aa..7ca46bcd 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -658,9 +658,6 @@ "helpers.extension.getVersion.changelog": "Check the changelog", "helpers.extension.getVersion.starIt": "Give it a ⭐️", "helpers.extension.getVersion.update.notification": "{0} has been updated to v{1} — check out what's new!", - "helpers.extension.migrateSettings.deprecated.warning": "The \"{0}\" and \"{1}\" settings have been deprecated. Please use the \"isPublishDate\" and \"isModifiedDate\" datetime field properties instead.", - "helpers.extension.migrateSettings.deprecated.warning.hide": "Hide", - "helpers.extension.migrateSettings.deprecated.warning.seeGuide": "See migration guide", "helpers.extension.migrateSettings.templates.quickPick.title": "{0} - Templates", "helpers.extension.migrateSettings.templates.quickPick.placeholder": "Do you want to keep on using the template functionality?", "helpers.extension.checkIfExtensionCanRun.warning": "Front Matter BETA cannot be used while the stable version is installed. Please ensure that you have only over version installed.", diff --git a/package.json b/package.json index b1b8fd78..9ddfc04b 100644 --- a/package.json +++ b/package.json @@ -707,17 +707,6 @@ "default": "", "markdownDescription": "%setting.frontMatter.dashboard.content.card.fields.title.markdownDescription%" }, - "frontMatter.dashboard.mediaSnippet": { - "type": "array", - "default": [], - "markdownDescription": "%setting.frontMatter.dashboard.mediaSnippet.markdownDescription%", - "deprecationMessage": "%setting.frontMatter.dashboard.mediaSnippet.deprecationMessage%", - "items": { - "type": "string", - "description": "%setting.frontMatter.dashboard.mediaSnippet.items.description%" - }, - "scope": "dashboard" - }, "frontMatter.dashboard.openOnStart": { "type": [ "boolean", @@ -1840,12 +1829,6 @@ }, "scope": "Taxonomy" }, - "frontMatter.taxonomy.dateField": { - "type": "string", - "default": "date", - "markdownDescription": "%setting.frontMatter.taxonomy.dateField.markdownDescription%", - "deprecationMessage": "%setting.frontMatter.taxonomy.dateField.deprecationMessage%" - }, "frontMatter.taxonomy.dateFormat": { "type": "string", "markdownDescription": "%setting.frontMatter.taxonomy.dateFormat.markdownDescription%", @@ -1899,12 +1882,6 @@ "markdownDescription": "%setting.frontMatter.taxonomy.indentArrays.markdownDescription%", "scope": "Taxonomy" }, - "frontMatter.taxonomy.modifiedField": { - "type": "string", - "default": "lastmod", - "markdownDescription": "%setting.frontMatter.taxonomy.modifiedField.markdownDescription%", - "deprecationMessage": "%setting.frontMatter.taxonomy.modifiedField.deprecationMessage%" - }, "frontMatter.taxonomy.quoteStringValues": { "type": "boolean", "default": false, diff --git a/src/constants/settings.ts b/src/constants/settings.ts index d184a65b..f24b7263 100644 --- a/src/constants/settings.ts +++ b/src/constants/settings.ts @@ -133,13 +133,3 @@ export const SETTING_CONTENT_FOLDERS = 'content.folders'; * Use the `isPublishDate` property on the content type datetime field instead */ export const SETTING_DATE_FIELD = 'taxonomy.dateField'; -/** - * @deprecated - * Use the `isModifiedDate` property on the content type datetime field instead - */ -export const SETTING_MODIFIED_FIELD = 'taxonomy.modifiedField'; -/** - * @deprecated - * Use the `frontMatter.content.snippets` setting instead - */ -export const SETTING_DASHBOARD_MEDIA_SNIPPET = 'dashboard.mediaSnippet'; diff --git a/src/dashboardWebView/components/Media/DetailsSlideOver.tsx b/src/dashboardWebView/components/Media/DetailsSlideOver.tsx index b689711b..10b25684 100644 --- a/src/dashboardWebView/components/Media/DetailsSlideOver.tsx +++ b/src/dashboardWebView/components/Media/DetailsSlideOver.tsx @@ -128,7 +128,7 @@ export const DetailsSlideOver: React.FunctionComponent =
-
+
{(isImageFile || isVideoFile) && (
diff --git a/src/helpers/ArticleHelper.ts b/src/helpers/ArticleHelper.ts index e1473736..2679d8f4 100644 --- a/src/helpers/ArticleHelper.ts +++ b/src/helpers/ArticleHelper.ts @@ -18,7 +18,6 @@ import { SETTING_SITE_BASEURL, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_TEMPLATES_PREFIX, - SETTING_MODIFIED_FIELD, DefaultFieldValues } from '../constants'; import { DumpOptions } from 'js-yaml'; @@ -387,11 +386,7 @@ export class ArticleHelper { const articleCt = ArticleHelper.getContentType(article); const modDateField = articleCt.fields.find((f) => f.isModifiedDate); - return ( - modDateField?.name || - (Settings.get(SETTING_MODIFIED_FIELD) as string) || - DefaultFields.LastModified - ); + return modDateField?.name || DefaultFields.LastModified; } /** diff --git a/src/helpers/Extension.ts b/src/helpers/Extension.ts index 16505bda..968adb8f 100644 --- a/src/helpers/Extension.ts +++ b/src/helpers/Extension.ts @@ -15,20 +15,15 @@ import { Template } from '../commands/Template'; import { EXTENSION_NAME, GITHUB_LINK, - SETTING_DATE_FIELD, - SETTING_MODIFIED_FIELD, EXTENSION_BETA_ID, EXTENSION_ID, ExtensionState, - CONFIG_KEY, SETTING_CONTENT_PAGE_FOLDERS, - SETTING_DASHBOARD_MEDIA_SNIPPET, - SETTING_CONTENT_SNIPPETS, SETTING_TEMPLATES_ENABLED, SETTING_TAXONOMY_TAGS, SETTING_TAXONOMY_CATEGORIES } from '../constants'; -import { ContentFolder, Snippet, TaxonomyType } from '../models'; +import { ContentFolder, TaxonomyType } from '../models'; import { Notifications } from './Notifications'; import { Settings } from './SettingsHelper'; import { TaxonomyHelper } from './TaxonomyHelper'; @@ -202,64 +197,6 @@ export class Extension { await Settings.createTeamSettings(); } - const hideDateDeprecation = await Extension.getInstance().getState( - ExtensionState.Updates.v7_0_0.dateFields, - 'workspace' - ); - if (!hideDateDeprecation) { - // Migration scripts can be written here - const publishField = Settings.inspect(SETTING_DATE_FIELD); - const modifiedField = Settings.inspect(SETTING_MODIFIED_FIELD); - - // Check for extension deprecations - if ( - publishField?.workspaceValue || - publishField?.globalValue || - publishField?.teamValue || - modifiedField?.workspaceValue || - modifiedField?.globalValue || - modifiedField?.teamValue - ) { - Notifications.warning( - l10n.t( - LocalizationKey.helpersExtensionMigrateSettingsDeprecatedWarning, - `${CONFIG_KEY}.${SETTING_DATE_FIELD}`, - `${CONFIG_KEY}.${SETTING_MODIFIED_FIELD}` - ), - l10n.t(LocalizationKey.helpersExtensionMigrateSettingsDeprecatedWarningHide), - l10n.t(LocalizationKey.helpersExtensionMigrateSettingsDeprecatedWarningSeeGuide) - ).then(async (value) => { - if ( - value === - l10n.t(LocalizationKey.helpersExtensionMigrateSettingsDeprecatedWarningSeeGuide) - ) { - const isProd = this.isProductionMode; - commands.executeCommand( - 'vscode.open', - Uri.parse( - `https://${ - isProd ? '' : 'beta.' - }frontmatter.codes/docs/troubleshooting#publish-and-modified-date-migration` - ) - ); - await Extension.getInstance().setState( - ExtensionState.Updates.v7_0_0.dateFields, - true, - 'workspace' - ); - } else if ( - value === l10n.t(LocalizationKey.helpersExtensionMigrateSettingsDeprecatedWarningHide) - ) { - await Extension.getInstance().setState( - ExtensionState.Updates.v7_0_0.dateFields, - true, - 'workspace' - ); - } - }); - } - } - if (major < 7) { const contentFolders: ContentFolder[] = Settings.get( SETTING_CONTENT_PAGE_FOLDERS @@ -282,28 +219,6 @@ export class Extension { } if (major <= 7 && minor < 3) { - const mediaSnippet = Settings.get(SETTING_DASHBOARD_MEDIA_SNIPPET); - if (mediaSnippet && mediaSnippet.length > 0) { - let snippet = mediaSnippet.join(`\n`); - - snippet = snippet.replace(`{mediaUrl}`, `[[&mediaUrl]]`); - snippet = snippet.replace(`{mediaHeight}`, `[[mediaHeight]]`); - snippet = snippet.replace(`{mediaWidth}`, `[[mediaWidth]]`); - snippet = snippet.replace(`{caption}`, `[[&caption]]`); - snippet = snippet.replace(`{alt}`, `[[alt]]`); - snippet = snippet.replace(`{filename}`, `[[filename]]`); - snippet = snippet.replace(`{title}`, `[[title]]`); - - const snippets = Settings.get(SETTING_CONTENT_SNIPPETS) || ({} as any); - snippets[`Media snippet (migrated)`] = { - body: snippet.split(`\n`), - isMediaSnippet: true, - description: `Migrated media snippet from frontMatter.dashboard.mediaSnippet setting` - }; - - await Settings.update(SETTING_CONTENT_SNIPPETS, snippets, true); - } - const templates = await Template.getTemplates(); if (templates && templates.length > 0) { const answer = await window.showQuickPick( From 40c722e38080420b01a30085b95c89e3e6051e55 Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Thu, 22 Feb 2024 13:30:40 +0100 Subject: [PATCH 09/23] Update changelog --- CHANGELOG.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 05531c40..bfbecbe1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Change Log -## [10.0.0] - 2024-xx-xx +## [10.0.0] - 2024-xx-xx - [Release notes](https://beta.frontmatter.codes/updates/v10.0.0) ### ✨ New features @@ -19,8 +19,6 @@ - [#747](https://github.com/estruyf/vscode-front-matter/issues/747): The `@frontmatter/extensibility` dependency now supports scripts for placeholders - [#752](https://github.com/estruyf/vscode-front-matter/issues/752): Placeholder support in default `list` field values -### ⚡️ Optimizations - ### 🐞 Fixes - [#721](https://github.com/estruyf/vscode-front-matter/issues/721): Fix keywords regex to support unicode characters From 4a0c1a40599fefcd286db47b13bf404eb0e87a97 Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Thu, 22 Feb 2024 13:35:14 +0100 Subject: [PATCH 10/23] Update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bfbecbe1..40c6997a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ - [#731](https://github.com/estruyf/vscode-front-matter/issues/731): Added the ability to map/unmap taxonomy to multiple pages at once - [#746](https://github.com/estruyf/vscode-front-matter/issues/746): Placeholder support added to to the `slug` field - [#749](https://github.com/estruyf/vscode-front-matter/issues/749): Ability to set your own filters on the content dashboard with the `frontMatter.content.filters` setting -- [[#756](https://github.com/estruyf/vscode-front-matter/issues/756): i18n/multilingual content support +- [#756](https://github.com/estruyf/vscode-front-matter/issues/756): i18n/multilingual content support ### 🎨 Enhancements From 1b0a99b8fb69e72854fda9075c479b103991f103 Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Thu, 22 Feb 2024 15:25:05 +0100 Subject: [PATCH 11/23] Azure translations --- l10n/bundle.l10n.json | 10 +- src/commands/i18n.ts | 55 +++----- src/constants/ExtensionState.ts | 8 +- .../SettingsView/IntegrationsView.tsx | 83 +++++++++-- src/localization/localization.enum.ts | 36 +++-- src/services/Translations.ts | 132 ++++++++++++++++++ 6 files changed, 256 insertions(+), 68 deletions(-) create mode 100644 src/services/Translations.ts diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 7ca46bcd..2075fef4 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -67,8 +67,14 @@ "settings.commonSettings.startCommand": "SSG/Framework start command", "settings.integrationsView.deepl.title": "DeepL", - "settings.integrationsView.deepl.intput.label": "Authentication key", - "settings.integrationsView.deepl.intput.placeholder": "Enter your DeepL authentication key", + "settings.integrationsView.deepl.intput.label": "API key", + "settings.integrationsView.deepl.intput.placeholder": "Enter your Azure Translator API key", + + "settings.integrationsView.azure.title": "Azure AI Translator Service", + "settings.integrationsView.azure.intput.label": "Subscription key", + "settings.integrationsView.azure.intput.placeholder": "Enter your Azure AI Translator - Subscription key", + "settings.integrationsView.azure.region.label": "Region", + "settings.integrationsView.azure.region.placeholder": "Enter your Azure AI Translator - Region. Example: westeurope", "developer.title": "Developer mode", "developer.reload.title": "Reload the dashboard", diff --git a/src/commands/i18n.ts b/src/commands/i18n.ts index 760be23d..bbe12970 100644 --- a/src/commands/i18n.ts +++ b/src/commands/i18n.ts @@ -4,13 +4,12 @@ import { ContentType, Extension, FrameworkDetector, - Logger, Notifications, Settings, openFileInEditor, parseWinPath } from '../helpers'; -import { COMMAND_NAME, ExtensionState, SETTING_CONTENT_I18N } from '../constants'; +import { COMMAND_NAME, SETTING_CONTENT_I18N } from '../constants'; import { ContentFolder, Field, I18nConfig, ContentType as IContentType } from '../models'; import { join, parse } from 'path'; import { existsAsync } from '../utils'; @@ -19,6 +18,7 @@ import { ParsedFrontMatter } from '../parsers'; import { PagesListener } from '../listeners/dashboard'; import * as l10n from '@vscode/l10n'; import { LocalizationKey } from '../localization'; +import { Translations } from '../services/Translations'; export class i18n { private static processedFiles: { @@ -380,12 +380,6 @@ export class i18n { targetLocale: I18nConfig ) { return new Promise(async (resolve) => { - const authKey = await Extension.getInstance().getSecret(ExtensionState.Secrets.DeeplApiKey); - if (!authKey) { - resolve(article); - return; - } - await window.withProgress( { location: ProgressLocation.Notification, @@ -393,42 +387,25 @@ export class i18n { cancellable: false }, async () => { - const title = article.data.title || ''; - const description = article.data.description || ''; - const content = article.content || ''; - try { - const body = JSON.stringify({ - text: [title, description, content], - source_lang: sourceLocale.locale, - target_lang: targetLocale.locale - }); + const title = article.data.title || ''; + const description = article.data.description || ''; + const content = article.content || ''; - let host = authKey.endsWith(':fx') ? 'api-free.deepl.com' : 'api.deepl.com'; + const text = [title, description, content]; + const translations = await Translations.translate( + text, + sourceLocale.locale, + targetLocale.locale + ); - const response = await fetch(`https://${host}/v2/translate`, { - method: 'POST', - headers: { - Authorization: `DeepL-Auth-Key ${authKey}`, - 'User-Agent': `FrontMatterCMS/${Extension.getInstance().version}`, - 'Content-Type': 'application/json', - Accept: 'application/json' - }, - body - }); - - if (!response.ok) { - throw new Error(`DeepL: ${response.statusText}`); + if (!translations || translations.length < 3) { + throw new Error('Invalid response'); } - const data = await response.json(); - if (!data.translations || data.translations.length < 3) { - throw new Error('DeepL: Invalid response'); - } - - article.data.title = article.data.title ? data.translations[0].text : ''; - article.data.description = article.data.description ? data.translations[1].text : ''; - article.content = article.content ? data.translations[2].text : ''; + article.data.title = article.data.title ? translations[0] : ''; + article.data.description = article.data.description ? translations[1] : ''; + article.content = article.content ? translations[2] : ''; } catch (error) { Notifications.error(`${(error as Error).message}`); } diff --git a/src/constants/ExtensionState.ts b/src/constants/ExtensionState.ts index e8fe9722..ae9c40a8 100644 --- a/src/constants/ExtensionState.ts +++ b/src/constants/ExtensionState.ts @@ -33,6 +33,12 @@ export const ExtensionState = { }, Secrets: { - DeeplApiKey: `frontMatter:Secrets:DeeplApiKey` + Deepl: { + ApiKey: `frontMatter:Secrets:DeeplApiKey` + }, + Azure: { + TranslatorKey: `frontMatter:Secrets:AzureTranslatorKey`, + TranslatorRegion: `frontMatter:Secrets:AzureTranslatorRegion` + } } }; diff --git a/src/dashboardWebView/components/SettingsView/IntegrationsView.tsx b/src/dashboardWebView/components/SettingsView/IntegrationsView.tsx index bc155dfe..c5ba04dc 100644 --- a/src/dashboardWebView/components/SettingsView/IntegrationsView.tsx +++ b/src/dashboardWebView/components/SettingsView/IntegrationsView.tsx @@ -10,26 +10,64 @@ export interface IIntegrationsViewProps { } export const IntegrationsView: React.FunctionComponent = ({ }: React.PropsWithChildren) => { const [deeplApiKey, setDeeplApiKey] = React.useState(''); + const [azureApiKey, setAzureApiKey] = React.useState(''); + const [azureRegion, setAzureRegion] = React.useState(''); const [crntDeeplApiKey, setCrntDeeplApiKey] = React.useState(''); + const [crntAzureApiKey, setCrntAzureApiKey] = React.useState(''); + const [crntAzureRegion, setCrntAzureRegion] = React.useState(''); const onSave = React.useCallback(() => { - messageHandler.request(GeneralCommands.toVSCode.secrets.set, { - key: ExtensionState.Secrets.DeeplApiKey, - value: crntDeeplApiKey - }).then((apiKey: string) => { - setDeeplApiKey(apiKey); - }); - }, [crntDeeplApiKey]); + if (crntDeeplApiKey !== deeplApiKey) { + messageHandler.request(GeneralCommands.toVSCode.secrets.set, { + key: ExtensionState.Secrets.Deepl.ApiKey, + value: crntDeeplApiKey + }).then((apiKey: string) => { + setDeeplApiKey(apiKey); + }); + } - const onChange = (_: string, value: string) => { - setCrntDeeplApiKey(value); + if (crntAzureApiKey !== azureApiKey) { + messageHandler.request(GeneralCommands.toVSCode.secrets.set, { + key: ExtensionState.Secrets.Azure.TranslatorKey, + value: crntAzureApiKey + }).then((apiKey: string) => { + setAzureApiKey(apiKey); + }); + } + + if (crntAzureRegion !== azureRegion) { + messageHandler.request(GeneralCommands.toVSCode.secrets.set, { + key: ExtensionState.Secrets.Azure.TranslatorRegion, + value: crntAzureRegion + }).then((apiKey: string) => { + setAzureRegion(apiKey); + }); + } + }, [crntDeeplApiKey, deeplApiKey, crntAzureApiKey, azureApiKey, crntAzureRegion, azureRegion]); + + const onChange = (key: string, value: string) => { + if (key === ExtensionState.Secrets.Deepl.ApiKey) { + setCrntDeeplApiKey(value); + } else if (key === ExtensionState.Secrets.Azure.TranslatorKey) { + setCrntAzureApiKey(value); + } else if (key === ExtensionState.Secrets.Azure.TranslatorRegion) { + setCrntAzureRegion(value); + } }; React.useEffect(() => { - messageHandler.request(GeneralCommands.toVSCode.secrets.get, ExtensionState.Secrets.DeeplApiKey).then((apiKey: string) => { + messageHandler.request(GeneralCommands.toVSCode.secrets.get, ExtensionState.Secrets.Deepl.ApiKey).then((apiKey: string) => { setDeeplApiKey(apiKey); setCrntDeeplApiKey(apiKey); }); + messageHandler.request(GeneralCommands.toVSCode.secrets.get, ExtensionState.Secrets.Azure.TranslatorKey).then((apiKey: string) => { + setAzureApiKey(apiKey); + setCrntAzureApiKey(apiKey); + }); + messageHandler.request(GeneralCommands.toVSCode.secrets.get, ExtensionState.Secrets.Azure.TranslatorRegion).then((apiKey: string) => { + setAzureRegion(apiKey); + setCrntAzureRegion(apiKey); + }); }, []); return ( @@ -39,16 +77,37 @@ export const IntegrationsView: React.FunctionComponent = +

{l10n.t(LocalizationKey.settingsIntegrationsViewAzureTitle)}

+ + + +
+ disabled={ + deeplApiKey === crntDeeplApiKey && + azureApiKey === crntAzureApiKey && + azureRegion === crntAzureRegion + }> {l10n.t(LocalizationKey.commonSave)}
diff --git a/src/localization/localization.enum.ts b/src/localization/localization.enum.ts index dcd9c9da..d3ab017f 100644 --- a/src/localization/localization.enum.ts +++ b/src/localization/localization.enum.ts @@ -248,13 +248,33 @@ export enum LocalizationKey { */ settingsIntegrationsViewDeeplTitle = 'settings.integrationsView.deepl.title', /** - * Authentication key + * API key */ settingsIntegrationsViewDeeplIntputLabel = 'settings.integrationsView.deepl.intput.label', /** - * Enter your DeepL authentication key + * Enter your Azure Translator API key */ settingsIntegrationsViewDeeplIntputPlaceholder = 'settings.integrationsView.deepl.intput.placeholder', + /** + * Azure AI Translator Service + */ + settingsIntegrationsViewAzureTitle = 'settings.integrationsView.azure.title', + /** + * Subscription key + */ + settingsIntegrationsViewAzureIntputLabel = 'settings.integrationsView.azure.intput.label', + /** + * Enter your Azure AI Translator - Subscription key + */ + settingsIntegrationsViewAzureIntputPlaceholder = 'settings.integrationsView.azure.intput.placeholder', + /** + * Region + */ + settingsIntegrationsViewAzureRegionLabel = 'settings.integrationsView.azure.region.label', + /** + * Enter your Azure AI Translator - Region. Example: westeurope + */ + settingsIntegrationsViewAzureRegionPlaceholder = 'settings.integrationsView.azure.region.placeholder', /** * Developer mode */ @@ -2156,18 +2176,6 @@ export enum LocalizationKey { * {0} has been updated to v{1} — check out what's new! */ helpersExtensionGetVersionUpdateNotification = 'helpers.extension.getVersion.update.notification', - /** - * The "{0}" and "{1}" settings have been deprecated. Please use the "isPublishDate" and "isModifiedDate" datetime field properties instead. - */ - helpersExtensionMigrateSettingsDeprecatedWarning = 'helpers.extension.migrateSettings.deprecated.warning', - /** - * Hide - */ - helpersExtensionMigrateSettingsDeprecatedWarningHide = 'helpers.extension.migrateSettings.deprecated.warning.hide', - /** - * See migration guide - */ - helpersExtensionMigrateSettingsDeprecatedWarningSeeGuide = 'helpers.extension.migrateSettings.deprecated.warning.seeGuide', /** * {0} - Templates */ diff --git a/src/services/Translations.ts b/src/services/Translations.ts new file mode 100644 index 00000000..554ed9e4 --- /dev/null +++ b/src/services/Translations.ts @@ -0,0 +1,132 @@ +import { ExtensionState } from '../constants'; +import { Extension } from '../helpers'; + +export class Translations { + /** + * Translates an array of text from a source language to a target language. + * @param text - The array of text to be translated. + * @param source - The source language code. + * @param target - The target language code. + * @returns A Promise that resolves to an array of translated text, or undefined if translation is not possible. + */ + public static async translate( + text: string[], + source: string, + target: string + ): Promise { + const deeplAuthKey = await Extension.getInstance().getSecret( + ExtensionState.Secrets.Deepl.ApiKey + ); + const azureAuthKey = await Extension.getInstance().getSecret( + ExtensionState.Secrets.Azure.TranslatorKey + ); + const azureRegion = await Extension.getInstance().getSecret( + ExtensionState.Secrets.Azure.TranslatorRegion + ); + + if (azureAuthKey && azureRegion) { + return this.translateAzure(text, source, target, azureAuthKey, azureRegion); + } + + if (deeplAuthKey) { + return this.translateDeepL(text, source, target, deeplAuthKey); + } + + return; + } + + /** + * Translates an array of text using Azure Cognitive Services Translator API. + * @param text - The array of text to be translated. + * @param source - The source language code. + * @param target - The target language code. + * @param azureAuthKey - The Azure authentication key. + * @param azureRegion - The Azure region for the translation service. + * @returns A promise that resolves to an array of translated text. + * @throws An error if the translation fails. + */ + private static async translateAzure( + text: string[], + source: string, + target: string, + azureAuthKey: string, + azureRegion: string + ): Promise { + try { + const body = JSON.stringify(text.map((t) => ({ Text: t }))); + + const response = await fetch( + `https://api.cognitive.microsofttranslator.com/translate?api-version=3.0&to=${target}&from=${source}`, + { + method: 'POST', + headers: { + 'Ocp-Apim-Subscription-Key': azureAuthKey, + 'Ocp-Apim-Subscription-Region': azureRegion, + 'Content-Type': 'application/json; charset=UTF-8' + }, + body + } + ); + + if (!response.ok) { + throw new Error(`${response.statusText}`); + } + + const data = await response.json(); + + return data.map((t: { translations: { text: string }[] }) => t.translations[0].text); + } catch (error) { + throw new Error(`Azure: ${(error as Error).message}`); + } + } + + /** + * Translates an array of text using the DeepL translation service. + * @param text - The text to be translated. + * @param source - The source language of the text. + * @param target - The target language for the translation. + * @param deeplAuthKey - The authentication key for accessing the DeepL API. + * @returns A Promise that resolves to an array of translated text. + * @throws If there is an error during the translation process. + */ + private static async translateDeepL( + text: string[], + source: string, + target: string, + deeplAuthKey: string + ): Promise { + try { + const body = JSON.stringify({ + text, + source_lang: source, + target_lang: target + }); + + let host = deeplAuthKey.endsWith(':fx') ? 'api-free.deepl.com' : 'api.deepl.com'; + + const response = await fetch(`https://${host}/v2/translate`, { + method: 'POST', + headers: { + Authorization: `DeepL-Auth-Key ${deeplAuthKey}`, + 'User-Agent': `FrontMatterCMS/${Extension.getInstance().version}`, + 'Content-Type': 'application/json', + Accept: 'application/json' + }, + body + }); + + if (!response.ok) { + throw new Error(`${response.statusText}`); + } + + const data = await response.json(); + if (!data.translations || data.translations.length < 3) { + throw new Error('Invalid response'); + } + + return data.translations.map((t: { text: string }) => t.text); + } catch (error) { + throw new Error(`DeepL: ${(error as Error).message}`); + } + } +} From 91ec23e77c7d35890dbe6ce8bdda24a7c10601f6 Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Thu, 22 Feb 2024 15:28:23 +0100 Subject: [PATCH 12/23] HTML text type --- src/services/Translations.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/Translations.ts b/src/services/Translations.ts index 554ed9e4..56a4aa1b 100644 --- a/src/services/Translations.ts +++ b/src/services/Translations.ts @@ -56,7 +56,7 @@ export class Translations { const body = JSON.stringify(text.map((t) => ({ Text: t }))); const response = await fetch( - `https://api.cognitive.microsofttranslator.com/translate?api-version=3.0&to=${target}&from=${source}`, + `https://api.cognitive.microsofttranslator.com/translate?api-version=3.0&to=${target}&from=${source}&textType=html`, { method: 'POST', headers: { From a8777c4032b17ab86710865caafc54f27ca49917 Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Fri, 23 Feb 2024 08:39:43 +0100 Subject: [PATCH 13/23] #760 - Type filter fix --- src/commands/Folders.ts | 10 +++++----- .../components/Header/FoldersFilter.tsx | 8 ++++++-- src/helpers/Questions.ts | 8 +++++++- src/models/ContentFolder.ts | 1 + 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/commands/Folders.ts b/src/commands/Folders.ts index 2f64e188..2663fa8a 100644 --- a/src/commands/Folders.ts +++ b/src/commands/Folders.ts @@ -365,8 +365,10 @@ export class Folders { } else if (i18n.locale !== folder.defaultLocale && i18n.path) { localeFolders.push({ ...folder, - title: `${folder.title} (${i18n.title})`, + title: folder.title, + originalPath: folder.path, locale: i18n.locale, + localeTitle: i18n?.title || i18n.locale, localeSourcePath: sourcePath, path: join(folderPath, i18n.path) }); @@ -374,13 +376,11 @@ export class Folders { } } - const defaultTitle = defaultLocale?.title - ? `${folder.title} (${defaultLocale.title})` - : folder.title; contentFolders.push({ ...folder, - title: defaultTitle, + title: folder.title, locale: folder.defaultLocale, + localeTitle: defaultLocale?.title || folder.defaultLocale, originalPath: folder.path, localeSourcePath: sourcePath, path: join(folderPath, defaultLocale?.path || '') diff --git a/src/dashboardWebView/components/Header/FoldersFilter.tsx b/src/dashboardWebView/components/Header/FoldersFilter.tsx index c683cb77..4dd010db 100644 --- a/src/dashboardWebView/components/Header/FoldersFilter.tsx +++ b/src/dashboardWebView/components/Header/FoldersFilter.tsx @@ -4,7 +4,7 @@ import { FolderAtom, SettingsSelector } from '../../state'; import { MenuButton, MenuItem } from '../Menu'; import * as l10n from '@vscode/l10n'; import { LocalizationKey } from '../../../localization'; -import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '../../../components/shadcn/Dropdown'; +import { DropdownMenu, DropdownMenuContent } from '../../../components/shadcn/Dropdown'; export interface IFoldersFilterProps { } @@ -14,7 +14,11 @@ export const FoldersFilter: React.FunctionComponent< const DEFAULT_TYPE = l10n.t(LocalizationKey.dashboardHeaderFoldersDefault); const [crntFolder, setCrntFolder] = useRecoilState(FolderAtom); const settings = useRecoilValue(SettingsSelector); - const contentFolders = settings?.contentFolders || []; + + const contentFolders = React.useMemo(() => { + return settings?.contentFolders + .filter((folder, index, self) => index === self.findIndex((t) => t.originalPath === folder.originalPath)) || []; + }, [settings?.contentFolders]); if (contentFolders.length <= 1) { return null; diff --git a/src/helpers/Questions.ts b/src/helpers/Questions.ts index d9111034..0a31e7d0 100644 --- a/src/helpers/Questions.ts +++ b/src/helpers/Questions.ts @@ -8,6 +8,7 @@ import { Logger } from './Logger'; import { SponsorAi } from '../services/SponsorAI'; import * as l10n from '@vscode/l10n'; import { LocalizationKey } from '../localization'; +import { ContentFolder } from '../models'; export class Questions { /** @@ -130,7 +131,12 @@ export class Questions { let selectedFolder: string | undefined; if (folders.length > 1) { selectedFolder = await window.showQuickPick( - folders.map((f) => f.title), + folders.map((f: ContentFolder) => { + if (f.locale) { + return `${f.title} (${f.localeTitle || f.locale})`; + } + return f.title; + }), { title: l10n.t(LocalizationKey.helpersQuestionsSelectContentFolderQuickPickTitle), placeHolder: l10n.t( diff --git a/src/models/ContentFolder.ts b/src/models/ContentFolder.ts index a39662e6..646fe5e0 100644 --- a/src/models/ContentFolder.ts +++ b/src/models/ContentFolder.ts @@ -14,6 +14,7 @@ export interface ContentFolder { extended?: boolean; locale?: string; + localeTitle?: string; localeSourcePath?: string; defaultLocale?: string; locales: I18nConfig[]; From 48ada1c352a9d9dd64484247f0298121e6e5553a Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Fri, 23 Feb 2024 08:58:43 +0100 Subject: [PATCH 14/23] #760 - Return locales from settings --- src/commands/i18n.ts | 24 +++++++++++++++++++ src/constants/GeneralCommands.ts | 3 +++ src/dashboardWebView/hooks/usePages.tsx | 12 ++++------ .../dashboard/LocalizationListener.ts | 13 ++++++++++ 4 files changed, 44 insertions(+), 8 deletions(-) diff --git a/src/commands/i18n.ts b/src/commands/i18n.ts index bbe12970..931b1197 100644 --- a/src/commands/i18n.ts +++ b/src/commands/i18n.ts @@ -43,6 +43,30 @@ export class i18n { i18n.processedFiles = {}; } + /** + * Retrieves all the I18nConfig settings. + * + * @returns An array of I18nConfig settings. + */ + public static getAll() { + const i18nSettings = Settings.get(SETTING_CONTENT_I18N) || []; + + const folders = Folders.get(); + if (folders) { + for (const folder of folders) { + if (folder.locales) { + for (const locale of folder.locales) { + if (!i18nSettings.some((i18n) => i18n.locale === locale.locale)) { + i18nSettings.push(locale); + } + } + } + } + } + + return i18nSettings; + } + /** * Retrieves the I18nConfig settings from the application. * @returns An array of I18nConfig objects if settings are found, otherwise undefined. diff --git a/src/constants/GeneralCommands.ts b/src/constants/GeneralCommands.ts index 5b14a453..5db92184 100644 --- a/src/constants/GeneralCommands.ts +++ b/src/constants/GeneralCommands.ts @@ -21,6 +21,9 @@ export const GeneralCommands = { get: 'getSecret', set: 'setSecret' }, + content: { + locales: 'getContentLocales' + }, runCommand: 'runCommand', getLocalization: 'getLocalization', openOnWebsite: 'openOnWebsite' diff --git a/src/dashboardWebView/hooks/usePages.tsx b/src/dashboardWebView/hooks/usePages.tsx index 76cf0612..5d4a1792 100644 --- a/src/dashboardWebView/hooks/usePages.tsx +++ b/src/dashboardWebView/hooks/usePages.tsx @@ -22,7 +22,7 @@ import { DashboardMessage } from '../DashboardMessage'; import { EventData } from '@estruyf/vscode/dist/models'; import { parseWinPath } from '../../helpers/parseWinPath'; import { sortPages } from '../../utils/sortPages'; -import { ExtensionState } from '../../constants'; +import { ExtensionState, GeneralCommands } from '../../constants'; import { SortingOption } from '../models'; import { I18nConfig } from '../../models'; import { usePrevious } from '../../panelWebView/hooks/usePrevious'; @@ -268,14 +268,10 @@ export default function usePages(pages: Page[]) { } if (pages && pages.length > 0) { - // Store the locale information - const config: I18nConfig[] = []; - pages.forEach((page) => { - if (page.fmLocale && !config.some(locale => locale.locale === page.fmLocale?.locale)) { - config.push(page.fmLocale); - } + messageHandler.request(GeneralCommands.toVSCode.content.locales).then((config) => { + console.log('config', config); + setLocales(config || []); }); - setLocales(config); } }, [settings?.draftField, pages, sorting, search, tag, category, locale, filters, folder]); diff --git a/src/listeners/dashboard/LocalizationListener.ts b/src/listeners/dashboard/LocalizationListener.ts index 34cde614..f2ff7ad0 100644 --- a/src/listeners/dashboard/LocalizationListener.ts +++ b/src/listeners/dashboard/LocalizationListener.ts @@ -2,6 +2,7 @@ import { GeneralCommands } from '../../constants'; import { PostMessageData } from '../../models'; import { BaseListener } from './BaseListener'; import { getLocalizationFile } from '../../utils/getLocalizationFile'; +import { i18n } from '../../commands/i18n'; export class LocalizationListener extends BaseListener { /** @@ -13,6 +14,9 @@ export class LocalizationListener extends BaseListener { case GeneralCommands.toVSCode.getLocalization: this.getLocalization(); break; + case GeneralCommands.toVSCode.content.locales: + this.getContentLocales(msg.command, msg.requestId); + break; } } @@ -21,4 +25,13 @@ export class LocalizationListener extends BaseListener { this.sendMsg(GeneralCommands.toWebview.setLocalization as any, fileContents); } + + private static async getContentLocales(command: string, requestId?: string) { + if (!command || !requestId) { + return; + } + + const config = i18n.getAll(); + this.sendRequest(command as any, requestId, config); + } } From 3f88b05a1c71fb9f16204d3c15e56148412b85d8 Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Fri, 23 Feb 2024 09:26:41 +0100 Subject: [PATCH 15/23] i10n provider for generic config --- src/dashboardWebView/components/App.tsx | 4 +- src/dashboardWebView/hooks/useMessages.tsx | 12 +---- src/dashboardWebView/hooks/usePages.tsx | 1 - src/dashboardWebView/index.tsx | 33 +++++++----- .../providers/I10nProvider.tsx | 52 +++++++++++++++++++ .../dashboard/LocalizationListener.ts | 11 ++-- src/listeners/panel/LocalizationListener.ts | 11 ++-- src/panelWebView/ViewPanel.tsx | 3 +- src/panelWebView/hooks/useMessages.tsx | 9 ---- src/panelWebView/index.tsx | 9 ++-- 10 files changed, 96 insertions(+), 49 deletions(-) create mode 100644 src/dashboardWebView/providers/I10nProvider.tsx diff --git a/src/dashboardWebView/components/App.tsx b/src/dashboardWebView/components/App.tsx index 9b4cdfeb..c0f14260 100644 --- a/src/dashboardWebView/components/App.tsx +++ b/src/dashboardWebView/components/App.tsx @@ -30,7 +30,7 @@ export interface IAppProps { export const App: React.FunctionComponent = ({ showWelcome }: React.PropsWithChildren) => { - const { pages, settings, localeReady } = useMessages(); + const { pages, settings } = useMessages(); const view = useRecoilValue(DashboardViewSelector); const mode = useRecoilValue(ModeAtom); const [isDevMode, setIsDevMode] = useState(false); @@ -70,7 +70,7 @@ export const App: React.FunctionComponent = ({ } }, []); - if (!settings || !localeReady) { + if (!settings) { return ; } diff --git a/src/dashboardWebView/hooks/useMessages.tsx b/src/dashboardWebView/hooks/useMessages.tsx index f9f41d7a..612a43e4 100644 --- a/src/dashboardWebView/hooks/useMessages.tsx +++ b/src/dashboardWebView/hooks/useMessages.tsx @@ -15,7 +15,6 @@ import { Messenger } from '@estruyf/vscode/dist/client'; import { EventData } from '@estruyf/vscode/dist/models'; import { NavigationType } from '../models'; import { GeneralCommands } from '../../constants'; -import * as l10n from '@vscode/l10n'; export default function useMessages() { const [loading, setLoading] = useRecoilState(LoadingAtom); @@ -25,7 +24,6 @@ export default function useMessages() { const [, setMode] = useRecoilState(ModeAtom); const [, setView] = useRecoilState(DashboardViewAtom); const [, setSearchReady] = useRecoilState(SearchReadyAtom); - const [localeReady, setLocaleReady] = useState(false); const messageListener = (event: MessageEvent>) => { const message = event.data; @@ -61,12 +59,6 @@ export default function useMessages() { case GeneralCommands.toWebview.setMode: setMode(message.payload); break; - case GeneralCommands.toWebview.setLocalization: - l10n.config({ - contents: message.payload - }); - setLocaleReady(true); - break; } }; @@ -78,7 +70,6 @@ export default function useMessages() { Messenger.send(DashboardMessage.getTheme); Messenger.send(DashboardMessage.getData); Messenger.send(DashboardMessage.getMode); - Messenger.send(GeneralCommands.toVSCode.getLocalization); return () => { Messenger.unlisten(messageListener); @@ -89,7 +80,6 @@ export default function useMessages() { loading, pages, viewData, - settings, - localeReady + settings }; } diff --git a/src/dashboardWebView/hooks/usePages.tsx b/src/dashboardWebView/hooks/usePages.tsx index 5d4a1792..1917f66e 100644 --- a/src/dashboardWebView/hooks/usePages.tsx +++ b/src/dashboardWebView/hooks/usePages.tsx @@ -269,7 +269,6 @@ export default function usePages(pages: Page[]) { if (pages && pages.length > 0) { messageHandler.request(GeneralCommands.toVSCode.content.locales).then((config) => { - console.log('config', config); setLocales(config || []); }); } diff --git a/src/dashboardWebView/index.tsx b/src/dashboardWebView/index.tsx index 42ad1420..32be46fc 100644 --- a/src/dashboardWebView/index.tsx +++ b/src/dashboardWebView/index.tsx @@ -12,6 +12,7 @@ import { SettingsProvider } from './providers/SettingsProvider'; import { CustomPanelViewResult } from '../models'; import { Chatbot } from './components/Chatbot/Chatbot'; import { updateCssVariables } from './utils'; +import { I10nProvider } from './providers/I10nProvider'; declare const acquireVsCodeApi: () => { getState: () => T; @@ -50,7 +51,7 @@ export const routePaths: { [name: string]: string } = { settings: '/settings', }; -const mutationObserver = new MutationObserver((mutationsList, observer) => { +const mutationObserver = new MutationObserver((_, __) => { updateCssVariables(); }); @@ -88,17 +89,21 @@ if (elm) { if (type === 'preview') { render( - - - , elm); + + + + + , elm); } else if (type === 'chatbot') { render( - - - , elm); + + + + + , elm); } else { render( @@ -106,9 +111,11 @@ if (elm) { initialEntries={Object.keys(routePaths).map((key: string) => routePaths[key]) as string[]} initialIndex={1} > - - - + + + + + , elm diff --git a/src/dashboardWebView/providers/I10nProvider.tsx b/src/dashboardWebView/providers/I10nProvider.tsx new file mode 100644 index 00000000..c90ecc52 --- /dev/null +++ b/src/dashboardWebView/providers/I10nProvider.tsx @@ -0,0 +1,52 @@ +import * as React from 'react'; +import { messageHandler } from '@estruyf/vscode/dist/client'; +import { GeneralCommands } from '../../constants'; +import * as l10n from '@vscode/l10n'; + +interface I10nProviderProps { } + +const I10nContext = React.createContext(undefined); + +const I10nProvider: React.FunctionComponent = ({ children }: React.PropsWithChildren) => { + const [localeReady, setLocaleReady] = React.useState(false); + + React.useEffect(() => { + messageHandler.request(GeneralCommands.toVSCode.getLocalization).then((contents) => { + if (contents) { + l10n.config({ + contents + }); + + setTimeout(() => { + setLocaleReady(true); + }, 0); + } + }).catch(() => { + setLocaleReady(false); + throw new Error('Error getting localization'); + }); + }, []); + + return ( + + { + localeReady && children + } + + ) +}; + +const useI10nContext = (): I10nProviderProps => { + const loadFunc = React.useContext(I10nContext); + + if (loadFunc === undefined) { + throw new Error('useI10nContext must be used within the I10nProvider'); + } + + return loadFunc; +}; + +I10nContext.displayName = 'I10nContext'; +I10nProvider.displayName = 'I10nProvider'; + +export { I10nProvider, useI10nContext }; diff --git a/src/listeners/dashboard/LocalizationListener.ts b/src/listeners/dashboard/LocalizationListener.ts index f2ff7ad0..0a8ca42f 100644 --- a/src/listeners/dashboard/LocalizationListener.ts +++ b/src/listeners/dashboard/LocalizationListener.ts @@ -12,7 +12,7 @@ export class LocalizationListener extends BaseListener { public static process(msg: PostMessageData) { switch (msg.command) { case GeneralCommands.toVSCode.getLocalization: - this.getLocalization(); + this.getLocalization(msg.command, msg.requestId); break; case GeneralCommands.toVSCode.content.locales: this.getContentLocales(msg.command, msg.requestId); @@ -20,10 +20,13 @@ export class LocalizationListener extends BaseListener { } } - public static async getLocalization() { - const fileContents = await getLocalizationFile(); + public static async getLocalization(command: string, requestId?: string) { + if (!command || !requestId) { + return; + } - this.sendMsg(GeneralCommands.toWebview.setLocalization as any, fileContents); + const fileContents = await getLocalizationFile(); + this.sendRequest(command as any, requestId, fileContents); } private static async getContentLocales(command: string, requestId?: string) { diff --git a/src/listeners/panel/LocalizationListener.ts b/src/listeners/panel/LocalizationListener.ts index 34cde614..6ac0f01f 100644 --- a/src/listeners/panel/LocalizationListener.ts +++ b/src/listeners/panel/LocalizationListener.ts @@ -11,14 +11,17 @@ export class LocalizationListener extends BaseListener { public static process(msg: PostMessageData) { switch (msg.command) { case GeneralCommands.toVSCode.getLocalization: - this.getLocalization(); + this.getLocalization(msg.command, msg.requestId); break; } } - public static async getLocalization() { - const fileContents = await getLocalizationFile(); + public static async getLocalization(command: string, requestId?: string) { + if (!command || !requestId) { + return; + } - this.sendMsg(GeneralCommands.toWebview.setLocalization as any, fileContents); + const fileContents = await getLocalizationFile(); + this.sendRequest(command, requestId, fileContents); } } diff --git a/src/panelWebView/ViewPanel.tsx b/src/panelWebView/ViewPanel.tsx index 2b32ae52..75bf377c 100644 --- a/src/panelWebView/ViewPanel.tsx +++ b/src/panelWebView/ViewPanel.tsx @@ -33,7 +33,6 @@ export const ViewPanel: React.FunctionComponent = ( folderAndFiles, focusElm, unsetFocus, - localeReady, mode } = useMessages(); const prevMediaSelection = usePrevious(mediaSelecting); @@ -83,7 +82,7 @@ export const ViewPanel: React.FunctionComponent = ( ); } - if (loading && !localeReady) { + if (loading) { return ; } diff --git a/src/panelWebView/hooks/useMessages.tsx b/src/panelWebView/hooks/useMessages.tsx index 9e36f43c..99b6d93b 100644 --- a/src/panelWebView/hooks/useMessages.tsx +++ b/src/panelWebView/hooks/useMessages.tsx @@ -10,13 +10,11 @@ import { Messenger } from '@estruyf/vscode/dist/client'; import { EventData } from '@estruyf/vscode/dist/models'; import { useRecoilState } from 'recoil'; import { PanelSettingsAtom } from '../state'; -import * as l10n from '@vscode/l10n'; export default function useMessages() { const [metadata, setMetadata] = useState({}); const [settings, setSettings] = useRecoilState(PanelSettingsAtom); const [loading, setLoading] = useState(false); - const [localeReady, setLocaleReady] = useState(false); const [focusElm, setFocus] = useState(null); const [folderAndFiles, setFolderAndFiles] = useState(undefined); const [mediaSelecting, setMediaSelecting] = useState(undefined); @@ -52,12 +50,6 @@ export default function useMessages() { case GeneralCommands.toWebview.setMode: setMode(message.payload); break; - case GeneralCommands.toWebview.setLocalization: - l10n.config({ - contents: message.payload - }) - setLocaleReady(true) - break; } }; @@ -99,7 +91,6 @@ export default function useMessages() { loading, mediaSelecting, mode, - localeReady, unsetFocus }; } diff --git a/src/panelWebView/index.tsx b/src/panelWebView/index.tsx index dc8385e9..c965f045 100644 --- a/src/panelWebView/index.tsx +++ b/src/panelWebView/index.tsx @@ -17,6 +17,7 @@ import '@bendera/vscode-webview-elements/dist/vscode-table-row.js'; import '@bendera/vscode-webview-elements/dist/vscode-table-cell.js'; import '@bendera/vscode-webview-elements/dist/vscode-collapsible.js'; import '@bendera/vscode-webview-elements/dist/vscode-label.js'; +import { I10nProvider } from '../dashboardWebView/providers/I10nProvider'; // import '@bendera/vscode-webview-elements/dist/vscode-checkbox.js'; // import '@vscode/webview-ui-toolkit/dist/esm/checkbox'; @@ -51,9 +52,11 @@ if (elm) { } render( - - - , + + + + + , elm ); } From 83b9f2380eaf70c5fe330d15fe27c85c4ea35cb7 Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Fri, 23 Feb 2024 16:37:15 +0100 Subject: [PATCH 16/23] Date format fix --- src/helpers/ContentType.ts | 11 +++++++++++ src/listeners/panel/DataListener.ts | 16 +--------------- .../components/Fields/DateTimeField.tsx | 4 ++-- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/src/helpers/ContentType.ts b/src/helpers/ContentType.ts index 64980049..8c63df1d 100644 --- a/src/helpers/ContentType.ts +++ b/src/helpers/ContentType.ts @@ -937,6 +937,13 @@ export class ContentType { contentType ); + let isTypeSet = false; + if (data.type) { + isTypeSet = true; + } else { + data.type = contentType.name; + } + const article: ParsedFrontMatter = { content: '', data: Object.assign({}, data), @@ -945,6 +952,10 @@ export class ContentType { data = ArticleHelper.updateDates(article); + if (isTypeSet) { + delete data.type; + } + if (contentType.name !== DEFAULT_CONTENT_TYPE_NAME) { data['type'] = contentType.name; } diff --git a/src/listeners/panel/DataListener.ts b/src/listeners/panel/DataListener.ts index 8af8a125..071b1926 100644 --- a/src/listeners/panel/DataListener.ts +++ b/src/listeners/panel/DataListener.ts @@ -306,7 +306,6 @@ export class DataListener extends BaseListener { } } - const dateFields = ContentType.findFieldsByTypeDeep(contentType.fields, 'datetime'); const imageFields = ContentType.findFieldsByTypeDeep(contentType.fields, 'image'); const fileFields = ContentType.findFieldsByTypeDeep(contentType.fields, 'file'); const fieldsWithEmojiEncoding = contentType.fields.filter((f) => f.encodeEmoji); @@ -314,13 +313,6 @@ export class DataListener extends BaseListener { // Support multi-level fields const parentObj = DataListener.getParentObject(article.data, article, parents, blockData); - const dateFieldsArray = dateFields.find((f: Field[]) => { - const lastField = f?.[f.length - 1]; - if (lastField) { - return lastField.name === field; - } - }); - // Check multi-image fields const multiImageFieldsArray = imageFields.find((f: Field[]) => { const lastField = f?.[f.length - 1]; @@ -338,13 +330,7 @@ export class DataListener extends BaseListener { }); // Check date fields - if (dateFieldsArray && dateFieldsArray.length > 0) { - for (const dateField of dateFieldsArray) { - if (field === dateField.name && value) { - parentObj[field] = Article.formatDate(new Date(value), dateField.dateFormat); - } - } - } else if (multiImageFieldsArray || multiFileFieldsArray) { + if (multiImageFieldsArray || multiFileFieldsArray) { const fields = multiImageFieldsArray && multiImageFieldsArray.length > 0 ? multiImageFieldsArray diff --git a/src/panelWebView/components/Fields/DateTimeField.tsx b/src/panelWebView/components/Fields/DateTimeField.tsx index e80b32be..213ea7d2 100644 --- a/src/panelWebView/components/Fields/DateTimeField.tsx +++ b/src/panelWebView/components/Fields/DateTimeField.tsx @@ -11,7 +11,7 @@ import { LocalizationKey } from '../../../localization'; export interface IDateTimeFieldProps extends BaseFieldProps { format?: string; - onChange: (date: Date) => void; + onChange: (date: string) => void; } type InputProps = JSX.IntrinsicElements['input']; @@ -37,7 +37,7 @@ export const DateTimeField: React.FunctionComponent = ({ const onDateChange = React.useCallback((date: Date) => { setDateValue(date); - onChange(date); + onChange(DateHelper.format(date, format || DEFAULT_FORMAT) || ""); }, [format, onChange]); const showRequiredState = useMemo(() => { From f19bd07359c489cfa722826f68a8144438a82ecb Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Fri, 23 Feb 2024 19:13:34 +0100 Subject: [PATCH 17/23] Support for using the `fieldCollection` field in a `block` field --- CHANGELOG.md | 1 + .../components/DataBlock/DataBlockField.tsx | 9 ++- .../components/Fields/FieldCollection.tsx | 71 +++++++++++++++++++ .../components/Fields/WrapperField.tsx | 24 ++++++- src/panelWebView/components/Fields/index.ts | 2 + 5 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 src/panelWebView/components/Fields/FieldCollection.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 40c6997a..d102eaa5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ - [#741](https://github.com/estruyf/vscode-front-matter/issues/741): Added message on the content dashboard when content is processed - [#747](https://github.com/estruyf/vscode-front-matter/issues/747): The `@frontmatter/extensibility` dependency now supports scripts for placeholders - [#752](https://github.com/estruyf/vscode-front-matter/issues/752): Placeholder support in default `list` field values +- Support for using the `fieldCollection` field in a `block` field ### 🐞 Fixes diff --git a/src/panelWebView/components/DataBlock/DataBlockField.tsx b/src/panelWebView/components/DataBlock/DataBlockField.tsx index 2145c07e..083890a8 100644 --- a/src/panelWebView/components/DataBlock/DataBlockField.tsx +++ b/src/panelWebView/components/DataBlock/DataBlockField.tsx @@ -95,6 +95,13 @@ export const DataBlockField: React.FunctionComponent = ({ // Delete the field group to have it added at the end delete data['fieldGroup']; + // Remove the empty fields + Object.keys(data).forEach((key) => { + if (data[key] === undefined || data[key] === null || Object.keys(data[key]).length === 0) { + delete data[key]; + } + }); + if (selectedIndex !== null && selectedIndex !== undefined && dataClone.length > 0) { dataClone[selectedIndex] = { ...data, @@ -306,7 +313,7 @@ export const DataBlockField: React.FunctionComponent = ({ {selectedGroup?.fields && fieldsRenderer( selectedGroup?.fields, - selectedBlockData || {}, + Object.assign({}, selectedBlockData) || {}, [...parentFields, field.name], { parentFields: [...parentFields, field.name], diff --git a/src/panelWebView/components/Fields/FieldCollection.tsx b/src/panelWebView/components/Fields/FieldCollection.tsx new file mode 100644 index 00000000..40c74cd1 --- /dev/null +++ b/src/panelWebView/components/Fields/FieldCollection.tsx @@ -0,0 +1,71 @@ +import * as React from 'react'; +import { BlockFieldData, Field, PanelSettings } from '../../../models'; +import { IMetadata } from '../Metadata'; +import { FieldTitle } from './FieldTitle'; + +export interface IFieldCollectionProps { + field: Field; + parent: IMetadata; + parentFields: string[]; + blockData: BlockFieldData | undefined; + settings: PanelSettings; + renderFields: ( + ctFields: Field[], + parent: IMetadata, + parentFields: string[], + blockData?: BlockFieldData, + onFieldUpdate?: (field: string | undefined, value: any, parents: string[]) => void, + parentBlock?: string | null + ) => (JSX.Element | null)[] | undefined; + onChange: (field: string | undefined, value: any, parents: string[]) => void; +} + +export const FieldCollection: React.FunctionComponent = ({ + field, + parent, + parentFields, + blockData, + settings, + renderFields, + onChange +}: React.PropsWithChildren) => { + const [fields, setFields] = React.useState([]); + + React.useEffect(() => { + if (!settings.fieldGroups) { + return + } + + const group = settings.fieldGroups.find((group) => group.id === field.fieldGroup); + if (group) { + setFields(group.fields); + } + }, [field, settings?.fieldGroups]); + + if (!fields || fields.length === 0) { + return null; + } + + return ( +
+ + + {field.description && ( +

{field.description}

+ )} + + {renderFields( + fields, + parent, + [...parentFields, field.name], + blockData, + onChange + )} +
+ ); +}; \ No newline at end of file diff --git a/src/panelWebView/components/Fields/WrapperField.tsx b/src/panelWebView/components/Fields/WrapperField.tsx index 7f9601cb..00dbbad5 100644 --- a/src/panelWebView/components/Fields/WrapperField.tsx +++ b/src/panelWebView/components/Fields/WrapperField.tsx @@ -26,7 +26,8 @@ import { PreviewImageField, PreviewImageValue, NumberField, - CustomField + CustomField, + FieldCollection } from '.'; import { fieldWhenClause } from '../../../utils/fieldWhenClause'; import { ContentTypeRelationshipField } from './ContentTypeRelationshipField'; @@ -521,6 +522,27 @@ export const WrapperField: React.FunctionComponent = ({ } else { return null; } + } else if (field.type === "fieldCollection") { + if (!parent[field.name]) { + parent[field.name] = {}; + } + + const subMetadata = parent[field.name] as IMetadata; + + return ( + + + + ); } else { console.warn(l10n.t(LocalizationKey.panelFieldsWrapperFieldUnknown, field.type)); return null; diff --git a/src/panelWebView/components/Fields/index.ts b/src/panelWebView/components/Fields/index.ts index d4a6fd60..35a8ebd5 100644 --- a/src/panelWebView/components/Fields/index.ts +++ b/src/panelWebView/components/Fields/index.ts @@ -1,9 +1,11 @@ export * from './ChoiceButton'; export * from './ChoiceField'; +export * from './ContentTypeRelationshipField'; export * from './CustomField'; export * from './DataFileField'; export * from './DateTimeField'; export * from './DraftField'; +export * from './FieldCollection'; export * from './FieldMessage'; export * from './FieldTitle'; export * from './FileField'; From b02a80c28eba4c0ac467a7bc4eb4355d2eaa80e0 Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Sat, 24 Feb 2024 13:14:45 +0100 Subject: [PATCH 18/23] #760 - Add locale to recently modified panel section --- src/commands/Folders.ts | 4 +++- src/models/PanelSettings.ts | 2 ++ src/panelWebView/components/FolderAndFiles.tsx | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/commands/Folders.ts b/src/commands/Folders.ts index 2663fa8a..396b04b1 100644 --- a/src/commands/Folders.ts +++ b/src/commands/Folders.ts @@ -670,7 +670,9 @@ export class Folders { return { title: folder.title, files: files.length, - lastModified: fileStats + lastModified: fileStats, + locale: folder.locale, + localeTitle: folder.localeTitle }; } } diff --git a/src/models/PanelSettings.ts b/src/models/PanelSettings.ts index a13f082c..fca2e8a7 100644 --- a/src/models/PanelSettings.ts +++ b/src/models/PanelSettings.ts @@ -191,6 +191,8 @@ export interface FolderInfo { title: string; files: number; lastModified: FileInfo[]; + locale?: string; + localeTitle?: string; } export interface FileInfo extends FileStat { diff --git a/src/panelWebView/components/FolderAndFiles.tsx b/src/panelWebView/components/FolderAndFiles.tsx index d510fb80..dea44c12 100644 --- a/src/panelWebView/components/FolderAndFiles.tsx +++ b/src/panelWebView/components/FolderAndFiles.tsx @@ -29,7 +29,7 @@ const FolderAndFiles: React.FunctionComponent = ({ {folder.lastModified ? (
From c298f2fd696309aa58c6d66447ea1ae59b54657d Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Sat, 24 Feb 2024 13:46:26 +0100 Subject: [PATCH 19/23] #760 - Windows file path fixes in multilingual --- l10n/bundle.l10n.json | 1 + src/commands/Folders.ts | 6 +-- src/commands/i18n.ts | 3 +- src/helpers/ContentType.ts | 2 +- src/helpers/Questions.ts | 40 ++++++++++++++----- src/localization/localization.enum.ts | 4 ++ .../components/FolderAndFiles.tsx | 2 +- 7 files changed, 43 insertions(+), 15 deletions(-) diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 2075fef4..523f8d44 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -698,6 +698,7 @@ "helpers.questions.selectContentType.quickPick.title": "Content type", "helpers.questions.selectContentType.quickPick.placeholder": "Select the content type to create your new content", "helpers.questions.selectContentType.noSelection.warning": "No content type was selected.", + "helpers.questions.selectContentType.quickPick.error.noContentTypes": "There are no matching content types configured for this folder.", "helpers.seoHelper.checkLength.diagnostic.message": "Article {0} is longer than {1} characters (current length: {2}). For SEO reasons, it would be better to make it less than {1} characters.", diff --git a/src/commands/Folders.ts b/src/commands/Folders.ts index 396b04b1..21b3410a 100644 --- a/src/commands/Folders.ts +++ b/src/commands/Folders.ts @@ -99,7 +99,7 @@ export class Folders { } const folders = Folders.get().filter((f) => !f.disableCreation); - const location = folders.find((f) => f.title === selectedFolder); + const location = folders.find((f) => f.path === selectedFolder.path); if (location) { const folderPath = Folders.getFolderPath(Uri.file(location.path)); if (folderPath) { @@ -370,7 +370,7 @@ export class Folders { locale: i18n.locale, localeTitle: i18n?.title || i18n.locale, localeSourcePath: sourcePath, - path: join(folderPath, i18n.path) + path: parseWinPath(join(folderPath, i18n.path)) }); } } @@ -383,7 +383,7 @@ export class Folders { localeTitle: defaultLocale?.title || folder.defaultLocale, originalPath: folder.path, localeSourcePath: sourcePath, - path: join(folderPath, defaultLocale?.path || '') + path: parseWinPath(join(folderPath, defaultLocale?.path || '')) }); contentFolders.push(...localeFolders); diff --git a/src/commands/i18n.ts b/src/commands/i18n.ts index 931b1197..e136b104 100644 --- a/src/commands/i18n.ts +++ b/src/commands/i18n.ts @@ -424,7 +424,8 @@ export class i18n { ); if (!translations || translations.length < 3) { - throw new Error('Invalid response'); + resolve(article); + return; } article.data.title = article.data.title ? translations[0] : ''; diff --git a/src/helpers/ContentType.ts b/src/helpers/ContentType.ts index 8c63df1d..525405c1 100644 --- a/src/helpers/ContentType.ts +++ b/src/helpers/ContentType.ts @@ -95,7 +95,7 @@ export class ContentType { const contentTypes = ContentType.getAll(); const folders = Folders.get().filter((f) => !f.disableCreation); - const folder = folders.find((f) => f.title === selectedFolder); + const folder = folders.find((f) => f.path === selectedFolder.path); if (!folder) { return; diff --git a/src/helpers/Questions.ts b/src/helpers/Questions.ts index 0a31e7d0..97b1088f 100644 --- a/src/helpers/Questions.ts +++ b/src/helpers/Questions.ts @@ -10,6 +10,11 @@ import * as l10n from '@vscode/l10n'; import { LocalizationKey } from '../localization'; import { ContentFolder } from '../models'; +interface FolderQuickPickItem extends QuickPickItem { + path: string; + locale?: string; +} + export class Questions { /** * Yes/No question @@ -125,18 +130,27 @@ export class Questions { */ public static async SelectContentFolder( showWarning: boolean = true - ): Promise { + ): Promise { let folders = Folders.get().filter((f) => !f.disableCreation); - let selectedFolder: string | undefined; + let selectedFolder: FolderQuickPickItem | undefined; if (folders.length > 1) { + const folderOptions = folders.map((f: ContentFolder) => { + if (f.locale) { + return { + label: `${f.title} (${f.localeTitle || f.locale})`, + locale: f.locale, + path: f.path + } as FolderQuickPickItem; + } + return { + label: f.title, + path: f.path + } as FolderQuickPickItem; + }); + selectedFolder = await window.showQuickPick( - folders.map((f: ContentFolder) => { - if (f.locale) { - return `${f.title} (${f.localeTitle || f.locale})`; - } - return f.title; - }), + folderOptions, { title: l10n.t(LocalizationKey.helpersQuestionsSelectContentFolderQuickPickTitle), placeHolder: l10n.t( @@ -146,7 +160,10 @@ export class Questions { } ); } else if (folders.length === 1) { - selectedFolder = folders[0].title; + selectedFolder = { + label: folders[0].title, + path: folders[0].path + } as FolderQuickPickItem; } else { // When no page folders are found, the welcome dashboard is shown return; @@ -195,6 +212,11 @@ export class Questions { label: contentType.name })); + if (options.length === 0) { + Notifications.error(LocalizationKey.helpersQuestionsSelectContentTypeQuickPickErrorNoContentTypes); + return; + } + const selectedOption = await window.showQuickPick(options, { title: l10n.t(LocalizationKey.helpersQuestionsSelectContentTypeQuickPickTitle), placeHolder: l10n.t(LocalizationKey.helpersQuestionsSelectContentTypeQuickPickPlaceholder), diff --git a/src/localization/localization.enum.ts b/src/localization/localization.enum.ts index d3ab017f..4d3f8fa4 100644 --- a/src/localization/localization.enum.ts +++ b/src/localization/localization.enum.ts @@ -2296,6 +2296,10 @@ export enum LocalizationKey { * No content type was selected. */ helpersQuestionsSelectContentTypeNoSelectionWarning = 'helpers.questions.selectContentType.noSelection.warning', + /** + * There are no matching content types configured for this folder. + */ + helpersQuestionsSelectContentTypeQuickPickErrorNoContentTypes = 'helpers.questions.selectContentType.quickPick.error.noContentTypes', /** * Article {0} is longer than {1} characters (current length: {2}). For SEO reasons, it would be better to make it less than {1} characters. */ diff --git a/src/panelWebView/components/FolderAndFiles.tsx b/src/panelWebView/components/FolderAndFiles.tsx index dea44c12..ee484911 100644 --- a/src/panelWebView/components/FolderAndFiles.tsx +++ b/src/panelWebView/components/FolderAndFiles.tsx @@ -29,7 +29,7 @@ const FolderAndFiles: React.FunctionComponent = ({ {folder.lastModified ? (
From 59528a3db01be8d34dc40638e6cf827090e31986 Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Sat, 24 Feb 2024 14:10:14 +0100 Subject: [PATCH 20/23] #746 - Slug handling when none is defined --- src/commands/Article.ts | 18 +++++++++++++++++- src/helpers/SlugHelper.ts | 3 +++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/commands/Article.ts b/src/commands/Article.ts index b4aad4dc..d8daeab7 100644 --- a/src/commands/Article.ts +++ b/src/commands/Article.ts @@ -10,7 +10,8 @@ import { SETTING_SLUG_PREFIX, SETTING_SLUG_SUFFIX, SETTING_CONTENT_PLACEHOLDERS, - TelemetryEvent + TelemetryEvent, + SETTING_SLUG_TEMPLATE } from './../constants'; import * as vscode from 'vscode'; import { CustomPlaceholder, Field } from '../models'; @@ -260,6 +261,21 @@ export class Article { return; } + const slugTemplate = Settings.get(SETTING_SLUG_TEMPLATE); + if (slugTemplate) { + if (slugTemplate === '{{title}}') { + const article = ArticleHelper.getFrontMatter(editor); + if (article?.data?.title) { + return article.data.title.toLowerCase().replace(/\s/g, '-'); + } + } else { + const article = ArticleHelper.getFrontMatter(editor); + if (article?.data) { + return SlugHelper.createSlug(article.data.title, article.data, slugTemplate); + } + } + } + const file = parseWinPath(editor.document.fileName); if (!isValidFile(file)) { diff --git a/src/helpers/SlugHelper.ts b/src/helpers/SlugHelper.ts index 6cf4ac36..9eed5c4d 100644 --- a/src/helpers/SlugHelper.ts +++ b/src/helpers/SlugHelper.ts @@ -24,6 +24,9 @@ export class SlugHelper { if (slugTemplate) { if (slugTemplate.includes('{{title}}')) { const regex = new RegExp('{{title}}', 'g'); + slugTemplate = slugTemplate.replace(regex, articleTitle.toLowerCase().replace(/\s/g, '-')); + } else if (slugTemplate.includes('{{seoTitle}}')) { + const regex = new RegExp('{{seoTitle}}', 'g'); slugTemplate = slugTemplate.replace(regex, SlugHelper.slugify(articleTitle)); } From 003d93b0f20db33b16bf0df4e168479a7133ed2f Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Mon, 26 Feb 2024 15:13:08 +0100 Subject: [PATCH 21/23] Added telemetry information --- README.beta.md | 15 +++++++++++++++ README.md | 15 +++++++++++++++ src/constants/SentryIgnore.ts | 3 ++- src/helpers/Telemetry.ts | 6 +++++- 4 files changed, 37 insertions(+), 2 deletions(-) diff --git a/README.beta.md b/README.beta.md index 5b6ace70..b9b010b7 100644 --- a/README.beta.md +++ b/README.beta.md @@ -171,6 +171,21 @@ You can open showcase issues for the following things: - Share an article/video/webcast/... that explains how you use Front Matter; - Got something else to share? Open an issue and we can see where it fits on our website. +## 📊 Telemetry + +The Front Matter CMS extension collects telemetry data to help us build a better understand which features from the CMS are used. The extension respects the `telemetry.enableTelemetry` setting which you can learn more about in the [Visual Studio Code FAQ](https://aka.ms/vscode-remote/telemetry), or you can only disable it for the extension by configuring the `frontMatter.telemetry.disable` setting. + +We only collect the following data: + +- Type of event +- Extension title (main or beta) +- Extension version + +No user-specific data is collected, you can check the telemetry implementation in the following files: + +- [Telemetry class](https://github.com/estruyf/vscode-front-matter/blob/59528a3db01be8d34dc40638e6cf827090e31986/src/helpers/Telemetry.ts) +- [Metrics API](https://github.com/FrontMatter/web-documentation-nextjs/blob/main/pages/api/metrics.ts) + ## 👉 Contributors 🤘

diff --git a/README.md b/README.md index 73ce1070..47238f4c 100644 --- a/README.md +++ b/README.md @@ -169,6 +169,21 @@ You can open showcase issues for the following things: - Share an article/video/webcast/... that explains how you use Front Matter; - Got something else to share? Open an issue and we can see where it fits on our website. +## 📊 Telemetry + +The Front Matter CMS extension collects telemetry data to help us build a better understand which features from the CMS are used. The extension respects the `telemetry.enableTelemetry` setting which you can learn more about in the [Visual Studio Code FAQ](https://aka.ms/vscode-remote/telemetry), or you can only disable it for the extension by configuring the `frontMatter.telemetry.disable` setting. + +We only collect the following data: + +- Type of event +- Extension title (main or beta) +- Extension version + +No user-specific data is collected, you can check the telemetry implementation in the following files: + +- [Telemetry class](https://github.com/estruyf/vscode-front-matter/blob/59528a3db01be8d34dc40638e6cf827090e31986/src/helpers/Telemetry.ts) +- [Metrics API](https://github.com/FrontMatter/web-documentation-nextjs/blob/main/pages/api/metrics.ts) + ## 👉 Contributors 🤘

diff --git a/src/constants/SentryIgnore.ts b/src/constants/SentryIgnore.ts index 76a3a848..d9016f6c 100644 --- a/src/constants/SentryIgnore.ts +++ b/src/constants/SentryIgnore.ts @@ -1,5 +1,6 @@ export const SentryIgnore = [ `ResizeObserver loop limit exceeded`, `Cannot read properties of undefined (reading 'unobserve')`, - `TypeError: Cannot read properties of undefined (reading 'unobserve')` + `TypeError: Cannot read properties of undefined (reading 'unobserve')`, + `ResizeObserver loop completed with undelivered notifications.` ]; diff --git a/src/helpers/Telemetry.ts b/src/helpers/Telemetry.ts index 245e058a..a863a69c 100644 --- a/src/helpers/Telemetry.ts +++ b/src/helpers/Telemetry.ts @@ -1,3 +1,4 @@ +import { workspace } from 'vscode'; import { Extension, Settings } from '.'; import { EXTENSION_BETA_ID, EXTENSION_ID, SETTING_TELEMETRY_DISABLE } from '../constants'; @@ -30,8 +31,11 @@ export class Telemetry { * @returns */ public static send(eventName: string, properties?: any) { + const config = workspace.getConfiguration('telemetry'); + const isVscodeEnable = config.get<'off' | undefined>('enableTelemetry'); + const isDisabled = Settings.get(SETTING_TELEMETRY_DISABLE); - if (isDisabled) { + if (isDisabled || isVscodeEnable === 'off') { return; } From 63e296d62f11be73ac86d9e823084247952a7ddc Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Mon, 26 Feb 2024 15:49:14 +0100 Subject: [PATCH 22/23] Sentry updates --- src/commands/Dashboard.ts | 2 +- src/dashboardWebView/index.tsx | 15 ++++----------- src/helpers/Telemetry.ts | 24 +++++++++++++++++++----- src/panelWebView/PanelProvider.ts | 4 +++- src/panelWebView/index.tsx | 19 ++++++------------- src/utils/sentryInit.ts | 21 +++++++++++++++++++++ 6 files changed, 54 insertions(+), 31 deletions(-) create mode 100644 src/utils/sentryInit.ts diff --git a/src/commands/Dashboard.ts b/src/commands/Dashboard.ts index 41e7dd5c..edc8912c 100644 --- a/src/commands/Dashboard.ts +++ b/src/commands/Dashboard.ts @@ -358,7 +358,7 @@ export class Dashboard { version.usedVersion ? '' : `data-showWelcome="true"` } ${ experimental ? `data-experimental="${experimental}"` : '' - } data-webview-url="${webviewUrl}" >

+ } data-webview-url="${webviewUrl}" data-is-crash-disabled="${!Telemetry.isVscodeEnabled()}" >
${(scriptsToLoad || []) .map((script) => { diff --git a/src/dashboardWebView/index.tsx b/src/dashboardWebView/index.tsx index 32be46fc..4628c94c 100644 --- a/src/dashboardWebView/index.tsx +++ b/src/dashboardWebView/index.tsx @@ -3,8 +3,6 @@ import { render } from 'react-dom'; import { RecoilRoot } from 'recoil'; import { App } from './components/App'; import * as Sentry from '@sentry/react'; -import { Integrations } from '@sentry/tracing'; -import { SENTRY_LINK, SentryIgnore } from '../constants'; import { MemoryRouter } from 'react-router-dom'; import './styles.css'; import { Preview } from './components/Preview'; @@ -13,6 +11,7 @@ import { CustomPanelViewResult } from '../models'; import { Chatbot } from './components/Chatbot/Chatbot'; import { updateCssVariables } from './utils'; import { I10nProvider } from './providers/I10nProvider'; +import { SentryInit } from '../utils/sentryInit'; declare const acquireVsCodeApi: () => { getState: () => T; @@ -65,19 +64,13 @@ if (elm) { const url = elm?.getAttribute('data-url'); const experimental = elm?.getAttribute('data-experimental'); const webviewUrl = elm?.getAttribute('data-webview-url'); + const isCrashDisabled = elm?.getAttribute('data-is-crash-disabled'); updateCssVariables(); mutationObserver.observe(document.body, { childList: false, attributes: true }); - if (isProd === 'true') { - Sentry.init({ - dsn: SENTRY_LINK, - integrations: [new Integrations.BrowserTracing()], - tracesSampleRate: 0, // No performance tracing required - release: version || '', - environment: environment || '', - ignoreErrors: SentryIgnore - }); + if (isProd === 'true' && isCrashDisabled === 'false') { + Sentry.init(SentryInit(version, environment)); Sentry.setTag("type", "dashboard"); if (document.body.getAttribute(`data-vscode-theme-id`)) { diff --git a/src/helpers/Telemetry.ts b/src/helpers/Telemetry.ts index a863a69c..12dc9fda 100644 --- a/src/helpers/Telemetry.ts +++ b/src/helpers/Telemetry.ts @@ -24,6 +24,24 @@ export class Telemetry { return Telemetry.instance; } + public static isVscodeEnabled(): boolean { + const config = workspace.getConfiguration('telemetry'); + const isVscodeEnable = config.get<'off' | undefined>('enableTelemetry'); + return isVscodeEnable === 'off' ? false : true; + } + + /** + * Checks if telemetry is enabled. + * @returns {boolean} Returns true if telemetry is enabled, false otherwise. + */ + public static isEnabled(): boolean { + const isVscodeEnable = Telemetry.isVscodeEnabled(); + + const isDisabled = Settings.get(SETTING_TELEMETRY_DISABLE); + + return isDisabled || isVscodeEnable ? false : true; + } + /** * Send metrics to our own database * @param eventName @@ -31,11 +49,7 @@ export class Telemetry { * @returns */ public static send(eventName: string, properties?: any) { - const config = workspace.getConfiguration('telemetry'); - const isVscodeEnable = config.get<'off' | undefined>('enableTelemetry'); - - const isDisabled = Settings.get(SETTING_TELEMETRY_DISABLE); - if (isDisabled || isVscodeEnable === 'off') { + if (!Telemetry.isEnabled()) { return; } diff --git a/src/panelWebView/PanelProvider.ts b/src/panelWebView/PanelProvider.ts index 0f8630c3..c68d1beb 100644 --- a/src/panelWebView/PanelProvider.ts +++ b/src/panelWebView/PanelProvider.ts @@ -286,7 +286,9 @@ export class PanelProvider implements WebviewViewProvider, Disposable {
+ }" data-version="${ + version.usedVersion + }" data-is-crash-disabled="${!Telemetry.isVscodeEnabled()}">
${(scriptsToLoad || []) .map((script) => { diff --git a/src/panelWebView/index.tsx b/src/panelWebView/index.tsx index c965f045..613ddbbd 100644 --- a/src/panelWebView/index.tsx +++ b/src/panelWebView/index.tsx @@ -1,10 +1,10 @@ import * as React from 'react'; +import * as Sentry from '@sentry/react'; import { render } from 'react-dom'; import { ViewPanel } from './ViewPanel'; -import * as Sentry from '@sentry/react'; -import { Integrations } from '@sentry/tracing'; -import { SENTRY_LINK, SentryIgnore } from '../constants'; import { RecoilRoot } from 'recoil'; +import { I10nProvider } from '../dashboardWebView/providers/I10nProvider'; +import { SentryInit } from '../utils/sentryInit'; import './styles.css'; @@ -17,7 +17,6 @@ import '@bendera/vscode-webview-elements/dist/vscode-table-row.js'; import '@bendera/vscode-webview-elements/dist/vscode-table-cell.js'; import '@bendera/vscode-webview-elements/dist/vscode-collapsible.js'; import '@bendera/vscode-webview-elements/dist/vscode-label.js'; -import { I10nProvider } from '../dashboardWebView/providers/I10nProvider'; // import '@bendera/vscode-webview-elements/dist/vscode-checkbox.js'; // import '@vscode/webview-ui-toolkit/dist/esm/checkbox'; @@ -34,16 +33,10 @@ if (elm) { const version = elm?.getAttribute('data-version'); const environment = elm?.getAttribute('data-environment'); const isProd = elm?.getAttribute('data-isProd'); + const isCrashDisabled = elm?.getAttribute('data-is-crash-disabled'); - if (isProd === 'true') { - Sentry.init({ - dsn: SENTRY_LINK, - integrations: [new Integrations.BrowserTracing()], - tracesSampleRate: 0, // No performance tracing required - release: version || '', - environment: environment || '', - ignoreErrors: SentryIgnore - }); + if (isProd === 'true' && isCrashDisabled === 'false') { + Sentry.init(SentryInit(version, environment)); Sentry.setTag("type", "panel"); if (document.body.getAttribute(`data-vscode-theme-id`)) { diff --git a/src/utils/sentryInit.ts b/src/utils/sentryInit.ts new file mode 100644 index 00000000..3bb46180 --- /dev/null +++ b/src/utils/sentryInit.ts @@ -0,0 +1,21 @@ +import * as Sentry from '@sentry/react'; +import { Integrations } from '@sentry/tracing'; +import { SENTRY_LINK, SentryIgnore } from '../constants'; + +export const SentryInit = ( + version: string | null, + environment: string | null +): Sentry.BrowserOptions => ({ + dsn: SENTRY_LINK, + integrations: [new Integrations.BrowserTracing()], + tracesSampleRate: 0, // No performance tracing required + release: version || '', + environment: environment || '', + ignoreErrors: SentryIgnore, + beforeSend(event) { + if (event.user) { + delete event.user.ip_address; + } + return event; + } +}); From 0a530dce277a7326390c37e49d3d30d8912b6c9b Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Mon, 26 Feb 2024 15:51:40 +0100 Subject: [PATCH 23/23] Update readme --- README.beta.md | 36 +++++++++++++++++++++--------------- README.md | 36 +++++++++++++++++++++--------------- 2 files changed, 42 insertions(+), 30 deletions(-) diff --git a/README.beta.md b/README.beta.md index b9b010b7..0e4cf3a1 100644 --- a/README.beta.md +++ b/README.beta.md @@ -171,21 +171,6 @@ You can open showcase issues for the following things: - Share an article/video/webcast/... that explains how you use Front Matter; - Got something else to share? Open an issue and we can see where it fits on our website. -## 📊 Telemetry - -The Front Matter CMS extension collects telemetry data to help us build a better understand which features from the CMS are used. The extension respects the `telemetry.enableTelemetry` setting which you can learn more about in the [Visual Studio Code FAQ](https://aka.ms/vscode-remote/telemetry), or you can only disable it for the extension by configuring the `frontMatter.telemetry.disable` setting. - -We only collect the following data: - -- Type of event -- Extension title (main or beta) -- Extension version - -No user-specific data is collected, you can check the telemetry implementation in the following files: - -- [Telemetry class](https://github.com/estruyf/vscode-front-matter/blob/59528a3db01be8d34dc40638e6cf827090e31986/src/helpers/Telemetry.ts) -- [Metrics API](https://github.com/FrontMatter/web-documentation-nextjs/blob/main/pages/api/metrics.ts) - ## 👉 Contributors 🤘

@@ -208,6 +193,27 @@ No user-specific data is collected, you can check the telemetry implementation i

+## 📊 Telemetry + +The Front Matter CMS extension collects telemetry data to help us build a better understand which features from the CMS are used. The extension respects the `telemetry.enableTelemetry` setting which you can learn more about in the [Visual Studio Code FAQ](https://aka.ms/vscode-remote/telemetry), or you can only disable it for the extension by configuring the `frontMatter.telemetry.disable` setting. + +We only collect the following data: + +- Type of event +- Extension title (main or beta) +- Extension version + +No user-specific data is collected, you can check the telemetry implementation in the following files: + +- [Telemetry class](https://github.com/estruyf/vscode-front-matter/blob/59528a3db01be8d34dc40638e6cf827090e31986/src/helpers/Telemetry.ts) +- [Metrics API](https://github.com/FrontMatter/web-documentation-nextjs/blob/main/pages/api/metrics.ts) + +For crash reports in the webviews, we make use of Sentry to help us understand what went wrong. This data is only used to fix issues and improve the extension. You can find more information about the Sentry implementation in the following files: + +- [Sentry config](https://github.com/estruyf/vscode-front-matter/blob/63e296d62f11be73ac86d9e823084247952a7ddc/src/utils/sentryInit.ts) + +> The user ip address is not collected. + ## 🔑 License [MIT](./LICENSE) diff --git a/README.md b/README.md index 47238f4c..eccd6a18 100644 --- a/README.md +++ b/README.md @@ -169,21 +169,6 @@ You can open showcase issues for the following things: - Share an article/video/webcast/... that explains how you use Front Matter; - Got something else to share? Open an issue and we can see where it fits on our website. -## 📊 Telemetry - -The Front Matter CMS extension collects telemetry data to help us build a better understand which features from the CMS are used. The extension respects the `telemetry.enableTelemetry` setting which you can learn more about in the [Visual Studio Code FAQ](https://aka.ms/vscode-remote/telemetry), or you can only disable it for the extension by configuring the `frontMatter.telemetry.disable` setting. - -We only collect the following data: - -- Type of event -- Extension title (main or beta) -- Extension version - -No user-specific data is collected, you can check the telemetry implementation in the following files: - -- [Telemetry class](https://github.com/estruyf/vscode-front-matter/blob/59528a3db01be8d34dc40638e6cf827090e31986/src/helpers/Telemetry.ts) -- [Metrics API](https://github.com/FrontMatter/web-documentation-nextjs/blob/main/pages/api/metrics.ts) - ## 👉 Contributors 🤘

@@ -214,6 +199,27 @@ No user-specific data is collected, you can check the telemetry implementation i

+## 📊 Telemetry + +The Front Matter CMS extension collects telemetry data to help us build a better understand which features from the CMS are used. The extension respects the `telemetry.enableTelemetry` setting which you can learn more about in the [Visual Studio Code FAQ](https://aka.ms/vscode-remote/telemetry), or you can only disable it for the extension by configuring the `frontMatter.telemetry.disable` setting. + +We only collect the following data: + +- Type of event +- Extension title (main or beta) +- Extension version + +No user-specific data is collected, you can check the telemetry implementation in the following files: + +- [Telemetry class](https://github.com/estruyf/vscode-front-matter/blob/59528a3db01be8d34dc40638e6cf827090e31986/src/helpers/Telemetry.ts) +- [Metrics API](https://github.com/FrontMatter/web-documentation-nextjs/blob/main/pages/api/metrics.ts) + +For crash reports in the webviews, we make use of Sentry to help us understand what went wrong. This data is only used to fix issues and improve the extension. You can find more information about the Sentry implementation in the following files: + +- [Sentry config](https://github.com/estruyf/vscode-front-matter/blob/63e296d62f11be73ac86d9e823084247952a7ddc/src/utils/sentryInit.ts) + +> The user ip address is not collected. + ## 🔑 License [MIT](./LICENSE)