mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-08-06 17:02:54 +02:00
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" width="16" height="16" viewBox="0 0 24 24" stroke-width="2" stroke="#C5C5C5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m10.5 21 5.25-11.25L21 21m-9-3h7.5M3 5.621a48.474 48.474 0 0 1 6-.371m0 0c1.12 0 2.233.038 3.334.114M9 5.25V3m3.334 2.364C11.176 10.658 7.69 15.08 3 17.502m9.334-12.138c.896.061 1.785.147 2.666.257m-4.589 8.495a18.023 18.023 0 0 1-3.827-5.802" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 442 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" width="16" height="16" viewBox="0 0 24 24" stroke-width="2" stroke="#424242">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m10.5 21 5.25-11.25L21 21m-9-3h7.5M3 5.621a48.474 48.474 0 0 1 6-.371m0 0c1.12 0 2.233.038 3.334.114M9 5.25V3m3.334 2.364C11.176 10.658 7.69 15.08 3 17.502m9.334-12.138c.896.061 1.785.147 2.666.257m-4.589 8.495a18.023 18.023 0 0 1-3.827-5.802" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 442 B |
@@ -46,6 +46,8 @@
|
||||
"settings.view.common": "Common",
|
||||
"settings.view.contentFolders": "Content folders",
|
||||
"settings.view.astro": "Astro",
|
||||
"settings.view.integration": "Integration",
|
||||
|
||||
"settings.openOnStartup": "Open dashboard on startup",
|
||||
"settings.contentTypes": "Content types",
|
||||
"settings.contentFolders": "Content folders",
|
||||
@@ -57,12 +59,17 @@
|
||||
"settings.git.commitMessage": "Commit message",
|
||||
"settings.git.submoduleInfo": "When working with Git submodules, you can refer to the submodule settings in the documentation.",
|
||||
"settings.git.submoduleLink": "Read more about Git submodules",
|
||||
"settings.integration.title": "Integration",
|
||||
|
||||
"settings.commonSettings.website.title": "Website and SSG settings",
|
||||
"settings.commonSettings.previewUrl": "Preview URL",
|
||||
"settings.commonSettings.websiteUrl": "Website URL",
|
||||
"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",
|
||||
|
||||
"developer.title": "Developer mode",
|
||||
"developer.reload.title": "Reload the dashboard",
|
||||
"developer.reload.label": "Reload",
|
||||
@@ -90,6 +97,8 @@
|
||||
"dashboard.contents.contentActions.menuItem.view": "View",
|
||||
"dashboard.contents.contentActions.alert.title": "Delete: {0}",
|
||||
"dashboard.contents.contentActions.alert.description": "Are you sure you want to delete the \"{0}\" content?",
|
||||
"dashboard.contents.contentActions.translations.create": "Create translation",
|
||||
"dashboard.contents.contentActions.translations.menu": "Translations",
|
||||
|
||||
"dashboard.contents.item.invalidTitle": "<invalid title>",
|
||||
"dashboard.contents.item.invalidDescription": "<invalid description>",
|
||||
@@ -128,6 +137,9 @@
|
||||
|
||||
"dashboard.errorView.description": "Please close the dashboard and try again.",
|
||||
|
||||
"dashboard.filters.languageFilter.label": "Locale",
|
||||
"dashboard.filters.languageFilter.all": "All",
|
||||
|
||||
"dashboard.header.breadcrumb.home": "Home",
|
||||
|
||||
"dashboard.header.clearFilters.title": "Clear filters, grouping, and sorting",
|
||||
@@ -523,6 +535,17 @@
|
||||
"commands.folders.get.notificationError.remove.action": "Remove folder",
|
||||
"commands.folders.get.notificationError.create.action": "Create folder",
|
||||
|
||||
"commands.i18n.create.warning.noFileSelected": "No file selected.",
|
||||
"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.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",
|
||||
"commands.i18n.create.quickPick.placeHolder": "To which locale do you want to create a new content?",
|
||||
"commands.i18n.translate.progress.title": "Translating content...",
|
||||
|
||||
"commands.preview.panel.title": "Preview: {0}",
|
||||
"commands.preview.askUserToPickFolder.title": "Select the folder of the article to preview",
|
||||
|
||||
@@ -712,6 +735,7 @@
|
||||
"listeners.dashboard.settingsListener.triggerTemplate.progress.title": "Downloading and initializing the template...",
|
||||
"listeners.dashboard.settingsListener.triggerTemplate.download.error": "Failed to download the template.",
|
||||
"listeners.dashboard.settingsListener.triggerTemplate.init.error": "Failed to initialize the template.",
|
||||
"listeners.dashboard.settingsListener.setSecretValue.message": "Setting has been updated.",
|
||||
|
||||
"listeners.dashboard.snippetListener.addSnippet.missingFields.warning": "Snippet missing title or body",
|
||||
"listeners.dashboard.snippetListener.addSnippet.exists.warning": "Snippet with the same title already exists",
|
||||
|
||||
+202
-182
@@ -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",
|
||||
@@ -311,6 +303,17 @@
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "%setting.frontMatter.content.pageFolders.items.properties.disableCreation.description%"
|
||||
},
|
||||
"defaultLocale": {
|
||||
"type": "string",
|
||||
"description": "%setting.frontMatter.content.pageFolders.items.properties.defaultLocale.description%"
|
||||
},
|
||||
"locales": {
|
||||
"type": "array",
|
||||
"description": "%setting.frontMatter.content.pageFolders.items.properties.locales.description%",
|
||||
"items": {
|
||||
"$ref": "#i18n"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
@@ -321,6 +324,34 @@
|
||||
},
|
||||
"scope": "Content"
|
||||
},
|
||||
"frontMatter.content.i18n": {
|
||||
"type": "array",
|
||||
"default": [],
|
||||
"markdownDescription": "%setting.frontMatter.content.i18n.markdownDescription%",
|
||||
"items": {
|
||||
"$id": "#i18n",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "%setting.frontMatter.content.i18n.items.properties.title.description%"
|
||||
},
|
||||
"locale": {
|
||||
"type": "string",
|
||||
"description": "%setting.frontMatter.content.i18n.items.properties.locale.description%"
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "%setting.frontMatter.content.i18n.items.properties.path.description%"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"locale"
|
||||
]
|
||||
},
|
||||
"scope": "Content"
|
||||
},
|
||||
"frontMatter.content.placeholders": {
|
||||
"type": "array",
|
||||
"default": [],
|
||||
@@ -500,8 +531,7 @@
|
||||
"categories"
|
||||
],
|
||||
"markdownDescription": "%setting.frontMatter.content.filters.markdownDescription%",
|
||||
"items": [
|
||||
{
|
||||
"items": [{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
@@ -569,8 +599,7 @@
|
||||
"command": {
|
||||
"$id": "#scriptCommand",
|
||||
"type": "string",
|
||||
"anyOf": [
|
||||
{
|
||||
"anyOf": [{
|
||||
"enum": [
|
||||
"node",
|
||||
"bash",
|
||||
@@ -777,8 +806,7 @@
|
||||
"title",
|
||||
"file"
|
||||
],
|
||||
"anyOf": [
|
||||
{
|
||||
"anyOf": [{
|
||||
"required": [
|
||||
"schema"
|
||||
]
|
||||
@@ -832,8 +860,7 @@
|
||||
"id",
|
||||
"path"
|
||||
],
|
||||
"anyOf": [
|
||||
{
|
||||
"anyOf": [{
|
||||
"required": [
|
||||
"schema"
|
||||
]
|
||||
@@ -1074,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": {
|
||||
@@ -1326,8 +1350,7 @@
|
||||
"default": "",
|
||||
"description": "%setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.taxonomyId.description%",
|
||||
"not": {
|
||||
"anyOf": [
|
||||
{
|
||||
"anyOf": [{
|
||||
"const": ""
|
||||
},
|
||||
{
|
||||
@@ -1521,8 +1544,7 @@
|
||||
"type",
|
||||
"name"
|
||||
],
|
||||
"allOf": [
|
||||
{
|
||||
"allOf": [{
|
||||
"if": {
|
||||
"properties": {
|
||||
"type": {
|
||||
@@ -1730,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": {
|
||||
@@ -1787,8 +1806,7 @@
|
||||
"type": "string",
|
||||
"description": "%setting.frontMatter.taxonomy.customTaxonomy.items.properties.id.description%",
|
||||
"not": {
|
||||
"anyOf": [
|
||||
{
|
||||
"anyOf": [{
|
||||
"const": ""
|
||||
},
|
||||
{
|
||||
@@ -1985,8 +2003,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"commands": [{
|
||||
"command": "frontMatter.project.switch",
|
||||
"title": "%command.frontMatter.project.switch%",
|
||||
"category": "Front Matter",
|
||||
@@ -2301,23 +2318,27 @@
|
||||
"command": "frontMatter.cache.clear",
|
||||
"title": "%command.frontMatter.cache.clear%",
|
||||
"category": "Front Matter"
|
||||
}
|
||||
],
|
||||
"submenus": [
|
||||
},
|
||||
{
|
||||
"id": "frontmatter.submenu",
|
||||
"label": "Front Matter"
|
||||
"command": "frontMatter.i18n.create",
|
||||
"title": "%command.frontMatter.i18n.create%",
|
||||
"category": "Front Matter",
|
||||
"icon": {
|
||||
"light": "assets/icons/i18n-light.svg",
|
||||
"dark": "assets/icons/i18n-dark.svg"
|
||||
}
|
||||
}
|
||||
],
|
||||
"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"
|
||||
@@ -2347,6 +2368,11 @@
|
||||
"group": "navigation@-128",
|
||||
"when": "frontMatter:file:isValid == true"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.i18n.create",
|
||||
"group": "navigation@-127",
|
||||
"when": "frontMatter:file:isValid && frontMatter:i18n:default"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.markup.options",
|
||||
"group": "navigation@-126",
|
||||
@@ -2398,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"
|
||||
@@ -2421,8 +2444,7 @@
|
||||
"group": "frontmatter@3"
|
||||
}
|
||||
],
|
||||
"commandPalette": [
|
||||
{
|
||||
"commandPalette": [{
|
||||
"command": "frontMatter.init",
|
||||
"when": "frontMatterCanInit"
|
||||
},
|
||||
@@ -2450,6 +2472,10 @@
|
||||
"command": "frontMatter.git.sync",
|
||||
"when": "frontMatter:git:enabled"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.i18n.create",
|
||||
"when": "frontMatter:i18n:default"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.collapseSections",
|
||||
"when": "false"
|
||||
@@ -2571,8 +2597,7 @@
|
||||
"when": "frontMatter:file:isValid == true"
|
||||
}
|
||||
],
|
||||
"view/title": [
|
||||
{
|
||||
"view/title": [{
|
||||
"command": "frontMatter.chatbot",
|
||||
"group": "navigation@0",
|
||||
"when": "view == frontMatter.explorer"
|
||||
@@ -2604,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:*",
|
||||
@@ -2783,4 +2803,4 @@
|
||||
"dependencies": {
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,7 @@
|
||||
"command.frontMatter.markup.unorderedlist": "Unordered list",
|
||||
"command.frontMatter.git.sync": "Sync",
|
||||
"command.frontMatter.cache.clear": "Clear cache",
|
||||
"command.frontMatter.i18n.create": "Create new translation",
|
||||
"settings.configuration.title": "Front Matter: use frontmatter.json for shared team settings",
|
||||
"setting.frontMatter.projects.markdownDescription": "Specify the list of projects to load in the Front Matter CMS. [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.projects)",
|
||||
"setting.frontMatter.projects.items.properties.name.markdownDescription": "Specify the name of the project.",
|
||||
@@ -75,6 +76,12 @@
|
||||
"setting.frontMatter.content.pageFolders.items.properties.filePrefix.description": "Defines a prefix for the file name.",
|
||||
"setting.frontMatter.content.pageFolders.items.properties.contentTypes.description": "Defines which content types can be used for the current location. If not defined, all content types will be available.",
|
||||
"setting.frontMatter.content.pageFolders.items.properties.disableCreation.description": "Disable the creation of new content in the folder.",
|
||||
"setting.frontMatter.content.pageFolders.items.properties.defaultLocale.description": "Set the page folder as a default locale for the content. All content from this folder is translatable to the languages defined in the `frontMatter.content.i18n` setting.",
|
||||
"setting.frontMatter.content.pageFolders.items.properties.locales.description": "Define the locales for the page folder. This will be used for the translation of the content.",
|
||||
"setting.frontMatter.content.i18n.markdownDescription": "Specify the locales you want to use for your website. This setting can be overwritten on page folder level. [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.content.i18n)",
|
||||
"setting.frontMatter.content.i18n.items.properties.title.description": "Title of the locale",
|
||||
"setting.frontMatter.content.i18n.items.properties.locale.description": "Locale code",
|
||||
"setting.frontMatter.content.i18n.items.properties.path.description": "Relative path of the locale folder",
|
||||
"setting.frontMatter.content.placeholders.markdownDescription": "This array of placeholders defines the placeholders that you can use in your content types and templates for automatically populating your content its front matter. [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.content.placeholders)",
|
||||
"setting.frontMatter.content.placeholders.items.properties.id.description": "ID of the placeholder, in your content type or template, use it as follows: {{placeholder}}",
|
||||
"setting.frontMatter.content.placeholders.items.properties.value.description": "The placeholder its value",
|
||||
|
||||
+115
-56
@@ -1,6 +1,7 @@
|
||||
import { STATIC_FOLDER_PLACEHOLDER } from './../constants/StaticFolderPlaceholder';
|
||||
import { Questions } from './../helpers/Questions';
|
||||
import {
|
||||
SETTING_CONTENT_I18N,
|
||||
SETTING_CONTENT_PAGE_FOLDERS,
|
||||
SETTING_CONTENT_STATIC_FOLDER,
|
||||
SETTING_CONTENT_SUPPORTED_FILETYPES,
|
||||
@@ -9,7 +10,7 @@ import {
|
||||
} from './../constants';
|
||||
import { commands, Uri, workspace, window } from 'vscode';
|
||||
import { basename, dirname, join, relative, sep } from 'path';
|
||||
import { ContentFolder, FileInfo, FolderInfo, StaticFolder } from '../models';
|
||||
import { ContentFolder, FileInfo, FolderInfo, I18nConfig, StaticFolder } from '../models';
|
||||
import uniqBy = require('lodash.uniqby');
|
||||
import { Template } from './Template';
|
||||
import { Notifications } from '../helpers/Notifications';
|
||||
@@ -288,67 +289,34 @@ export class Folders {
|
||||
const folderInfo: FolderInfo[] = [];
|
||||
|
||||
for (const folder of folders) {
|
||||
try {
|
||||
const folderPath = parseWinPath(folder.path);
|
||||
const crntFolderInfo = await Folders.getFilesByFolder(folder, supportedFiles, limit);
|
||||
if (crntFolderInfo) {
|
||||
folderInfo.push(crntFolderInfo);
|
||||
}
|
||||
|
||||
if (typeof folderPath === 'string') {
|
||||
let files: Uri[] = [];
|
||||
// Process localization folders
|
||||
if (folder.defaultLocale) {
|
||||
const i18nConfig = folder.locales || Settings.get<I18nConfig[]>(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;
|
||||
|
||||
for (const fileType of supportedFiles || DEFAULT_FILE_TYPES) {
|
||||
let filePath = join(
|
||||
folderPath,
|
||||
folder.excludeSubdir ? '/' : '**',
|
||||
`*${fileType.startsWith('.') ? '' : '.'}${fileType}`
|
||||
);
|
||||
|
||||
if (folderPath === '' && folder.excludeSubdir) {
|
||||
filePath = `*${fileType.startsWith('.') ? '' : '.'}${fileType}`;
|
||||
}
|
||||
|
||||
let foundFiles = await Folders.findFiles(filePath);
|
||||
|
||||
// Make sure these file are coming from the folder path (this could be an issue in multi-root workspaces)
|
||||
foundFiles = foundFiles.filter((f) => parseWinPath(f.fsPath).startsWith(folderPath));
|
||||
|
||||
files = [...files, ...foundFiles];
|
||||
}
|
||||
|
||||
if (files) {
|
||||
let fileStats: FileInfo[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
try {
|
||||
const fileName = basename(file.fsPath);
|
||||
const folderName = dirname(file.fsPath).split(sep).pop();
|
||||
|
||||
const stats = await workspace.fs.stat(file);
|
||||
|
||||
fileStats.push({
|
||||
filePath: file.fsPath,
|
||||
fileName,
|
||||
folderName,
|
||||
...stats
|
||||
});
|
||||
} catch (error) {
|
||||
// Skip the file
|
||||
const crntFolderInfo = await Folders.getFilesByFolder(
|
||||
i18nFolder,
|
||||
supportedFiles,
|
||||
limit
|
||||
);
|
||||
if (crntFolderInfo) {
|
||||
folderInfo.push(crntFolderInfo);
|
||||
}
|
||||
}
|
||||
|
||||
fileStats = fileStats.sort((a, b) => b.mtime - a.mtime);
|
||||
|
||||
if (limit) {
|
||||
fileStats = fileStats.slice(0, limit);
|
||||
}
|
||||
|
||||
folderInfo.push({
|
||||
title: folder.title,
|
||||
files: files.length,
|
||||
lastModified: fileStats
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Skip the current folder
|
||||
}
|
||||
}
|
||||
|
||||
@@ -603,6 +571,97 @@ export class Folders {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the page folder that matches the given file path.
|
||||
*
|
||||
* @param filePath - The file path to match against the page folders.
|
||||
* @returns The page folder that matches the file path, or undefined if no match is found.
|
||||
*/
|
||||
public static getPageFolderByFilePath(filePath: string): ContentFolder | undefined {
|
||||
const folders = Folders.get();
|
||||
const parsedPath = parseWinPath(filePath);
|
||||
const pageFolderMatches = folders
|
||||
.filter((folder) => parsedPath && folder.path && parsedPath.includes(folder.path))
|
||||
.sort((a, b) => b.path.length - a.path.length);
|
||||
|
||||
if (pageFolderMatches.length > 0 && pageFolderMatches[0]) {
|
||||
return pageFolderMatches[0];
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
private static async getFilesByFolder(
|
||||
folder: ContentFolder,
|
||||
supportedFiles: string[] | undefined,
|
||||
limit?: number
|
||||
): Promise<FolderInfo | undefined> {
|
||||
try {
|
||||
const folderPath = parseWinPath(folder.path);
|
||||
|
||||
if (typeof folderPath === 'string') {
|
||||
let files: Uri[] = [];
|
||||
|
||||
for (const fileType of supportedFiles || DEFAULT_FILE_TYPES) {
|
||||
let filePath = join(
|
||||
folderPath,
|
||||
folder.excludeSubdir ? '/' : '**',
|
||||
`*${fileType.startsWith('.') ? '' : '.'}${fileType}`
|
||||
);
|
||||
|
||||
if (folderPath === '' && folder.excludeSubdir) {
|
||||
filePath = `*${fileType.startsWith('.') ? '' : '.'}${fileType}`;
|
||||
}
|
||||
|
||||
let foundFiles = await Folders.findFiles(filePath);
|
||||
|
||||
// Make sure these file are coming from the folder path (this could be an issue in multi-root workspaces)
|
||||
foundFiles = foundFiles.filter((f) => parseWinPath(f.fsPath).startsWith(folderPath));
|
||||
|
||||
files = [...files, ...foundFiles];
|
||||
}
|
||||
|
||||
if (files) {
|
||||
let fileStats: FileInfo[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
try {
|
||||
const fileName = basename(file.fsPath);
|
||||
const folderName = dirname(file.fsPath).split(sep).pop();
|
||||
|
||||
const stats = await workspace.fs.stat(file);
|
||||
|
||||
fileStats.push({
|
||||
filePath: file.fsPath,
|
||||
fileName,
|
||||
folderName,
|
||||
...stats
|
||||
});
|
||||
} catch (error) {
|
||||
// Skip the file
|
||||
}
|
||||
}
|
||||
|
||||
fileStats = fileStats.sort((a, b) => b.mtime - a.mtime);
|
||||
|
||||
if (limit) {
|
||||
fileStats = fileStats.slice(0, limit);
|
||||
}
|
||||
|
||||
return {
|
||||
title: folder.title,
|
||||
files: files.length,
|
||||
lastModified: fileStats
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Skip the current folder
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all content folders
|
||||
* @param pattern
|
||||
|
||||
@@ -19,6 +19,7 @@ import { Field } from '../models';
|
||||
import { Preview } from './Preview';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
import { LocalizationKey } from '../localization';
|
||||
import { i18n } from './i18n';
|
||||
|
||||
export class StatusListener {
|
||||
/**
|
||||
@@ -42,6 +43,10 @@ export class StatusListener {
|
||||
try {
|
||||
commands.executeCommand('setContext', CONTEXT.isValidFile, true);
|
||||
|
||||
// Check i18n
|
||||
const isI18nDefault = await i18n.isDefaultLanguage(document.uri.fsPath);
|
||||
commands.executeCommand('setContext', CONTEXT.isI18nDefault, isI18nDefault);
|
||||
|
||||
const article = editor
|
||||
? ArticleHelper.getFrontMatter(editor)
|
||||
: await ArticleHelper.getFrontMatterByPath(document.uri.fsPath);
|
||||
@@ -83,6 +88,7 @@ export class StatusListener {
|
||||
}
|
||||
} else {
|
||||
commands.executeCommand('setContext', CONTEXT.isValidFile, false);
|
||||
commands.executeCommand('setContext', CONTEXT.isI18nDefault, false);
|
||||
|
||||
const panel = PanelProvider.getInstance();
|
||||
if (panel && panel.visible) {
|
||||
|
||||
@@ -0,0 +1,517 @@
|
||||
import { ProgressLocation, Uri, commands, window, workspace } from 'vscode';
|
||||
import {
|
||||
ArticleHelper,
|
||||
ContentType,
|
||||
Extension,
|
||||
FrameworkDetector,
|
||||
Logger,
|
||||
Notifications,
|
||||
Settings,
|
||||
openFileInEditor,
|
||||
parseWinPath
|
||||
} from '../helpers';
|
||||
import { COMMAND_NAME, ExtensionState, SETTING_CONTENT_I18N } from '../constants';
|
||||
import { ContentFolder, Field, I18nConfig, ContentType as IContentType } from '../models';
|
||||
import { join, parse } from 'path';
|
||||
import { existsAsync } from '../utils';
|
||||
import { Folders } from '.';
|
||||
import { ParsedFrontMatter } from '../parsers';
|
||||
import { PagesListener } from '../listeners/dashboard';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
import { LocalizationKey } from '../localization';
|
||||
|
||||
export class i18n {
|
||||
private static processedFiles: {
|
||||
[filePath: string]: { dir: string; filename: string; isPageBundle: boolean };
|
||||
} = {};
|
||||
|
||||
/**
|
||||
* Registers the i18n commands.
|
||||
*/
|
||||
public static register() {
|
||||
const subscriptions = Extension.getInstance().subscriptions;
|
||||
|
||||
subscriptions.push(commands.registerCommand(COMMAND_NAME.i18n.create, i18n.create));
|
||||
|
||||
i18n.clearFiles();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the processed files
|
||||
*/
|
||||
public static clearFiles() {
|
||||
i18n.processedFiles = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the I18nConfig settings from the application.
|
||||
* @returns An array of I18nConfig objects if settings are found, otherwise undefined.
|
||||
*/
|
||||
public static async getSettings(filePath: string): Promise<I18nConfig[] | undefined> {
|
||||
if (!filePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
const i18nSettings = Settings.get<I18nConfig[]>(SETTING_CONTENT_I18N);
|
||||
let pageFolder = Folders.getPageFolderByFilePath(filePath);
|
||||
if (!pageFolder) {
|
||||
pageFolder = await i18n.getPageFolder(filePath);
|
||||
}
|
||||
|
||||
if (!pageFolder || !pageFolder.locales) {
|
||||
return i18nSettings;
|
||||
}
|
||||
|
||||
return pageFolder.locales;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given file path corresponds to the default language.
|
||||
* @param filePath - The file path to check.
|
||||
* @returns True if the file path corresponds to the default language, false otherwise.
|
||||
*/
|
||||
public static async isDefaultLanguage(filePath: string): Promise<boolean> {
|
||||
const i18nSettings = await i18n.getSettings(filePath);
|
||||
if (!i18nSettings) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const pageFolder = Folders.getPageFolderByFilePath(filePath);
|
||||
if (!pageFolder || !pageFolder.defaultLocale) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const fileInfo = await i18n.getFileInfo(filePath);
|
||||
|
||||
if (pageFolder.path) {
|
||||
let pageFolderPath = parseWinPath(pageFolder.path);
|
||||
if (!pageFolderPath.endsWith('/')) {
|
||||
pageFolderPath += '/';
|
||||
}
|
||||
|
||||
return (
|
||||
parseWinPath(fileInfo.dir).toLowerCase() === parseWinPath(pageFolderPath).toLowerCase()
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the I18nConfig for a given file path.
|
||||
* @param filePath - The path of the file.
|
||||
* @returns The I18nConfig object if found, otherwise undefined.
|
||||
*/
|
||||
public static async getLocale(filePath: string): Promise<I18nConfig | undefined> {
|
||||
const i18nSettings = await i18n.getSettings(filePath);
|
||||
if (!i18nSettings) {
|
||||
return;
|
||||
}
|
||||
|
||||
let pageFolder = Folders.getPageFolderByFilePath(filePath);
|
||||
|
||||
const fileInfo = await i18n.getFileInfo(filePath);
|
||||
|
||||
if (pageFolder && pageFolder.defaultLocale) {
|
||||
let pageFolderPath = parseWinPath(pageFolder.path);
|
||||
if (!pageFolderPath.endsWith('/')) {
|
||||
pageFolderPath += '/';
|
||||
}
|
||||
|
||||
if (
|
||||
pageFolder.path &&
|
||||
parseWinPath(fileInfo.dir).toLowerCase() === parseWinPath(pageFolderPath).toLowerCase()
|
||||
) {
|
||||
return i18nSettings.find((i18n) => i18n.locale === pageFolder?.defaultLocale);
|
||||
}
|
||||
}
|
||||
|
||||
pageFolder = await i18n.getPageFolder(filePath);
|
||||
if (!pageFolder) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const locale of i18nSettings) {
|
||||
if (locale.path && pageFolder.defaultLocale !== locale.locale) {
|
||||
const translation = join(pageFolder.path, locale.path, fileInfo.filename);
|
||||
if (parseWinPath(translation).toLowerCase() === parseWinPath(filePath).toLowerCase()) {
|
||||
return locale;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves translations for a given file path.
|
||||
* @param filePath - The path of the file for which translations are requested.
|
||||
* @returns A promise that resolves to an object containing translations for each locale, or undefined if i18n settings are not available.
|
||||
*/
|
||||
public static async getTranslations(filePath: string): Promise<
|
||||
| {
|
||||
[locale: string]: {
|
||||
locale: I18nConfig;
|
||||
path: string;
|
||||
};
|
||||
}
|
||||
| undefined
|
||||
> {
|
||||
const i18nSettings = await i18n.getSettings(filePath);
|
||||
if (!i18nSettings) {
|
||||
return;
|
||||
}
|
||||
|
||||
const translations: {
|
||||
[locale: string]: {
|
||||
locale: I18nConfig;
|
||||
path: string;
|
||||
};
|
||||
} = {};
|
||||
|
||||
let pageFolder = Folders.getPageFolderByFilePath(filePath);
|
||||
const fileInfo = await i18n.getFileInfo(filePath);
|
||||
|
||||
if (pageFolder && pageFolder.defaultLocale) {
|
||||
for (const i18n of i18nSettings) {
|
||||
const translation = join(pageFolder.path, i18n.path || '', fileInfo.filename);
|
||||
if (await existsAsync(translation)) {
|
||||
translations[i18n.locale] = {
|
||||
locale: i18n,
|
||||
path: translation
|
||||
};
|
||||
}
|
||||
}
|
||||
return translations;
|
||||
}
|
||||
|
||||
pageFolder = await i18n.getPageFolder(filePath);
|
||||
if (!pageFolder) {
|
||||
return translations;
|
||||
}
|
||||
|
||||
for (const i18n of i18nSettings) {
|
||||
const translation = join(pageFolder.path, i18n.path || '', fileInfo.filename);
|
||||
if (await existsAsync(translation)) {
|
||||
translations[i18n.locale] = {
|
||||
locale: i18n,
|
||||
path: translation
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return translations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new content file for a specific locale based on the i18n configuration.
|
||||
* If a file path is provided, the new content file will be created in the same directory.
|
||||
* If no file path is provided, the active file in the editor will be used.
|
||||
* @param filePath The path of the file where the new content file should be created.
|
||||
*/
|
||||
private static async create(fileUri?: Uri | string) {
|
||||
if (!fileUri) {
|
||||
const filePath = ArticleHelper.getActiveFile();
|
||||
fileUri = filePath ? Uri.file(filePath) : undefined;
|
||||
}
|
||||
|
||||
if (!fileUri) {
|
||||
Notifications.warning(l10n.t(LocalizationKey.commandsI18nCreateWarningNoFileSelected));
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof fileUri === 'string') {
|
||||
fileUri = Uri.file(fileUri);
|
||||
}
|
||||
|
||||
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));
|
||||
return;
|
||||
}
|
||||
|
||||
const locale = await window.showQuickPick(
|
||||
i18nSettings.filter((i18n) => i18n.path).map((i18n) => i18n.title || i18n.locale),
|
||||
{
|
||||
title: l10n.t(LocalizationKey.commandsI18nCreateQuickPickTitle),
|
||||
placeHolder: l10n.t(LocalizationKey.commandsI18nCreateQuickPickPlaceHolder),
|
||||
ignoreFocusOut: true
|
||||
}
|
||||
);
|
||||
|
||||
if (!locale) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedI18n = i18nSettings.find(
|
||||
(i18n) => i18n.title === locale || i18n.locale === locale
|
||||
);
|
||||
if (!selectedI18n || !selectedI18n.path) {
|
||||
Notifications.warning(l10n.t(LocalizationKey.commandsI18nCreateWarningNoConfig));
|
||||
return;
|
||||
}
|
||||
|
||||
let article = await ArticleHelper.getFrontMatterByPath(fileUri.fsPath);
|
||||
if (!article) {
|
||||
Notifications.warning(l10n.t(LocalizationKey.commandsI18nCreateWarningNoFile));
|
||||
return;
|
||||
}
|
||||
|
||||
const contentType = ArticleHelper.getContentType(article);
|
||||
if (!contentType) {
|
||||
Notifications.warning(l10n.t(LocalizationKey.commandsI18nCreateWarningNoContentType));
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the directory of the file
|
||||
const fileInfo = parse(fileUri.fsPath);
|
||||
let dir = fileInfo.dir;
|
||||
let pageBundleDir = '';
|
||||
|
||||
if (await ArticleHelper.isPageBundle(fileUri.fsPath)) {
|
||||
dir = ArticleHelper.getPageFolderFromBundlePath(fileUri.fsPath);
|
||||
pageBundleDir = fileUri.fsPath.replace(dir, '');
|
||||
pageBundleDir = join(parse(pageBundleDir).dir);
|
||||
}
|
||||
|
||||
const i18nDir = join(dir, selectedI18n.path, pageBundleDir);
|
||||
|
||||
if (!(await existsAsync(i18nDir))) {
|
||||
await workspace.fs.createDirectory(Uri.file(i18nDir));
|
||||
}
|
||||
|
||||
article = await i18n.updateFrontMatter(
|
||||
article,
|
||||
fileUri.fsPath,
|
||||
contentType,
|
||||
selectedI18n,
|
||||
i18nDir
|
||||
);
|
||||
|
||||
const newFilePath = join(i18nDir, fileInfo.base);
|
||||
if (await existsAsync(newFilePath)) {
|
||||
Notifications.error(l10n.t(LocalizationKey.commandsI18nCreateErrorFileExists));
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceLocale = await i18n.getLocale(fileUri.fsPath);
|
||||
if (sourceLocale?.locale) {
|
||||
article = await i18n.translate(article, sourceLocale, selectedI18n);
|
||||
}
|
||||
|
||||
const newFileUri = Uri.file(newFilePath);
|
||||
await workspace.fs.writeFile(
|
||||
newFileUri,
|
||||
Buffer.from(ArticleHelper.stringifyFrontMatter(article.content, article.data))
|
||||
);
|
||||
|
||||
await openFileInEditor(newFilePath);
|
||||
|
||||
PagesListener.refresh();
|
||||
|
||||
Notifications.info(
|
||||
l10n.t(
|
||||
LocalizationKey.commandsI18nCreateSuccessCreated,
|
||||
selectedI18n.title || selectedI18n.locale
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates the given article from the source locale to the target locale using DeepL translation service.
|
||||
* @param article - The article to be translated.
|
||||
* @param sourceLocale - The source locale configuration.
|
||||
* @param targetLocale - The target locale configuration.
|
||||
* @returns A promise that resolves to the translated article.
|
||||
*/
|
||||
private static async translate(
|
||||
article: ParsedFrontMatter,
|
||||
sourceLocale: I18nConfig,
|
||||
targetLocale: I18nConfig
|
||||
) {
|
||||
return new Promise<ParsedFrontMatter>(async (resolve) => {
|
||||
const authKey = await Extension.getInstance().getSecret(ExtensionState.Secrets.DeeplApiKey);
|
||||
if (!authKey) {
|
||||
resolve(article);
|
||||
return;
|
||||
}
|
||||
|
||||
await window.withProgress(
|
||||
{
|
||||
location: ProgressLocation.Notification,
|
||||
title: l10n.t(LocalizationKey.commandsI18nTranslateProgressTitle),
|
||||
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
|
||||
});
|
||||
|
||||
let host = authKey.endsWith(':fx') ? 'api-free.deepl.com' : 'api.deepl.com';
|
||||
|
||||
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',
|
||||
'content-length': body.length.toString(),
|
||||
Accept: 'application/json'
|
||||
},
|
||||
body
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`DeepL: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (!data.translations || data.translations.length < 3) {
|
||||
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;
|
||||
} catch (error) {
|
||||
Notifications.error(`${(error as Error).message}`);
|
||||
}
|
||||
|
||||
resolve(article);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the filename and directory information from the given file path.
|
||||
* If the file is a page bundle, the directory will be adjusted accordingly.
|
||||
* @param filePath - The path of the file.
|
||||
* @returns An object containing the filename and directory.
|
||||
*/
|
||||
private static async getFileInfo(filePath: string): Promise<{ filename: string; dir: string }> {
|
||||
if (i18n.processedFiles[filePath]) {
|
||||
return i18n.processedFiles[filePath];
|
||||
}
|
||||
|
||||
const fileInfo = parse(filePath);
|
||||
let filename = fileInfo.base;
|
||||
let dir = fileInfo.dir;
|
||||
|
||||
const isPageBundle = await ArticleHelper.isPageBundle(filePath);
|
||||
if (isPageBundle) {
|
||||
dir = ArticleHelper.getPageFolderFromBundlePath(filePath);
|
||||
filename = join(parseWinPath(filePath).replace(parseWinPath(dir), ''));
|
||||
}
|
||||
|
||||
if (!dir.endsWith('/')) {
|
||||
dir += '/';
|
||||
}
|
||||
|
||||
i18n.processedFiles[filePath] = {
|
||||
isPageBundle,
|
||||
filename,
|
||||
dir
|
||||
};
|
||||
|
||||
return i18n.processedFiles[filePath];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the page folder for a given file path.
|
||||
*
|
||||
* @param filePath - The path of the file.
|
||||
* @returns A promise that resolves to the ContentFolder object representing the page folder, or undefined if not found.
|
||||
*/
|
||||
private static async getPageFolder(filePath: string): Promise<ContentFolder | undefined> {
|
||||
const folders = Folders.get();
|
||||
|
||||
const localeFolders = folders?.filter((folder) => folder.defaultLocale);
|
||||
if (!localeFolders) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fileInfo = await i18n.getFileInfo(filePath);
|
||||
|
||||
for (const folder of localeFolders) {
|
||||
const defaultFile = join(folder.path, fileInfo.filename);
|
||||
if (await existsAsync(defaultFile)) {
|
||||
return folder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the front matter of an article with internationalization (i18n) support.
|
||||
*
|
||||
* @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 i18nDir - The directory where the i18n files are located.
|
||||
* @returns A Promise that resolves to the updated parsed front matter.
|
||||
*/
|
||||
private static async updateFrontMatter(
|
||||
article: ParsedFrontMatter,
|
||||
filePath: string,
|
||||
contentType: IContentType,
|
||||
i18nConfig: I18nConfig,
|
||||
i18nDir: string
|
||||
): Promise<ParsedFrontMatter> {
|
||||
const imageFields = ContentType.findFieldsByTypeDeep(contentType.fields, 'image');
|
||||
if (imageFields.length > 0) {
|
||||
article.data = await i18n.processImageFields(article.data, filePath, imageFields, i18nDir);
|
||||
}
|
||||
|
||||
return article;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the image fields in the provided data object.
|
||||
* Replaces the image field values with the relative path to the image file.
|
||||
*
|
||||
* @param data - The data object containing the field values.
|
||||
* @param filePath - The absolute file path of the data object.
|
||||
* @param fields - The array of field arrays to process.
|
||||
* @param i18nDir - The directory path for internationalization.
|
||||
* @returns The updated data object with image field values replaced by relative paths.
|
||||
*/
|
||||
private static async processImageFields(
|
||||
data: { [key: string]: any },
|
||||
filePath: string,
|
||||
fields: Field[][],
|
||||
i18nDir: string
|
||||
) {
|
||||
for (const field of fields) {
|
||||
if (!field) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const f of field) {
|
||||
if (f.type === 'image') {
|
||||
const value = data[f.name];
|
||||
if (value) {
|
||||
let imgPath = FrameworkDetector.getAbsPathByFile(value, filePath);
|
||||
imgPath = FrameworkDetector.getRelPathByFileDir(imgPath, i18nDir);
|
||||
data[f.name] = imgPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
import * as React from "react"
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||
import { cn } from "../../utils/cn"
|
||||
import { CheckCircleIcon, CheckIcon, ChevronRightIcon } from "@heroicons/react/24/outline"
|
||||
import { ChevronRightIcon } from "@heroicons/react/24/outline"
|
||||
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root
|
||||
@@ -16,8 +16,6 @@ const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
||||
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
||||
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
||||
|
||||
const DropdownMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
@@ -27,7 +25,7 @@ const DropdownMenuSubTrigger = React.forwardRef<
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-[var(--vscode-sideBar-background)] data-[state=open]:bg-[var(--vscode-sideBar-background)]",
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-[var(--vscode-list-hoverBackground)] data-[state=open]:bg-[var(--vscode-list-hoverBackground)]",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
@@ -47,7 +45,7 @@ const DropdownMenuSubContent = React.forwardRef<
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-[var(--vscode-sideBar-background)] p-1 text-[var(--vscode-editor-foreground)] shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded border border-[var(--frontmatter-border)] bg-[var(--vscode-sideBar-background)] p-1 text-[var(--vscode-editor-foreground)] shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -65,7 +63,7 @@ const DropdownMenuContent = React.forwardRef<
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] rounded-md border border-[var(--frontmatter-border)] bg-[var(--vscode-sideBar-background)] p-1 text-[var(--vscode-editor-foreground)] shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 max-h-96 overflow-auto",
|
||||
"z-50 min-w-[8rem] rounded border border-[var(--frontmatter-border)] bg-[var(--vscode-sideBar-background)] p-1 text-[var(--vscode-editor-foreground)] shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 max-h-96 overflow-auto",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -92,52 +90,6 @@ const DropdownMenuItem = React.forwardRef<
|
||||
))
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-[var(--vscode-list-hoverBackground)] data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon className="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
))
|
||||
DropdownMenuCheckboxItem.displayName =
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName
|
||||
|
||||
const DropdownMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-[var(--vscode-list-hoverBackground)] data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CheckCircleIcon className="h-2 w-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
))
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
@@ -186,8 +138,6 @@ export {
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
@@ -196,5 +146,4 @@ export {
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
}
|
||||
@@ -67,6 +67,11 @@ export const COMMAND_NAME = {
|
||||
addMissingFields: getCommandName('contenttype.addMissingFields'),
|
||||
setContentType: getCommandName('contenttype.setContentType'),
|
||||
|
||||
// i18n
|
||||
i18n: {
|
||||
create: getCommandName('i18n.create')
|
||||
},
|
||||
|
||||
// Project
|
||||
switchProject: getCommandName('project.switch'),
|
||||
|
||||
|
||||
@@ -30,5 +30,9 @@ export const ExtensionState = {
|
||||
v7_0_0: {
|
||||
dateFields: `frontMatter:Updates:v7.0.0:dateFields`
|
||||
}
|
||||
},
|
||||
|
||||
Secrets: {
|
||||
DeeplApiKey: `frontMatter:Secrets:DeeplApiKey`
|
||||
}
|
||||
};
|
||||
|
||||
@@ -17,6 +17,11 @@ export const GeneralCommands = {
|
||||
getBranch: 'getBranch',
|
||||
selectBranch: 'gitSelectBranch'
|
||||
},
|
||||
secrets: {
|
||||
get: 'getSecret',
|
||||
set: 'setSecret'
|
||||
},
|
||||
runCommand: 'runCommand',
|
||||
getLocalization: 'getLocalization',
|
||||
openOnWebsite: 'openOnWebsite'
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ export const CONTEXT = {
|
||||
isValidFile: 'frontMatter:file:isValid',
|
||||
isDevelopment: 'frontMatter:isDevelopment',
|
||||
|
||||
isI18nDefault: 'frontMatter:i18n:default',
|
||||
|
||||
hasViewModes: 'frontMatter:has:modes',
|
||||
|
||||
isSnippetsDashboardEnabled: 'frontMatter:dashboard:snippets:enabled',
|
||||
|
||||
@@ -57,6 +57,7 @@ export const SETTING_CUSTOM_SCRIPTS = 'custom.scripts';
|
||||
|
||||
export const SETTING_AUTO_UPDATE_DATE = 'content.autoUpdateDate';
|
||||
export const SETTING_CONTENT_PAGE_FOLDERS = 'content.pageFolders';
|
||||
export const SETTING_CONTENT_I18N = 'content.i18n';
|
||||
export const SETTING_CONTENT_STATIC_FOLDER = 'content.publicFolder';
|
||||
export const SETTING_CONTENT_FRONTMATTER_HIGHLIGHT = 'content.fmHighlight';
|
||||
export const SETTING_CONTENT_DRAFT_FIELD = 'content.draftField';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Messenger, messageHandler } from '@estruyf/vscode/dist/client';
|
||||
import { EyeIcon, GlobeEuropeAfricaIcon, CommandLineIcon, TrashIcon, EllipsisVerticalIcon } from '@heroicons/react/24/outline';
|
||||
import { EyeIcon, GlobeEuropeAfricaIcon, CommandLineIcon, TrashIcon, EllipsisVerticalIcon, LanguageIcon } from '@heroicons/react/24/outline';
|
||||
import * as React from 'react';
|
||||
import { CustomScript, ScriptType } from '../../../models';
|
||||
import { CustomScript, I18nConfig, ScriptType } from '../../../models';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
import { QuickAction } from '../Menu';
|
||||
import { Alert } from '../Modals/Alert';
|
||||
@@ -9,10 +9,10 @@ import * as l10n from '@vscode/l10n';
|
||||
import { LocalizationKey } from '../../../localization';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { SettingsSelector } from '../../state';
|
||||
import { GeneralCommands } from '../../../constants';
|
||||
import { COMMAND_NAME, GeneralCommands } from '../../../constants';
|
||||
import { PinIcon } from '../Icons/PinIcon';
|
||||
import { PinnedItemsAtom } from '../../state/atom/PinnedItems';
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '../../../components/shadcn/Dropdown';
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuPortal, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger } from '../../../components/shadcn/Dropdown';
|
||||
|
||||
export interface IContentActionsProps {
|
||||
title: string;
|
||||
@@ -20,6 +20,14 @@ export interface IContentActionsProps {
|
||||
relPath: string;
|
||||
scripts: CustomScript[] | undefined;
|
||||
listView?: boolean;
|
||||
locale?: I18nConfig;
|
||||
isDefaultLocale?: boolean;
|
||||
translations?: {
|
||||
[locale: string]: {
|
||||
locale: I18nConfig;
|
||||
path: string;
|
||||
};
|
||||
};
|
||||
onOpen: () => void;
|
||||
}
|
||||
|
||||
@@ -29,7 +37,10 @@ export const ContentActions: React.FunctionComponent<IContentActionsProps> = ({
|
||||
relPath,
|
||||
scripts,
|
||||
onOpen,
|
||||
listView
|
||||
listView,
|
||||
isDefaultLocale,
|
||||
translations,
|
||||
locale
|
||||
}: React.PropsWithChildren<IContentActionsProps>) => {
|
||||
const [pinnedItems, setPinnedItems] = useRecoilState(PinnedItemsAtom);
|
||||
const [showDeletionAlert, setShowDeletionAlert] = React.useState(false);
|
||||
@@ -52,6 +63,10 @@ export const ContentActions: React.FunctionComponent<IContentActionsProps> = ({
|
||||
setShowDeletionAlert(false);
|
||||
};
|
||||
|
||||
const onOpenFile = (filePath: string) => {
|
||||
messageHandler.send(DashboardMessage.openFile, filePath);
|
||||
}
|
||||
|
||||
const openOnWebsite = React.useCallback((e: React.MouseEvent<HTMLButtonElement | HTMLDivElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
if (settings?.websiteUrl && path) {
|
||||
@@ -84,6 +99,13 @@ export const ContentActions: React.FunctionComponent<IContentActionsProps> = ({
|
||||
[path]
|
||||
);
|
||||
|
||||
const runCommand = React.useCallback((commandId: string) => {
|
||||
messageHandler.send(GeneralCommands.toVSCode.runCommand, {
|
||||
command: commandId,
|
||||
args: path
|
||||
})
|
||||
}, [path]);
|
||||
|
||||
const isPinned = React.useMemo(() => {
|
||||
return pinnedItems.includes(relPath);
|
||||
}, [pinnedItems, relPath]);
|
||||
@@ -104,6 +126,49 @@ export const ContentActions: React.FunctionComponent<IContentActionsProps> = ({
|
||||
));
|
||||
}, [scripts]);
|
||||
|
||||
const translationsMenu = React.useMemo(() => {
|
||||
if (!locale || !translations || Object.keys(translations).length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const crntLocale = translations[locale.locale];
|
||||
const otherLocales = Object.entries(translations).filter(([key]) => key !== locale.locale);
|
||||
|
||||
if (otherLocales.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<LanguageIcon className={`mr-2 h-4 w-4`} aria-hidden={true} />
|
||||
<span>{l10n.t(LocalizationKey.dashboardContentsContentActionsTranslationsMenu)}</span>
|
||||
</DropdownMenuSubTrigger>
|
||||
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuItem onClick={() => onOpenFile(crntLocale.path)}>
|
||||
<span>{crntLocale.locale.title || crntLocale.locale.locale}</span>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
{
|
||||
otherLocales.map(([key, value]) => (
|
||||
<DropdownMenuItem
|
||||
key={key}
|
||||
onClick={() => onOpenFile(value.path)}
|
||||
>
|
||||
<span>{value.locale.title || value.locale.locale}</span>
|
||||
</DropdownMenuItem>
|
||||
))
|
||||
}
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenuSub>
|
||||
);
|
||||
}, [translations, locale, isDefaultLocale]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
@@ -129,14 +194,17 @@ export const ContentActions: React.FunctionComponent<IContentActionsProps> = ({
|
||||
)
|
||||
}
|
||||
|
||||
<QuickAction title={l10n.t(LocalizationKey.commonDelete)} onClick={onDelete}>
|
||||
<QuickAction
|
||||
title={l10n.t(LocalizationKey.commonDelete)}
|
||||
className={`hover:text-[var(--vscode-statusBarItem-errorBackground)]`}
|
||||
onClick={onDelete}>
|
||||
<TrashIcon className={`w-4 h-4`} aria-hidden="true" />
|
||||
</QuickAction>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className='text-[var(--vscode-tab-inactiveForeground)] hover:text-[var(--vscode-tab-activeForeground)]'>
|
||||
<DropdownMenuTrigger className='text-[var(--vscode-tab-inactiveForeground)] hover:text-[var(--vscode-tab-activeForeground)] data-[state=open]:text-[var(--vscode-tab-activeForeground)] focus:outline-none'>
|
||||
<span className="sr-only">{l10n.t(LocalizationKey.dashboardContentsContentActionsActionMenuButtonTitle)}</span>
|
||||
<EllipsisVerticalIcon className="w-4 h-4" aria-hidden="true" />
|
||||
</DropdownMenuTrigger>
|
||||
@@ -161,6 +229,17 @@ export const ContentActions: React.FunctionComponent<IContentActionsProps> = ({
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
locale && isDefaultLocale && (
|
||||
<DropdownMenuItem onClick={() => runCommand(COMMAND_NAME.i18n.create)}>
|
||||
<LanguageIcon className={`mr-2 h-4 w-4`} aria-hidden={true} />
|
||||
<span>{l10n.t(LocalizationKey.dashboardContentsContentActionsTranslationsCreate)}</span>
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
}
|
||||
|
||||
{translationsMenu}
|
||||
|
||||
{customScriptActions}
|
||||
|
||||
<DropdownMenuItem onClick={onDelete} className={`focus:bg-[var(--vscode-statusBarItem-errorBackground)] focus:text-[var(--vscode-statusBarItem-errorForeground)]`}>
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import * as React from 'react';
|
||||
import { Page } from '../../models';
|
||||
import { ChevronDownIcon, LanguageIcon } from '@heroicons/react/24/outline';
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from '../../../components/shadcn/Dropdown';
|
||||
import { MenuItem } from '../Menu';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
import { messageHandler } from '@estruyf/vscode/dist/client';
|
||||
|
||||
export interface II18nLabelProps {
|
||||
page: Page;
|
||||
}
|
||||
|
||||
export const I18nLabel: React.FunctionComponent<II18nLabelProps> = ({
|
||||
page
|
||||
}: React.PropsWithChildren<II18nLabelProps>) => {
|
||||
if (!page.fmLocale) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-2 flex items-center">
|
||||
<LanguageIcon className="mr-1 h-4 w-4 inline-block" />
|
||||
<span className="text-xs">{page.fmLocale.title || page.fmLocale.locale}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -16,6 +16,7 @@ import { LocalizationKey } from '../../../localization';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { routePaths } from '../..';
|
||||
import useCard from '../../hooks/useCard';
|
||||
import { I18nLabel } from './I18nLabel';
|
||||
|
||||
export interface IItemProps extends Page { }
|
||||
|
||||
@@ -69,6 +70,34 @@ export const Item: React.FunctionComponent<IItemProps> = ({
|
||||
return [];
|
||||
}, [settings, pageData]);
|
||||
|
||||
const statusPlaceholder = useMemo(() => {
|
||||
if (!statusHtml && !cardFields?.state) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
statusHtml ? (
|
||||
<div dangerouslySetInnerHTML={{ __html: statusHtml }} />
|
||||
) : (
|
||||
cardFields?.state && draftField && draftField.name && typeof pageData[draftField.name] !== "undefined" ? <Status draft={pageData[draftField.name]} published={pageData.fmPublished} /> : null
|
||||
)
|
||||
)
|
||||
}, [statusHtml, cardFields?.state, draftField, pageData]);
|
||||
|
||||
const datePlaceholder = useMemo(() => {
|
||||
if (!dateHtml && !cardFields?.date) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
dateHtml ? (
|
||||
<div className='mr-4' dangerouslySetInnerHTML={{ __html: dateHtml }} />
|
||||
) : (
|
||||
cardFields?.date && pageData.date ? <DateField className={`mr-4`} value={pageData.date} format={pageData.fmDateFormat} /> : null
|
||||
)
|
||||
)
|
||||
}, [dateHtml, cardFields?.date, pageData]);
|
||||
|
||||
const hasDraftOrDate = useMemo(() => {
|
||||
return cardFields && (cardFields.state || cardFields.date);
|
||||
}, [cardFields]);
|
||||
@@ -82,8 +111,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({
|
||||
<button
|
||||
title={escapedTitle ? l10n.t(LocalizationKey.commonOpenWithValue, escapedTitle) : l10n.t(LocalizationKey.commonOpen)}
|
||||
onClick={openFile}
|
||||
className={`relative h-36 w-full overflow-hidden border-b cursor-pointer border-[var(--frontmatter-border)]
|
||||
}`}
|
||||
className={`relative h-36 w-full overflow-hidden border-b cursor-pointer border-[var(--frontmatter-border)]`}
|
||||
>
|
||||
{
|
||||
imageHtml ?
|
||||
@@ -106,32 +134,28 @@ export const Item: React.FunctionComponent<IItemProps> = ({
|
||||
</button>
|
||||
|
||||
<div className="relative p-4 w-full grow">
|
||||
<div className={`flex justify-between items-center ${hasDraftOrDate ? `mb-2` : ``}`}>
|
||||
{
|
||||
statusHtml ? (
|
||||
<div dangerouslySetInnerHTML={{ __html: statusHtml }} />
|
||||
) : (
|
||||
cardFields?.state && draftField && draftField.name && <Status draft={pageData[draftField.name]} published={pageData.fmPublished} />
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
dateHtml ? (
|
||||
<div className='mr-4' dangerouslySetInnerHTML={{ __html: dateHtml }} />
|
||||
) : (
|
||||
cardFields?.date && <DateField className={`mr-4`} value={pageData.date} format={pageData.fmDateFormat} />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{
|
||||
(statusPlaceholder || datePlaceholder) && (
|
||||
<div className={`flex justify-between items-center ${hasDraftOrDate ? `mb-2` : ``}`}>
|
||||
{statusPlaceholder}
|
||||
{datePlaceholder}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<ContentActions
|
||||
title={pageData.title}
|
||||
path={pageData.fmFilePath}
|
||||
relPath={pageData.fmRelFileWsPath}
|
||||
locale={pageData.fmLocale}
|
||||
isDefaultLocale={pageData.fmDefaultLocale}
|
||||
translations={pageData.fmTranslations}
|
||||
scripts={settings?.scripts}
|
||||
onOpen={openFile}
|
||||
/>
|
||||
|
||||
<I18nLabel page={pageData} />
|
||||
|
||||
<button
|
||||
title={escapedTitle ? l10n.t(LocalizationKey.commonOpenWithValue, escapedTitle) : l10n.t(LocalizationKey.commonOpen)}
|
||||
onClick={openFile}
|
||||
@@ -140,25 +164,29 @@ export const Item: React.FunctionComponent<IItemProps> = ({
|
||||
titleHtml ? (
|
||||
<div dangerouslySetInnerHTML={{ __html: titleHtml }} />
|
||||
) : (
|
||||
<h2 className="mb-2 font-bold">
|
||||
{escapedTitle}
|
||||
<h2 className="font-bold">
|
||||
<span>{escapedTitle}</span>
|
||||
</h2>
|
||||
)
|
||||
}
|
||||
</button>
|
||||
|
||||
<button
|
||||
title={escapedTitle ? l10n.t(LocalizationKey.commonOpenWithValue, escapedTitle) : l10n.t(LocalizationKey.commonOpen)}
|
||||
onClick={openFile}
|
||||
className={`text-left block`}>
|
||||
{
|
||||
descriptionHtml ? (
|
||||
<div dangerouslySetInnerHTML={{ __html: descriptionHtml }} />
|
||||
) : (
|
||||
<p className={`text-xs text-[vara(--vscode-titleBar-activeForeground)]`}>{escapedDescription}</p>
|
||||
)
|
||||
}
|
||||
</button>
|
||||
{
|
||||
(escapedDescription || descriptionHtml) && (
|
||||
<button
|
||||
title={escapedTitle ? l10n.t(LocalizationKey.commonOpenWithValue, escapedTitle) : l10n.t(LocalizationKey.commonOpen)}
|
||||
onClick={openFile}
|
||||
className={`mt-2 text-left block`}>
|
||||
{
|
||||
descriptionHtml ? (
|
||||
<div dangerouslySetInnerHTML={{ __html: descriptionHtml }} />
|
||||
) : (
|
||||
<p className={`text-xs text-[vara(--vscode-titleBar-activeForeground)]`}>{escapedDescription}</p>
|
||||
)
|
||||
}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
tagsHtml ? (
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import * as React from 'react';
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuSeparator } from '../../../components/shadcn/Dropdown';
|
||||
import { LanguageIcon } from '@heroicons/react/24/outline';
|
||||
import { MenuButton, MenuItem } from '../Menu';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { DEFAULT_LOCALE_STATE, LocaleAtom, LocalesAtom } from '../../state';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
import { LocalizationKey } from '../../../localization';
|
||||
|
||||
export interface ILanguageFilterProps { }
|
||||
|
||||
export const LanguageFilter: React.FunctionComponent<ILanguageFilterProps> = ({ }: React.PropsWithChildren<ILanguageFilterProps>) => {
|
||||
const locales = useRecoilValue(LocalesAtom);
|
||||
const [crntLocale, setCrntLocale] = useRecoilState(LocaleAtom);
|
||||
|
||||
const crntLocaleName = React.useMemo(() => {
|
||||
if (!crntLocale || !locales || locales.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const locale = locales.find(locale => locale.locale === crntLocale);
|
||||
|
||||
return locale?.title || locale?.locale;
|
||||
}, [crntLocale, locales]);
|
||||
|
||||
if (!locales || locales.length <= 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<MenuButton
|
||||
label={
|
||||
<>
|
||||
<LanguageIcon className={`inline-block w-4 h-4 mr-2`} />
|
||||
<span>{l10n.t(LocalizationKey.dashboardFiltersLanguageFilterLabel)}</span>
|
||||
</>
|
||||
}
|
||||
title={crntLocaleName || l10n.t(LocalizationKey.dashboardFiltersLanguageFilterAll)}
|
||||
/>
|
||||
|
||||
<DropdownMenuContent align='start'>
|
||||
<MenuItem
|
||||
title={l10n.t(LocalizationKey.dashboardFiltersLanguageFilterAll)}
|
||||
value={null}
|
||||
isCurrent={crntLocale === DEFAULT_LOCALE_STATE}
|
||||
onClick={() => setCrntLocale(DEFAULT_LOCALE_STATE)}
|
||||
/>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
{
|
||||
locales.map((locale) => (
|
||||
<MenuItem
|
||||
key={locale.locale}
|
||||
title={locale.title || locale.locale}
|
||||
value={locale.locale}
|
||||
isCurrent={locale.locale === crntLocale}
|
||||
onClick={(value) => setCrntLocale(value)}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
@@ -12,7 +12,9 @@ import {
|
||||
CategoryAtom,
|
||||
DEFAULT_TAG_STATE,
|
||||
DEFAULT_CATEGORY_STATE,
|
||||
FiltersAtom
|
||||
FiltersAtom,
|
||||
LocaleAtom,
|
||||
DEFAULT_LOCALE_STATE
|
||||
} from '../../state';
|
||||
import { DefaultValue } from 'recoil';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
@@ -34,12 +36,14 @@ export const ClearFilters: React.FunctionComponent<IClearFiltersProps> = (
|
||||
const folder = useRecoilValue(FolderSelector);
|
||||
const tag = useRecoilValue(TagSelector);
|
||||
const category = useRecoilValue(CategorySelector);
|
||||
const locale = useRecoilValue(LocaleAtom);
|
||||
const filters = useRecoilValue(FiltersAtom);
|
||||
|
||||
const resetSorting = useResetRecoilState(SortingAtom);
|
||||
const resetFolder = useResetRecoilState(FolderAtom);
|
||||
const resetTag = useResetRecoilState(TagAtom);
|
||||
const resetCategory = useResetRecoilState(CategoryAtom);
|
||||
const resetLocale = useResetRecoilState(LocaleAtom);
|
||||
const resetFilters = useResetRecoilState(FiltersAtom);
|
||||
|
||||
const reset = () => {
|
||||
@@ -48,6 +52,7 @@ export const ClearFilters: React.FunctionComponent<IClearFiltersProps> = (
|
||||
resetFolder();
|
||||
resetTag();
|
||||
resetCategory();
|
||||
resetLocale();
|
||||
resetFilters();
|
||||
};
|
||||
|
||||
@@ -61,19 +66,20 @@ export const ClearFilters: React.FunctionComponent<IClearFiltersProps> = (
|
||||
folder !== DEFAULT_FOLDER_STATE ||
|
||||
tag !== DEFAULT_TAG_STATE ||
|
||||
category !== DEFAULT_CATEGORY_STATE ||
|
||||
locale !== DEFAULT_LOCALE_STATE ||
|
||||
hasCustomFilters
|
||||
) {
|
||||
setShow(true);
|
||||
} else {
|
||||
setShow(false);
|
||||
}
|
||||
}, [folder, tag, category, hasCustomFilters]);
|
||||
}, [folder, tag, category, locale, hasCustomFilters]);
|
||||
|
||||
if (!show) return null;
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`flex items-center hover:text-[var(--vscode-textLink-activeForeground)]`}
|
||||
className={`flex items-center hover:text-[var(--vscode-statusBarItem-errorBackground)]`}
|
||||
onClick={reset}
|
||||
title={l10n.t(LocalizationKey.dashboardHeaderClearFiltersTitle)}
|
||||
>
|
||||
|
||||
@@ -40,7 +40,7 @@ export const Filter: React.FunctionComponent<IFilterProps> = ({
|
||||
<MenuItem
|
||||
title={DEFAULT_VALUE}
|
||||
value={null}
|
||||
isCurrent={!!activeItem}
|
||||
isCurrent={!activeItem}
|
||||
onClick={() => onClick(null)}
|
||||
/>
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { CategoryAtom, SettingsSelector, TagAtom, FiltersAtom, FilterValuesAtom
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { firstToUpper } from '../../../helpers/StringHelpers';
|
||||
import { LanguageFilter } from '../Filters/LanguageFilter';
|
||||
|
||||
export interface IFiltersProps { }
|
||||
|
||||
@@ -17,7 +18,6 @@ export const Filters: React.FunctionComponent<IFiltersProps> = (_: 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 otherFilterValues = useMemo(() => {
|
||||
@@ -74,6 +74,8 @@ export const Filters: React.FunctionComponent<IFiltersProps> = (_: React.PropsWi
|
||||
|
||||
return (
|
||||
<>
|
||||
<LanguageFilter />
|
||||
|
||||
{
|
||||
settings?.filters?.includes("pageFolders") && (
|
||||
<FoldersFilter />
|
||||
|
||||
@@ -119,7 +119,10 @@ export const ItemMenu: React.FunctionComponent<IItemMenuProps> = ({
|
||||
</>
|
||||
)}
|
||||
|
||||
<QuickAction title={l10n.t(LocalizationKey.dashboardMediaItemQuickActionDelete)} onClick={onDelete}>
|
||||
<QuickAction
|
||||
title={l10n.t(LocalizationKey.dashboardMediaItemQuickActionDelete)}
|
||||
className={`hover:text-[var(--vscode-statusBarItem-errorBackground)]`}
|
||||
onClick={onDelete}>
|
||||
<TrashIcon className={`w-4 h-4`} aria-hidden="true" />
|
||||
</QuickAction>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ export interface IMenuItemProps {
|
||||
title: JSX.Element | string;
|
||||
value?: any;
|
||||
isCurrent?: boolean;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
onClick: (value: any, e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||
}
|
||||
@@ -13,12 +14,13 @@ export const MenuItem: React.FunctionComponent<IMenuItemProps> = ({
|
||||
title,
|
||||
value,
|
||||
isCurrent,
|
||||
className,
|
||||
disabled,
|
||||
onClick
|
||||
}: React.PropsWithChildren<IMenuItemProps>) => {
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
className={`${!isCurrent ? `font-normal` : `font-bold`}`}
|
||||
className={`${!isCurrent ? `font-normal` : `font-bold`} ${className || ''}`}
|
||||
disabled={disabled}
|
||||
onClick={(e) => onClick(value, e)}
|
||||
>
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import * as React from 'react';
|
||||
import { cn } from '../../../utils/cn';
|
||||
|
||||
export interface IQuickActionProps {
|
||||
title: string;
|
||||
className?: string;
|
||||
onClick: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
}
|
||||
|
||||
export const QuickAction: React.FunctionComponent<IQuickActionProps> = ({
|
||||
title,
|
||||
className,
|
||||
onClick,
|
||||
children
|
||||
}: React.PropsWithChildren<IQuickActionProps>) => {
|
||||
@@ -15,7 +18,7 @@ export const QuickAction: React.FunctionComponent<IQuickActionProps> = ({
|
||||
type="button"
|
||||
title={title}
|
||||
onClick={onClick}
|
||||
className={`px-2 group inline-flex justify-center text-sm font-medium text-[var(--vscode-foreground)] hover:text-[var(--frontmatter-button-hoverBackground)]`}
|
||||
className={cn(`px-2 group inline-flex justify-center text-sm font-medium text-[var(--vscode-foreground)] hover:text-[var(--frontmatter-button-hoverBackground)]`, className)}
|
||||
>
|
||||
{children}
|
||||
<span className="sr-only">{title}</span>
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import * as React from 'react';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
import { messageHandler } from '@estruyf/vscode/dist/client';
|
||||
import { LocalizationKey } from '../../../localization';
|
||||
import { GeneralCommands, ExtensionState } from '../../../constants';
|
||||
import { SettingsInput } from './SettingsInput';
|
||||
import { VSCodeButton } from '@vscode/webview-ui-toolkit/react';
|
||||
|
||||
export interface IIntegrationsViewProps { }
|
||||
|
||||
export const IntegrationsView: React.FunctionComponent<IIntegrationsViewProps> = ({ }: React.PropsWithChildren<IIntegrationsViewProps>) => {
|
||||
const [deeplApiKey, setDeeplApiKey] = React.useState<string>('');
|
||||
const [crntDeeplApiKey, setCrntDeeplApiKey] = React.useState<string>('');
|
||||
|
||||
const onSave = React.useCallback(() => {
|
||||
messageHandler.request<string>(GeneralCommands.toVSCode.secrets.set, {
|
||||
key: ExtensionState.Secrets.DeeplApiKey,
|
||||
value: crntDeeplApiKey
|
||||
}).then((apiKey: string) => {
|
||||
setDeeplApiKey(apiKey);
|
||||
});
|
||||
}, [crntDeeplApiKey]);
|
||||
|
||||
const onChange = (_: string, value: string) => {
|
||||
setCrntDeeplApiKey(value);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
messageHandler.request<string>(GeneralCommands.toVSCode.secrets.get, ExtensionState.Secrets.DeeplApiKey).then((apiKey: string) => {
|
||||
setDeeplApiKey(apiKey);
|
||||
setCrntDeeplApiKey(apiKey);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className='w-full divide-y divide-[var(--frontmatter-border)]'>
|
||||
<div className='py-4 space-y-4'>
|
||||
<h2 className='text-xl mb-2'>{l10n.t(LocalizationKey.settingsIntegrationsViewDeeplTitle)}</h2>
|
||||
|
||||
<SettingsInput
|
||||
label={l10n.t(LocalizationKey.settingsIntegrationsViewDeeplIntputLabel)}
|
||||
name={ExtensionState.Secrets.DeeplApiKey}
|
||||
value={crntDeeplApiKey || ""}
|
||||
placeholder={l10n.t(LocalizationKey.settingsIntegrationsViewDeeplIntputPlaceholder)}
|
||||
onChange={onChange}
|
||||
/>
|
||||
|
||||
<div className={`mt-4 flex gap-2`}>
|
||||
<VSCodeButton
|
||||
onClick={onSave}
|
||||
disabled={deeplApiKey === crntDeeplApiKey}>
|
||||
{l10n.t(LocalizationKey.commonSave)}
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -12,6 +12,7 @@ import { COMMAND_NAME } from '../../../constants';
|
||||
import { ArrowPathIcon } from '@heroicons/react/24/outline';
|
||||
import { VSCodePanelTab, VSCodePanelView, VSCodePanels } from '@vscode/webview-ui-toolkit/react';
|
||||
import { CommonSettings } from './CommonSettings';
|
||||
import { IntegrationsView } from './IntegrationsView';
|
||||
|
||||
export interface ISettingsViewProps { }
|
||||
|
||||
@@ -54,6 +55,8 @@ export const SettingsView: React.FunctionComponent<ISettingsViewProps> = (_: Rea
|
||||
)
|
||||
}
|
||||
|
||||
<VSCodePanelTab id="view-4">{l10n.t(LocalizationKey.settingsViewIntegration)}</VSCodePanelTab>
|
||||
|
||||
<VSCodePanelView id="view-1">
|
||||
<CommonSettings />
|
||||
</VSCodePanelView>
|
||||
@@ -82,6 +85,10 @@ export const SettingsView: React.FunctionComponent<ISettingsViewProps> = (_: Rea
|
||||
</VSCodePanelView>
|
||||
)
|
||||
}
|
||||
|
||||
<VSCodePanelView id="view-4">
|
||||
<IntegrationsView />
|
||||
</VSCodePanelView>
|
||||
</VSCodePanels>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
FilterValuesAtom,
|
||||
FiltersAtom,
|
||||
FolderSelector,
|
||||
LocaleAtom,
|
||||
LocalesAtom,
|
||||
SearchSelector,
|
||||
SettingsSelector,
|
||||
SortingAtom,
|
||||
@@ -22,20 +24,25 @@ import { parseWinPath } from '../../helpers/parseWinPath';
|
||||
import { sortPages } from '../../utils/sortPages';
|
||||
import { ExtensionState } from '../../constants';
|
||||
import { SortingOption } from '../models';
|
||||
import { I18nConfig } from '../../models';
|
||||
import { usePrevious } from '../../panelWebView/hooks/usePrevious';
|
||||
|
||||
export default function usePages(pages: Page[]) {
|
||||
const [pageItems, setPageItems] = useRecoilState(AllPagesAtom);
|
||||
const [sortedPages, setSortedPages] = useState<Page[]>([]);
|
||||
const [pageItems, setPageItems] = useRecoilState(AllPagesAtom);
|
||||
const [sorting, setSorting] = useRecoilState(SortingAtom);
|
||||
const [tabInfo, setTabInfo] = useRecoilState(TabInfoAtom);
|
||||
const [locales, setLocales] = useRecoilState(LocalesAtom);
|
||||
const [, setFilterValues] = useRecoilState(FilterValuesAtom);
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
const tab = useRecoilValue(TabSelector);
|
||||
const folder = useRecoilValue(FolderSelector);
|
||||
const search = useRecoilValue(SearchSelector);
|
||||
const tag = useRecoilValue(TagSelector);
|
||||
const locale = useRecoilValue(LocaleAtom);
|
||||
const category = useRecoilValue(CategorySelector);
|
||||
const filters = useRecoilValue(FiltersAtom);
|
||||
const tabPrevious = usePrevious(tab);
|
||||
|
||||
/**
|
||||
* Process all the pages by applying the sorting, filtering and searching.
|
||||
@@ -90,6 +97,11 @@ export default function usePages(pages: Page[]) {
|
||||
);
|
||||
}
|
||||
|
||||
// If filtered by locale
|
||||
if (locale) {
|
||||
pagesSorted = pagesSorted.filter((page) => page.fmLocale && page.fmLocale.locale === locale);
|
||||
}
|
||||
|
||||
const filterNames = Object.keys(filters);
|
||||
if (filterNames.length > 0) {
|
||||
for (const filter of filterNames) {
|
||||
@@ -102,7 +114,7 @@ export default function usePages(pages: Page[]) {
|
||||
|
||||
setSortedPages(pagesSorted);
|
||||
},
|
||||
[settings, tab, folder, search, tag, category, sorting, tabInfo, filters]
|
||||
[settings, tab, folder, search, tag, category, locale, sorting, tabInfo, filters]
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -114,6 +126,24 @@ export default function usePages(pages: Page[]) {
|
||||
|
||||
let crntPages: Page[] = Object.assign([], pages);
|
||||
|
||||
// Update the translations of pages
|
||||
crntPages = crntPages.map((page) => {
|
||||
if (page.fmTranslations) {
|
||||
const translations = Object.assign({}, page.fmTranslations);
|
||||
|
||||
for (const [key, value] of Object.entries(translations)) {
|
||||
const translatedPage = crntPages.find((p) => parseWinPath(p.fmFilePath).toLowerCase() === parseWinPath(value.path).toLowerCase());
|
||||
if (!translatedPage) {
|
||||
delete translations[key];
|
||||
}
|
||||
}
|
||||
|
||||
return { ...page, fmTranslations: translations };
|
||||
}
|
||||
|
||||
return page;
|
||||
});
|
||||
|
||||
// Process the tab data
|
||||
const draftTypes = Object.assign({}, tabInfo);
|
||||
draftTypes[Tab.All] = crntPages.length;
|
||||
@@ -190,10 +220,21 @@ 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);
|
||||
},
|
||||
[tab, tabInfo, settings, filters]
|
||||
[tab, tabInfo, settings, filters, locales, tabPrevious]
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -235,7 +276,7 @@ export default function usePages(pages: Page[]) {
|
||||
} else {
|
||||
startPageProcessing();
|
||||
}
|
||||
}, [settings?.draftField, pages, sorting, search, tag, category, filters, folder]);
|
||||
}, [settings?.draftField, pages, sorting, search, tag, category, locale, filters, folder]);
|
||||
|
||||
useEffect(() => {
|
||||
processByTab(sortedPages);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { I18nConfig } from '../../models';
|
||||
|
||||
export interface Page {
|
||||
// Properties for caching
|
||||
fmCachePath: string;
|
||||
@@ -19,6 +21,16 @@ export interface Page {
|
||||
fmContentType: string;
|
||||
fmDateFormat: string | undefined;
|
||||
|
||||
// i18n fields
|
||||
fmDefaultLocale?: boolean;
|
||||
fmLocale?: I18nConfig;
|
||||
fmTranslations?: {
|
||||
[locale: string]: {
|
||||
locale: I18nConfig;
|
||||
path: string;
|
||||
}
|
||||
};
|
||||
|
||||
title: string;
|
||||
slug: string;
|
||||
date: string | Date;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const DEFAULT_LOCALE_STATE = '';
|
||||
|
||||
export const LocaleAtom = atom<string | null>({
|
||||
key: 'LocaleAtom',
|
||||
default: DEFAULT_LOCALE_STATE
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
import { atom } from 'recoil';
|
||||
import { I18nConfig } from '../../../models';
|
||||
|
||||
export const LocalesAtom = atom<I18nConfig[] | undefined>({
|
||||
key: 'LocalesAtom',
|
||||
default: undefined
|
||||
});
|
||||
@@ -9,6 +9,8 @@ export * from './FolderAtom';
|
||||
export * from './GroupingAtom';
|
||||
export * from './LightboxAtom';
|
||||
export * from './LoadingAtom';
|
||||
export * from './LocaleAtom';
|
||||
export * from './LocalesAtom';
|
||||
export * from './MediaFoldersAtom';
|
||||
export * from './MediaTotalAtom';
|
||||
export * from './ModeAtom';
|
||||
|
||||
+4
-2
@@ -13,11 +13,10 @@ import {
|
||||
} from './helpers';
|
||||
import ContentProvider from './providers/ContentProvider';
|
||||
import { PagesListener } from './listeners/dashboard';
|
||||
import { NavigationType } from './dashboardWebView/models';
|
||||
import { ModeSwitch } from './services/ModeSwitch';
|
||||
import { PagesParser } from './services/PagesParser';
|
||||
import { ContentType, Telemetry, Extension } from './helpers';
|
||||
import { TaxonomyType, DashboardData } from './models';
|
||||
import { TaxonomyType } from './models';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
import {
|
||||
Backers,
|
||||
@@ -37,6 +36,7 @@ import {
|
||||
} from './commands';
|
||||
import { join } from 'path';
|
||||
import { Terminal } from './services';
|
||||
import { i18n } from './commands/i18n';
|
||||
|
||||
let pageUpdateDebouncer: { (fnc: any, time: number): void };
|
||||
let editDebounce: { (fnc: any, time: number): void };
|
||||
@@ -89,6 +89,8 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
Dashboard.init();
|
||||
Dashboard.registerCommands();
|
||||
|
||||
i18n.register();
|
||||
|
||||
if (!extension.getVersion().usedVersion) {
|
||||
vscode.commands.executeCommand(COMMAND_NAME.dashboard);
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ import {
|
||||
import { format, parse } from 'date-fns';
|
||||
import { Notifications } from './Notifications';
|
||||
import { Article } from '../commands';
|
||||
import { join } from 'path';
|
||||
import { join, parse as parseFile } from 'path';
|
||||
import { EditorHelper } from '@estruyf/vscode';
|
||||
import sanitize from '../helpers/Sanitize';
|
||||
import { ContentType as IContentType } from '../models';
|
||||
@@ -49,7 +49,7 @@ import { Link, Parent } from 'mdast-util-from-markdown/lib';
|
||||
import { Content } from 'mdast';
|
||||
import { CustomScript } from './CustomScript';
|
||||
import { Folders } from '../commands/Folders';
|
||||
import { existsAsync, readFileAsync } from '../utils';
|
||||
import { existsAsync } from '../utils';
|
||||
import { mkdirAsync } from '../utils/mkdirAsync';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
import { LocalizationKey } from '../localization';
|
||||
@@ -123,8 +123,14 @@ export class ArticleHelper {
|
||||
* Retrieve the file's front matter by its path
|
||||
* @param filePath
|
||||
*/
|
||||
public static async getFrontMatterByPath(filePath: string) {
|
||||
const file = await readFileAsync(filePath, { encoding: 'utf-8' });
|
||||
public static async getFrontMatterByPath(
|
||||
filePath: string
|
||||
): Promise<ParsedFrontMatter | undefined> {
|
||||
const file = await ArticleHelper.getContents(filePath);
|
||||
if (!file) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const article = ArticleHelper.parseFile(file, filePath);
|
||||
if (!article) {
|
||||
return undefined;
|
||||
@@ -136,6 +142,20 @@ export class ArticleHelper {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the contents of a file asynchronously.
|
||||
* @param filePath - The path of the file to read.
|
||||
* @returns A promise that resolves to the contents of the file, or undefined if the file does not exist.
|
||||
*/
|
||||
public static async getContents(filePath: string): Promise<string | undefined> {
|
||||
const file = await workspace.fs.readFile(Uri.file(parseWinPath(filePath)));
|
||||
if (!file) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return new TextDecoder().decode(file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the new information in the file
|
||||
*
|
||||
@@ -282,6 +302,35 @@ export class ArticleHelper {
|
||||
return isSupportedLanguage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given file path represents a page bundle.
|
||||
*
|
||||
* @param filePath - The path of the file to check.
|
||||
* @returns A boolean indicating whether the file is a page bundle or not.
|
||||
*/
|
||||
public static async isPageBundle(filePath: string) {
|
||||
let article = await ArticleHelper.getFrontMatterByPath(filePath);
|
||||
if (!article) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const contentType = ArticleHelper.getContentType(article);
|
||||
return !!contentType.pageBundle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the page folder from the given bundle file path.
|
||||
*
|
||||
* @param filePath - The file path of the bundle.
|
||||
* @returns The page folder path.
|
||||
*/
|
||||
public static getPageFolderFromBundlePath(filePath: string) {
|
||||
// Remove the last folder from the dir
|
||||
const dir = parseFile(filePath).dir;
|
||||
const lastSlash = dir.lastIndexOf('/');
|
||||
return dir.substring(0, lastSlash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get date from front matter
|
||||
*/
|
||||
@@ -370,21 +419,9 @@ export class ArticleHelper {
|
||||
if (article.data.type) {
|
||||
contentType = contentTypes.find((ct) => ct.name === article.data.type);
|
||||
} else if (!contentType && article.path) {
|
||||
// Get the content type by the folder name
|
||||
let folders = Folders.get();
|
||||
let parsedPath = parseWinPath(article.path);
|
||||
let pageFolderMatches = folders.filter(
|
||||
(folder) => parsedPath && folder.path && parsedPath.includes(folder.path)
|
||||
);
|
||||
|
||||
// Sort by longest path
|
||||
pageFolderMatches = pageFolderMatches.sort((a, b) => b.path.length - a.path.length);
|
||||
if (
|
||||
pageFolderMatches.length > 0 &&
|
||||
pageFolderMatches[0].contentTypes &&
|
||||
pageFolderMatches[0].contentTypes.length === 1
|
||||
) {
|
||||
const contentTypeName = pageFolderMatches[0].contentTypes[0];
|
||||
const pageFolder = Folders.getPageFolderByFilePath(article.path);
|
||||
if (pageFolder && pageFolder.contentTypes?.length === 1) {
|
||||
const contentTypeName = pageFolder.contentTypes[0];
|
||||
contentType = contentTypes.find((ct) => ct.name === contentTypeName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1039,7 +1039,6 @@ export class ContentType {
|
||||
}
|
||||
} else {
|
||||
const defaultValue = field.default;
|
||||
console.log(field.name, defaultValue, Array.isArray(defaultValue));
|
||||
|
||||
if (typeof defaultValue === 'string') {
|
||||
data[field.name] = await ContentType.processFieldPlaceholders(
|
||||
|
||||
@@ -424,6 +424,14 @@ export class Extension {
|
||||
}
|
||||
}
|
||||
|
||||
public async getSecret(key: string): Promise<string | undefined> {
|
||||
return this.ctx.secrets.get(key);
|
||||
}
|
||||
|
||||
public async setSecret(key: string, value: string): Promise<void> {
|
||||
return this.ctx.secrets.store(key, value);
|
||||
}
|
||||
|
||||
public isBetaVersion() {
|
||||
return basename(this.ctx.globalStorageUri.fsPath) === EXTENSION_BETA_ID;
|
||||
}
|
||||
|
||||
@@ -151,6 +151,60 @@ export class FrameworkDetector {
|
||||
return parseWinPath(relAssetPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute path by combining the relative path and the file path.
|
||||
* If a static folder is configured, it will be taken into account.
|
||||
* @param relAssetPath The relative path.
|
||||
* @param filePath The file path.
|
||||
* @returns The absolute path.
|
||||
*/
|
||||
public static getAbsPathByFile(relAssetPath: string, filePath: string): string {
|
||||
const staticFolderValue = Settings.get<string | StaticFolder>(SETTING_CONTENT_STATIC_FOLDER);
|
||||
const staticFolder = Folders.getStaticFolderRelativePath();
|
||||
|
||||
if (
|
||||
staticFolderValue &&
|
||||
staticFolder &&
|
||||
typeof staticFolderValue !== 'string' &&
|
||||
staticFolderValue.relative
|
||||
) {
|
||||
const fileDir = dirname(filePath);
|
||||
return parseWinPath(join(fileDir, relAssetPath));
|
||||
}
|
||||
|
||||
return relAssetPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the relative path of an asset file based on the provided absolute asset path and file path.
|
||||
* If the static folder setting is configured and the static folder is available, the relative path is calculated based on the file and asset directories.
|
||||
* Otherwise, the absolute asset path is returned as is.
|
||||
*
|
||||
* @param absAssetPath The absolute path of the asset file.
|
||||
* @param fileDir The path of the directory
|
||||
* @returns The relative path of the asset file.
|
||||
*/
|
||||
public static getRelPathByFileDir(absAssetPath: string, fileDir: string): string {
|
||||
const staticFolderValue = Settings.get<string | StaticFolder>(SETTING_CONTENT_STATIC_FOLDER);
|
||||
const staticFolder = Folders.getStaticFolderRelativePath();
|
||||
|
||||
if (
|
||||
staticFolderValue &&
|
||||
staticFolder &&
|
||||
typeof staticFolderValue !== 'string' &&
|
||||
staticFolderValue.relative
|
||||
) {
|
||||
const assetDir = dirname(absAssetPath);
|
||||
const fileName = parse(absAssetPath);
|
||||
|
||||
let relAssetPath = relative(fileDir, assetDir);
|
||||
relAssetPath = join(relAssetPath, `${fileName.name}${fileName.ext}`);
|
||||
return parseWinPath(relAssetPath);
|
||||
}
|
||||
|
||||
return absAssetPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the default settings for Hexo
|
||||
*/
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Folders } from '../../commands/Folders';
|
||||
import {
|
||||
COMMAND_NAME,
|
||||
ExtensionState,
|
||||
GeneralCommands,
|
||||
SETTING_CONTENT_STATIC_FOLDER,
|
||||
SETTING_FRAMEWORK_ID,
|
||||
SETTING_PREVIEW_HOST
|
||||
@@ -61,9 +62,21 @@ export class SettingsListener extends BaseListener {
|
||||
case DashboardMessage.setSettings:
|
||||
this.setConfigSettings(msg);
|
||||
break;
|
||||
case GeneralCommands.toVSCode.secrets.get:
|
||||
this.getSecretValue(msg.command, msg.payload, msg.requestId);
|
||||
break;
|
||||
case GeneralCommands.toVSCode.secrets.set:
|
||||
this.setSecretValue(msg.command, msg.payload, msg.requestId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the configuration settings based on the provided payload.
|
||||
* @param command - The command to execute.
|
||||
* @param requestId - The ID of the request.
|
||||
* @param payload - The payload containing the settings to retrieve.
|
||||
*/
|
||||
public static async getConfigSettings({ command, requestId, payload }: PostMessageData) {
|
||||
if (!command || !requestId || !payload) {
|
||||
return;
|
||||
@@ -96,6 +109,10 @@ export class SettingsListener extends BaseListener {
|
||||
this.sendRequest(command as any, requestId, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches the current project to the specified project.
|
||||
* @param project - The name of the project to switch to.
|
||||
*/
|
||||
public static async switchProject(project: string) {
|
||||
if (project) {
|
||||
this.sendMsg(DashboardCommand.loading, 'loading' as LoadingType);
|
||||
@@ -123,6 +140,50 @@ export class SettingsListener extends BaseListener {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the secret value for a given command and value.
|
||||
* @param command - The command to retrieve the secret value for.
|
||||
* @param key - The key associated with the secret.
|
||||
* @param requestId - Optional. The ID of the request.
|
||||
* @returns A Promise that resolves to the secret value.
|
||||
*/
|
||||
private static async getSecretValue(command: string, key: string, requestId?: string) {
|
||||
if (!command || !requestId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const extension = Extension.getInstance();
|
||||
const value = await extension.getSecret(key);
|
||||
|
||||
this.sendRequest(command as any, requestId, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the secret value for a given key.
|
||||
* @param command - The command to execute.
|
||||
* @param key - The key for the secret value.
|
||||
* @param value - The secret value to set.
|
||||
* @param requestId - Optional. The request ID.
|
||||
*/
|
||||
private static async setSecretValue(
|
||||
command: string,
|
||||
{ key, value }: { key: string; value: string },
|
||||
requestId?: string
|
||||
) {
|
||||
if (!command || !requestId || !key) {
|
||||
return;
|
||||
}
|
||||
|
||||
const extension = Extension.getInstance();
|
||||
await extension.setSecret(key, value || '');
|
||||
|
||||
Notifications.info(
|
||||
l10n.t(LocalizationKey.listenersDashboardSettingsListenerSetSecretValueMessage)
|
||||
);
|
||||
|
||||
this.sendRequest(command as any, requestId, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a setting from the dashboard
|
||||
* @param data
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Dashboard } from '../../commands/Dashboard';
|
||||
import { PanelProvider } from '../../panelWebView/PanelProvider';
|
||||
import { ArticleHelper, Extension } from '../../helpers';
|
||||
import { Logger } from '../../helpers/Logger';
|
||||
import { commands, Uri, window } from 'vscode';
|
||||
import { commands, Uri, window, workspace } from 'vscode';
|
||||
import { PostMessageData } from '../../models';
|
||||
import { Preview } from '../../commands';
|
||||
import { urlJoin } from 'url-join-ts';
|
||||
@@ -19,6 +19,12 @@ export abstract class BaseListener {
|
||||
case GeneralCommands.toVSCode.openOnWebsite:
|
||||
this.openOnWebsite(msg.payload);
|
||||
break;
|
||||
case GeneralCommands.toVSCode.runCommand:
|
||||
if (msg.payload) {
|
||||
const { command, args } = msg.payload;
|
||||
commands.executeCommand(command, args);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -175,6 +175,10 @@ export enum LocalizationKey {
|
||||
* Astro
|
||||
*/
|
||||
settingsViewAstro = 'settings.view.astro',
|
||||
/**
|
||||
* Integration
|
||||
*/
|
||||
settingsViewIntegration = 'settings.view.integration',
|
||||
/**
|
||||
* Open dashboard on startup
|
||||
*/
|
||||
@@ -219,6 +223,10 @@ export enum LocalizationKey {
|
||||
* Read more about Git submodules
|
||||
*/
|
||||
settingsGitSubmoduleLink = 'settings.git.submoduleLink',
|
||||
/**
|
||||
* Integration
|
||||
*/
|
||||
settingsIntegrationTitle = 'settings.integration.title',
|
||||
/**
|
||||
* Website and SSG settings
|
||||
*/
|
||||
@@ -235,6 +243,18 @@ export enum LocalizationKey {
|
||||
* SSG/Framework start command
|
||||
*/
|
||||
settingsCommonSettingsStartCommand = 'settings.commonSettings.startCommand',
|
||||
/**
|
||||
* DeepL
|
||||
*/
|
||||
settingsIntegrationsViewDeeplTitle = 'settings.integrationsView.deepl.title',
|
||||
/**
|
||||
* Authentication key
|
||||
*/
|
||||
settingsIntegrationsViewDeeplIntputLabel = 'settings.integrationsView.deepl.intput.label',
|
||||
/**
|
||||
* Enter your DeepL authentication key
|
||||
*/
|
||||
settingsIntegrationsViewDeeplIntputPlaceholder = 'settings.integrationsView.deepl.intput.placeholder',
|
||||
/**
|
||||
* Developer mode
|
||||
*/
|
||||
@@ -315,6 +335,14 @@ export enum LocalizationKey {
|
||||
* Are you sure you want to delete the "{0}" content?
|
||||
*/
|
||||
dashboardContentsContentActionsAlertDescription = 'dashboard.contents.contentActions.alert.description',
|
||||
/**
|
||||
* Create translation
|
||||
*/
|
||||
dashboardContentsContentActionsTranslationsCreate = 'dashboard.contents.contentActions.translations.create',
|
||||
/**
|
||||
* Translations
|
||||
*/
|
||||
dashboardContentsContentActionsTranslationsMenu = 'dashboard.contents.contentActions.translations.menu',
|
||||
/**
|
||||
* <invalid title>
|
||||
*/
|
||||
@@ -427,6 +455,14 @@ export enum LocalizationKey {
|
||||
* Please close the dashboard and try again.
|
||||
*/
|
||||
dashboardErrorViewDescription = 'dashboard.errorView.description',
|
||||
/**
|
||||
* Locale
|
||||
*/
|
||||
dashboardFiltersLanguageFilterLabel = 'dashboard.filters.languageFilter.label',
|
||||
/**
|
||||
* All
|
||||
*/
|
||||
dashboardFiltersLanguageFilterAll = 'dashboard.filters.languageFilter.all',
|
||||
/**
|
||||
* Home
|
||||
*/
|
||||
@@ -1672,6 +1708,46 @@ export enum LocalizationKey {
|
||||
* Create folder
|
||||
*/
|
||||
commandsFoldersGetNotificationErrorCreateAction = 'commands.folders.get.notificationError.create.action',
|
||||
/**
|
||||
* No file selected.
|
||||
*/
|
||||
commandsI18nCreateWarningNoFileSelected = 'commands.i18n.create.warning.noFileSelected',
|
||||
/**
|
||||
* The file could not be retrieved.
|
||||
*/
|
||||
commandsI18nCreateWarningNoFile = 'commands.i18n.create.warning.noFile',
|
||||
/**
|
||||
* Content type could not be retrieved for the current file.
|
||||
*/
|
||||
commandsI18nCreateWarningNoContentType = 'commands.i18n.create.warning.noContentType',
|
||||
/**
|
||||
* No i18n configuration found.
|
||||
*/
|
||||
commandsI18nCreateWarningNoConfig = 'commands.i18n.create.warning.noConfig',
|
||||
/**
|
||||
* The current file cannot be used for i18n content creation.
|
||||
*/
|
||||
commandsI18nCreateWarningNotDefaultLocale = 'commands.i18n.create.warning.notDefaultLocale',
|
||||
/**
|
||||
* The i18n translation already exists.
|
||||
*/
|
||||
commandsI18nCreateErrorFileExists = 'commands.i18n.create.error.fileExists',
|
||||
/**
|
||||
* Created "{0}" i18n content file.
|
||||
*/
|
||||
commandsI18nCreateSuccessCreated = 'commands.i18n.create.success.created',
|
||||
/**
|
||||
* Create content for locale
|
||||
*/
|
||||
commandsI18nCreateQuickPickTitle = 'commands.i18n.create.quickPick.title',
|
||||
/**
|
||||
* To which locale do you want to create a new content?
|
||||
*/
|
||||
commandsI18nCreateQuickPickPlaceHolder = 'commands.i18n.create.quickPick.placeHolder',
|
||||
/**
|
||||
* Translating content...
|
||||
*/
|
||||
commandsI18nTranslateProgressTitle = 'commands.i18n.translate.progress.title',
|
||||
/**
|
||||
* Preview: {0}
|
||||
*/
|
||||
@@ -2352,6 +2428,10 @@ export enum LocalizationKey {
|
||||
* Failed to initialize the template.
|
||||
*/
|
||||
listenersDashboardSettingsListenerTriggerTemplateInitError = 'listeners.dashboard.settingsListener.triggerTemplate.init.error',
|
||||
/**
|
||||
* Setting has been updated.
|
||||
*/
|
||||
listenersDashboardSettingsListenerSetSecretValueMessage = 'listeners.dashboard.settingsListener.setSecretValue.message',
|
||||
/**
|
||||
* Snippet missing title or body
|
||||
*/
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { I18nConfig } from './i18nConfig';
|
||||
|
||||
export interface ContentFolder {
|
||||
title: string;
|
||||
path: string;
|
||||
@@ -10,4 +12,6 @@ export interface ContentFolder {
|
||||
originalPath?: string;
|
||||
$schema?: string;
|
||||
extended?: boolean;
|
||||
defaultLocale?: string;
|
||||
locales: I18nConfig[];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface I18nConfig {
|
||||
locale: string;
|
||||
title?: string;
|
||||
path?: string;
|
||||
}
|
||||
@@ -31,3 +31,4 @@ export * from './TaxonomyType';
|
||||
export * from './Template';
|
||||
export * from './UnmappedMedia';
|
||||
export * from './VersionInfo';
|
||||
export * from './i18nConfig';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { i18n } from './../commands/i18n';
|
||||
import { STATIC_FOLDER_PLACEHOLDER } from './../constants/StaticFolderPlaceholder';
|
||||
import { parseWinPath } from './../helpers/parseWinPath';
|
||||
import { dirname, extname, join } from 'path';
|
||||
@@ -76,6 +77,7 @@ export class PagesParser {
|
||||
* Parse all pages in the workspace
|
||||
*/
|
||||
public static async parsePages() {
|
||||
i18n.clearFiles();
|
||||
const ext = Extension.getInstance();
|
||||
|
||||
// Update the dashboard with the fresh data
|
||||
@@ -209,6 +211,10 @@ export class PagesParser {
|
||||
escapedDescription = '<invalid title>';
|
||||
}
|
||||
|
||||
const isDefaultLanguage = await i18n.isDefaultLanguage(filePath);
|
||||
const locale = await i18n.getLocale(filePath);
|
||||
const translations = await i18n.getTranslations(filePath);
|
||||
|
||||
const page: Page = {
|
||||
...article.data,
|
||||
// Cache properties
|
||||
@@ -230,6 +236,10 @@ export class PagesParser {
|
||||
fmContentType: contentType.name || DEFAULT_CONTENT_TYPE_NAME,
|
||||
fmBody: article?.content || '',
|
||||
fmDateFormat: dateFormat,
|
||||
// i18n properties
|
||||
fmDefaultLocale: isDefaultLanguage,
|
||||
fmLocale: locale,
|
||||
fmTranslations: translations,
|
||||
// Make sure these are always set
|
||||
title: escapedTitle,
|
||||
description: escapedDescription,
|
||||
|
||||
Reference in New Issue
Block a user