mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-08-06 08:52:47 +02:00
Merge branch 'dev' of github.com:estruyf/vscode-front-matter into dev
This commit is contained in:
+3
-3
@@ -1,12 +1,13 @@
|
||||
# Change Log
|
||||
|
||||
## [9.5.0] - 2024-xx-xx
|
||||
## [10.0.0] - 2024-xx-xx - [Release notes](https://beta.frontmatter.codes/updates/v10.0.0)
|
||||
|
||||
### ✨ New features
|
||||
|
||||
- [#731](https://github.com/estruyf/vscode-front-matter/issues/731): Added the ability to map/unmap taxonomy to multiple pages at once
|
||||
- [#746](https://github.com/estruyf/vscode-front-matter/issues/746): Placeholder support added to to the `slug` field
|
||||
- [#749](https://github.com/estruyf/vscode-front-matter/issues/749): Ability to set your own filters on the content dashboard with the `frontMatter.content.filters` setting
|
||||
- [#756](https://github.com/estruyf/vscode-front-matter/issues/756): i18n/multilingual content support
|
||||
|
||||
### 🎨 Enhancements
|
||||
|
||||
@@ -17,8 +18,7 @@
|
||||
- [#741](https://github.com/estruyf/vscode-front-matter/issues/741): Added message on the content dashboard when content is processed
|
||||
- [#747](https://github.com/estruyf/vscode-front-matter/issues/747): The `@frontmatter/extensibility` dependency now supports scripts for placeholders
|
||||
- [#752](https://github.com/estruyf/vscode-front-matter/issues/752): Placeholder support in default `list` field values
|
||||
|
||||
### ⚡️ Optimizations
|
||||
- Support for using the `fieldCollection` field in a `block` field
|
||||
|
||||
- Updated the list of commands which are available in the command palette
|
||||
|
||||
|
||||
@@ -54,6 +54,12 @@ A couple of our extension highlights that hopefully get you interested in giving
|
||||
|
||||
> If you see something missing in your article creation flow, please feel free to reach out.
|
||||
|
||||
**Version 10**
|
||||
|
||||
In version 10, we introduced the new i18n/multilingual support for your content. You can now manage your content in multiple languages, more information can be found in the [multilingual](https://frontmatter.codes/docs/content-creation/multilingual) section of our documentation.
|
||||
|
||||

|
||||
|
||||
**Version 9**
|
||||
|
||||
The extension is now available in multiple languages: English, German, and Japanese. Want to add your language? Check out the [localization the extension](https://frontmatter.codes/docs/contributing#translating-the-extension).
|
||||
@@ -187,6 +193,27 @@ You can open showcase issues for the following things:
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## 📊 Telemetry
|
||||
|
||||
The Front Matter CMS extension collects telemetry data to help us build a better understand which features from the CMS are used. The extension respects the `telemetry.enableTelemetry` setting which you can learn more about in the [Visual Studio Code FAQ](https://aka.ms/vscode-remote/telemetry), or you can only disable it for the extension by configuring the `frontMatter.telemetry.disable` setting.
|
||||
|
||||
We only collect the following data:
|
||||
|
||||
- Type of event
|
||||
- Extension title (main or beta)
|
||||
- Extension version
|
||||
|
||||
No user-specific data is collected, you can check the telemetry implementation in the following files:
|
||||
|
||||
- [Telemetry class](https://github.com/estruyf/vscode-front-matter/blob/59528a3db01be8d34dc40638e6cf827090e31986/src/helpers/Telemetry.ts)
|
||||
- [Metrics API](https://github.com/FrontMatter/web-documentation-nextjs/blob/main/pages/api/metrics.ts)
|
||||
|
||||
For crash reports in the webviews, we make use of Sentry to help us understand what went wrong. This data is only used to fix issues and improve the extension. You can find more information about the Sentry implementation in the following files:
|
||||
|
||||
- [Sentry config](https://github.com/estruyf/vscode-front-matter/blob/63e296d62f11be73ac86d9e823084247952a7ddc/src/utils/sentryInit.ts)
|
||||
|
||||
> The user ip address is not collected.
|
||||
|
||||
## 🔑 License
|
||||
|
||||
[MIT](./LICENSE)
|
||||
|
||||
@@ -52,6 +52,12 @@ A couple of our extension highlights that hopefully get you interested in giving
|
||||
|
||||
> If you see something missing in your article creation flow, please feel free to reach out.
|
||||
|
||||
**Version 10**
|
||||
|
||||
In version 10, we introduced the new i18n/multilingual support for your content. You can now manage your content in multiple languages, more information can be found in the [multilingual](https://frontmatter.codes/docs/content-creation/multilingual) section of our documentation.
|
||||
|
||||

|
||||
|
||||
**Version 9**
|
||||
|
||||
The extension is now available in multiple languages: English, German, and Japanese. Want to add your language? Check out the [localization the extension](https://frontmatter.codes/docs/contributing#translating-the-extension).
|
||||
@@ -193,6 +199,27 @@ You can open showcase issues for the following things:
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## 📊 Telemetry
|
||||
|
||||
The Front Matter CMS extension collects telemetry data to help us build a better understand which features from the CMS are used. The extension respects the `telemetry.enableTelemetry` setting which you can learn more about in the [Visual Studio Code FAQ](https://aka.ms/vscode-remote/telemetry), or you can only disable it for the extension by configuring the `frontMatter.telemetry.disable` setting.
|
||||
|
||||
We only collect the following data:
|
||||
|
||||
- Type of event
|
||||
- Extension title (main or beta)
|
||||
- Extension version
|
||||
|
||||
No user-specific data is collected, you can check the telemetry implementation in the following files:
|
||||
|
||||
- [Telemetry class](https://github.com/estruyf/vscode-front-matter/blob/59528a3db01be8d34dc40638e6cf827090e31986/src/helpers/Telemetry.ts)
|
||||
- [Metrics API](https://github.com/FrontMatter/web-documentation-nextjs/blob/main/pages/api/metrics.ts)
|
||||
|
||||
For crash reports in the webviews, we make use of Sentry to help us understand what went wrong. This data is only used to fix issues and improve the extension. You can find more information about the Sentry implementation in the following files:
|
||||
|
||||
- [Sentry config](https://github.com/estruyf/vscode-front-matter/blob/63e296d62f11be73ac86d9e823084247952a7ddc/src/utils/sentryInit.ts)
|
||||
|
||||
> The user ip address is not collected.
|
||||
|
||||
## 🔑 License
|
||||
|
||||
[MIT](./LICENSE)
|
||||
|
||||
+12
-6
@@ -67,8 +67,14 @@
|
||||
"settings.commonSettings.startCommand": "SSG/Framework start command",
|
||||
|
||||
"settings.integrationsView.deepl.title": "DeepL",
|
||||
"settings.integrationsView.deepl.intput.label": "Authentication key",
|
||||
"settings.integrationsView.deepl.intput.placeholder": "Enter your DeepL authentication key",
|
||||
"settings.integrationsView.deepl.intput.label": "API key",
|
||||
"settings.integrationsView.deepl.intput.placeholder": "Enter your Azure Translator API key",
|
||||
|
||||
"settings.integrationsView.azure.title": "Azure AI Translator Service",
|
||||
"settings.integrationsView.azure.intput.label": "Subscription key",
|
||||
"settings.integrationsView.azure.intput.placeholder": "Enter your Azure AI Translator - Subscription key",
|
||||
"settings.integrationsView.azure.region.label": "Region",
|
||||
"settings.integrationsView.azure.region.placeholder": "Enter your Azure AI Translator - Region. Example: westeurope",
|
||||
|
||||
"developer.title": "Developer mode",
|
||||
"developer.reload.title": "Reload the dashboard",
|
||||
@@ -539,7 +545,9 @@
|
||||
"commands.i18n.create.warning.noFile": "The file could not be retrieved.",
|
||||
"commands.i18n.create.warning.noContentType": "Content type could not be retrieved for the current file.",
|
||||
"commands.i18n.create.warning.noConfig": "No i18n configuration found.",
|
||||
"commands.i18n.create.warning.notDefaultLocale": "The current file cannot be used for i18n content creation.",
|
||||
"commands.i18n.create.error.noLocaleDefinition": "Could not retrieve the locale for the current file.",
|
||||
"commands.i18n.create.error.noLocales": "Current file has been translated to all available languages.",
|
||||
"commands.i18n.create.error.noContentFolder": "Could not define a content folder for the current file.",
|
||||
"commands.i18n.create.error.fileExists": "The i18n translation already exists.",
|
||||
"commands.i18n.create.success.created": "Created \"{0}\" i18n content file.",
|
||||
"commands.i18n.create.quickPick.title": "Create content for locale",
|
||||
@@ -656,9 +664,6 @@
|
||||
"helpers.extension.getVersion.changelog": "Check the changelog",
|
||||
"helpers.extension.getVersion.starIt": "Give it a ⭐️",
|
||||
"helpers.extension.getVersion.update.notification": "{0} has been updated to v{1} — check out what's new!",
|
||||
"helpers.extension.migrateSettings.deprecated.warning": "The \"{0}\" and \"{1}\" settings have been deprecated. Please use the \"isPublishDate\" and \"isModifiedDate\" datetime field properties instead.",
|
||||
"helpers.extension.migrateSettings.deprecated.warning.hide": "Hide",
|
||||
"helpers.extension.migrateSettings.deprecated.warning.seeGuide": "See migration guide",
|
||||
"helpers.extension.migrateSettings.templates.quickPick.title": "{0} - Templates",
|
||||
"helpers.extension.migrateSettings.templates.quickPick.placeholder": "Do you want to keep on using the template functionality?",
|
||||
"helpers.extension.checkIfExtensionCanRun.warning": "Front Matter BETA cannot be used while the stable version is installed. Please ensure that you have only over version installed.",
|
||||
@@ -693,6 +698,7 @@
|
||||
"helpers.questions.selectContentType.quickPick.title": "Content type",
|
||||
"helpers.questions.selectContentType.quickPick.placeholder": "Select the content type to create your new content",
|
||||
"helpers.questions.selectContentType.noSelection.warning": "No content type was selected.",
|
||||
"helpers.questions.selectContentType.quickPick.error.noContentTypes": "There are no matching content types configured for this folder.",
|
||||
|
||||
"helpers.seoHelper.checkLength.diagnostic.message": "Article {0} is longer than {1} characters (current length: {2}). For SEO reasons, it would be better to make it less than {1} characters.",
|
||||
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "vscode-front-matter-beta",
|
||||
"version": "9.5.0",
|
||||
"version": "10.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "vscode-front-matter-beta",
|
||||
"version": "9.5.0",
|
||||
"version": "10.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6"
|
||||
|
||||
+12
-29
@@ -3,7 +3,7 @@
|
||||
"displayName": "Front Matter CMS",
|
||||
"description": "Front Matter is a CMS that runs within Visual Studio Code. It gives you the power and control of a full-blown CMS while also providing you the flexibility and speed of the static site generator of your choice like: Hugo, Jekyll, Docusaurus, NextJs, Gatsby, and many more...",
|
||||
"icon": "assets/frontmatter-teal-128x128.png",
|
||||
"version": "9.5.0",
|
||||
"version": "10.0.0",
|
||||
"preview": false,
|
||||
"publisher": "eliostruyf",
|
||||
"galleryBanner": {
|
||||
@@ -347,7 +347,8 @@
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"locale"
|
||||
"locale",
|
||||
"path"
|
||||
]
|
||||
},
|
||||
"scope": "Content"
|
||||
@@ -526,13 +527,18 @@
|
||||
"frontMatter.content.filters": {
|
||||
"type": "array",
|
||||
"default": [
|
||||
"pageFolders",
|
||||
"contentFolders",
|
||||
"tags",
|
||||
"categories"
|
||||
],
|
||||
"markdownDescription": "%setting.frontMatter.content.filters.markdownDescription%",
|
||||
"items": [{
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"contentFolders",
|
||||
"tags",
|
||||
"categories"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
@@ -701,17 +707,6 @@
|
||||
"default": "",
|
||||
"markdownDescription": "%setting.frontMatter.dashboard.content.card.fields.title.markdownDescription%"
|
||||
},
|
||||
"frontMatter.dashboard.mediaSnippet": {
|
||||
"type": "array",
|
||||
"default": [],
|
||||
"markdownDescription": "%setting.frontMatter.dashboard.mediaSnippet.markdownDescription%",
|
||||
"deprecationMessage": "%setting.frontMatter.dashboard.mediaSnippet.deprecationMessage%",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"description": "%setting.frontMatter.dashboard.mediaSnippet.items.description%"
|
||||
},
|
||||
"scope": "dashboard"
|
||||
},
|
||||
"frontMatter.dashboard.openOnStart": {
|
||||
"type": [
|
||||
"boolean",
|
||||
@@ -1834,12 +1829,6 @@
|
||||
},
|
||||
"scope": "Taxonomy"
|
||||
},
|
||||
"frontMatter.taxonomy.dateField": {
|
||||
"type": "string",
|
||||
"default": "date",
|
||||
"markdownDescription": "%setting.frontMatter.taxonomy.dateField.markdownDescription%",
|
||||
"deprecationMessage": "%setting.frontMatter.taxonomy.dateField.deprecationMessage%"
|
||||
},
|
||||
"frontMatter.taxonomy.dateFormat": {
|
||||
"type": "string",
|
||||
"markdownDescription": "%setting.frontMatter.taxonomy.dateFormat.markdownDescription%",
|
||||
@@ -1893,12 +1882,6 @@
|
||||
"markdownDescription": "%setting.frontMatter.taxonomy.indentArrays.markdownDescription%",
|
||||
"scope": "Taxonomy"
|
||||
},
|
||||
"frontMatter.taxonomy.modifiedField": {
|
||||
"type": "string",
|
||||
"default": "lastmod",
|
||||
"markdownDescription": "%setting.frontMatter.taxonomy.modifiedField.markdownDescription%",
|
||||
"deprecationMessage": "%setting.frontMatter.taxonomy.modifiedField.deprecationMessage%"
|
||||
},
|
||||
"frontMatter.taxonomy.quoteStringValues": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
@@ -2371,7 +2354,7 @@
|
||||
{
|
||||
"command": "frontMatter.i18n.create",
|
||||
"group": "navigation@-127",
|
||||
"when": "frontMatter:file:isValid && frontMatter:i18n:default"
|
||||
"when": "frontMatter:file:isValid && frontMatter:i18n:enabled"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.markup.options",
|
||||
@@ -2466,7 +2449,7 @@
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.i18n.create",
|
||||
"when": "frontMatter:i18n:default"
|
||||
"when": "frontMatter:i18n:enabled"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.authenticate",
|
||||
|
||||
+17
-1
@@ -10,7 +10,8 @@ import {
|
||||
SETTING_SLUG_PREFIX,
|
||||
SETTING_SLUG_SUFFIX,
|
||||
SETTING_CONTENT_PLACEHOLDERS,
|
||||
TelemetryEvent
|
||||
TelemetryEvent,
|
||||
SETTING_SLUG_TEMPLATE
|
||||
} from './../constants';
|
||||
import * as vscode from 'vscode';
|
||||
import { CustomPlaceholder, Field } from '../models';
|
||||
@@ -260,6 +261,21 @@ export class Article {
|
||||
return;
|
||||
}
|
||||
|
||||
const slugTemplate = Settings.get<string>(SETTING_SLUG_TEMPLATE);
|
||||
if (slugTemplate) {
|
||||
if (slugTemplate === '{{title}}') {
|
||||
const article = ArticleHelper.getFrontMatter(editor);
|
||||
if (article?.data?.title) {
|
||||
return article.data.title.toLowerCase().replace(/\s/g, '-');
|
||||
}
|
||||
} else {
|
||||
const article = ArticleHelper.getFrontMatter(editor);
|
||||
if (article?.data) {
|
||||
return SlugHelper.createSlug(article.data.title, article.data, slugTemplate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const file = parseWinPath(editor.document.fileName);
|
||||
|
||||
if (!isValidFile(file)) {
|
||||
|
||||
@@ -358,7 +358,7 @@ export class Dashboard {
|
||||
version.usedVersion ? '' : `data-showWelcome="true"`
|
||||
} ${
|
||||
experimental ? `data-experimental="${experimental}"` : ''
|
||||
} data-webview-url="${webviewUrl}" ></div>
|
||||
} data-webview-url="${webviewUrl}" data-is-crash-disabled="${!Telemetry.isVscodeEnabled()}" ></div>
|
||||
|
||||
${(scriptsToLoad || [])
|
||||
.map((script) => {
|
||||
|
||||
+54
-33
@@ -99,7 +99,7 @@ export class Folders {
|
||||
}
|
||||
|
||||
const folders = Folders.get().filter((f) => !f.disableCreation);
|
||||
const location = folders.find((f) => f.title === selectedFolder);
|
||||
const location = folders.find((f) => f.path === selectedFolder.path);
|
||||
if (location) {
|
||||
const folderPath = Folders.getFolderPath(Uri.file(location.path));
|
||||
if (folderPath) {
|
||||
@@ -293,31 +293,6 @@ export class Folders {
|
||||
if (crntFolderInfo) {
|
||||
folderInfo.push(crntFolderInfo);
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
const crntFolderInfo = await Folders.getFilesByFolder(
|
||||
i18nFolder,
|
||||
supportedFiles,
|
||||
limit
|
||||
);
|
||||
if (crntFolderInfo) {
|
||||
folderInfo.push(crntFolderInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return folderInfo;
|
||||
@@ -333,11 +308,14 @@ export class Folders {
|
||||
public static get(): ContentFolder[] {
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
let folders: ContentFolder[] = Settings.get(SETTING_CONTENT_PAGE_FOLDERS) as ContentFolder[];
|
||||
const i18nSettings = Settings.get<I18nConfig[]>(SETTING_CONTENT_I18N);
|
||||
|
||||
// Filter out folders without a path
|
||||
folders = folders.filter((f) => f.path);
|
||||
|
||||
const contentFolders = folders.map((folder) => {
|
||||
const contentFolders: ContentFolder[] = [];
|
||||
|
||||
folders.forEach((folder) => {
|
||||
if (!folder.title) {
|
||||
folder.title = basename(folder.path);
|
||||
}
|
||||
@@ -371,11 +349,52 @@ export class Folders {
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...folder,
|
||||
originalPath: folder.path,
|
||||
path: folderPath
|
||||
};
|
||||
// Check i18n
|
||||
if (folder.defaultLocale && (folder.locales || i18nSettings)) {
|
||||
const i18nConfig =
|
||||
folder.locales && folder.locales.length > 0 ? folder.locales : i18nSettings;
|
||||
|
||||
let defaultLocale;
|
||||
let sourcePath = folderPath;
|
||||
let localeFolders: ContentFolder[] = [];
|
||||
|
||||
if (i18nConfig && i18nConfig.length > 0) {
|
||||
for (const i18n of i18nConfig) {
|
||||
if (i18n.locale === folder.defaultLocale) {
|
||||
defaultLocale = i18n;
|
||||
} else if (i18n.locale !== folder.defaultLocale && i18n.path) {
|
||||
localeFolders.push({
|
||||
...folder,
|
||||
title: folder.title,
|
||||
originalPath: folder.path,
|
||||
locale: i18n.locale,
|
||||
localeTitle: i18n?.title || i18n.locale,
|
||||
localeSourcePath: sourcePath,
|
||||
path: parseWinPath(join(folderPath, i18n.path))
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contentFolders.push({
|
||||
...folder,
|
||||
title: folder.title,
|
||||
locale: folder.defaultLocale,
|
||||
localeTitle: defaultLocale?.title || folder.defaultLocale,
|
||||
originalPath: folder.path,
|
||||
localeSourcePath: sourcePath,
|
||||
path: parseWinPath(join(folderPath, defaultLocale?.path || ''))
|
||||
});
|
||||
|
||||
contentFolders.push(...localeFolders);
|
||||
} else {
|
||||
contentFolders.push({
|
||||
...folder,
|
||||
locale: folder.defaultLocale,
|
||||
originalPath: folder.path,
|
||||
path: folderPath
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return contentFolders.filter((folder) => folder !== null) as ContentFolder[];
|
||||
@@ -651,7 +670,9 @@ export class Folders {
|
||||
return {
|
||||
title: folder.title,
|
||||
files: files.length,
|
||||
lastModified: fileStats
|
||||
lastModified: fileStats,
|
||||
locale: folder.locale,
|
||||
localeTitle: folder.localeTitle
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,8 +44,8 @@ export class StatusListener {
|
||||
commands.executeCommand('setContext', CONTEXT.isValidFile, true);
|
||||
|
||||
// Check i18n
|
||||
const isI18nDefault = await i18n.isDefaultLanguage(document.uri.fsPath);
|
||||
commands.executeCommand('setContext', CONTEXT.isI18nDefault, isI18nDefault);
|
||||
const isI18nEnabled = await i18n.isLocaleEnabled(document.uri.fsPath);
|
||||
commands.executeCommand('setContext', CONTEXT.isI18nEnabled, isI18nEnabled);
|
||||
|
||||
const article = editor
|
||||
? ArticleHelper.getFrontMatter(editor)
|
||||
@@ -88,7 +88,7 @@ export class StatusListener {
|
||||
}
|
||||
} else {
|
||||
commands.executeCommand('setContext', CONTEXT.isValidFile, false);
|
||||
commands.executeCommand('setContext', CONTEXT.isI18nDefault, false);
|
||||
commands.executeCommand('setContext', CONTEXT.isI18nEnabled, false);
|
||||
|
||||
const panel = PanelProvider.getInstance();
|
||||
if (panel && panel.visible) {
|
||||
|
||||
+103
-56
@@ -4,13 +4,12 @@ import {
|
||||
ContentType,
|
||||
Extension,
|
||||
FrameworkDetector,
|
||||
Logger,
|
||||
Notifications,
|
||||
Settings,
|
||||
openFileInEditor,
|
||||
parseWinPath
|
||||
} from '../helpers';
|
||||
import { COMMAND_NAME, ExtensionState, SETTING_CONTENT_I18N } from '../constants';
|
||||
import { COMMAND_NAME, SETTING_CONTENT_I18N } from '../constants';
|
||||
import { ContentFolder, Field, I18nConfig, ContentType as IContentType } from '../models';
|
||||
import { join, parse } from 'path';
|
||||
import { existsAsync } from '../utils';
|
||||
@@ -19,6 +18,7 @@ import { ParsedFrontMatter } from '../parsers';
|
||||
import { PagesListener } from '../listeners/dashboard';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
import { LocalizationKey } from '../localization';
|
||||
import { Translations } from '../services/Translations';
|
||||
|
||||
export class i18n {
|
||||
private static processedFiles: {
|
||||
@@ -43,6 +43,30 @@ export class i18n {
|
||||
i18n.processedFiles = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all the I18nConfig settings.
|
||||
*
|
||||
* @returns An array of I18nConfig settings.
|
||||
*/
|
||||
public static getAll() {
|
||||
const i18nSettings = Settings.get<I18nConfig[]>(SETTING_CONTENT_I18N) || [];
|
||||
|
||||
const folders = Folders.get();
|
||||
if (folders) {
|
||||
for (const folder of folders) {
|
||||
if (folder.locales) {
|
||||
for (const locale of folder.locales) {
|
||||
if (!i18nSettings.some((i18n) => i18n.locale === locale.locale)) {
|
||||
i18nSettings.push(locale);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return i18nSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the I18nConfig settings from the application.
|
||||
* @returns An array of I18nConfig objects if settings are found, otherwise undefined.
|
||||
@@ -65,6 +89,25 @@ export class i18n {
|
||||
return pageFolder.locales;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the locale is enabled for the given file path.
|
||||
* @param filePath - The file path to check.
|
||||
* @returns A promise that resolves to a boolean indicating whether the locale is enabled or not.
|
||||
*/
|
||||
public static async isLocaleEnabled(filePath: string): Promise<boolean> {
|
||||
const i18nSettings = await i18n.getSettings(filePath);
|
||||
if (!i18nSettings) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const pageFolder = Folders.getPageFolderByFilePath(filePath);
|
||||
if (!pageFolder || !pageFolder.locale) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return i18nSettings.some((i18n) => i18n.locale === pageFolder.locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given file path corresponds to the default language.
|
||||
* @param filePath - The file path to check.
|
||||
@@ -84,6 +127,10 @@ export class i18n {
|
||||
const fileInfo = await i18n.getFileInfo(filePath);
|
||||
|
||||
if (pageFolder.path) {
|
||||
if (pageFolder.locale) {
|
||||
return pageFolder.locale === pageFolder.defaultLocale;
|
||||
}
|
||||
|
||||
let pageFolderPath = parseWinPath(pageFolder.path);
|
||||
if (!pageFolderPath.endsWith('/')) {
|
||||
pageFolderPath += '/';
|
||||
@@ -120,9 +167,10 @@ export class i18n {
|
||||
|
||||
if (
|
||||
pageFolder.path &&
|
||||
pageFolder.locale &&
|
||||
parseWinPath(fileInfo.dir).toLowerCase() === parseWinPath(pageFolderPath).toLowerCase()
|
||||
) {
|
||||
return i18nSettings.find((i18n) => i18n.locale === pageFolder?.defaultLocale);
|
||||
return i18nSettings.find((i18n) => i18n.locale === pageFolder?.locale);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,9 +220,9 @@ export class i18n {
|
||||
let pageFolder = Folders.getPageFolderByFilePath(filePath);
|
||||
const fileInfo = await i18n.getFileInfo(filePath);
|
||||
|
||||
if (pageFolder && pageFolder.defaultLocale) {
|
||||
if (pageFolder && pageFolder.defaultLocale && pageFolder.localeSourcePath) {
|
||||
for (const i18n of i18nSettings) {
|
||||
const translation = join(pageFolder.path, i18n.path || '', fileInfo.filename);
|
||||
const translation = join(pageFolder.localeSourcePath, i18n.path || '', fileInfo.filename);
|
||||
if (await existsAsync(translation)) {
|
||||
translations[i18n.locale] = {
|
||||
locale: i18n,
|
||||
@@ -224,20 +272,40 @@ export class i18n {
|
||||
fileUri = Uri.file(fileUri);
|
||||
}
|
||||
|
||||
const pageFolder = Folders.getPageFolderByFilePath(fileUri.fsPath);
|
||||
if (!pageFolder || !pageFolder.localeSourcePath) {
|
||||
Notifications.error(l10n.t(LocalizationKey.commandsI18nCreateErrorNoContentFolder));
|
||||
return;
|
||||
}
|
||||
|
||||
const i18nSettings = await i18n.getSettings(fileUri.fsPath);
|
||||
if (!i18nSettings) {
|
||||
Notifications.warning(l10n.t(LocalizationKey.commandsI18nCreateWarningNoConfig));
|
||||
return;
|
||||
}
|
||||
|
||||
const isDefaultLanguage = await i18n.isDefaultLanguage(fileUri.fsPath);
|
||||
if (!isDefaultLanguage) {
|
||||
Notifications.warning(l10n.t(LocalizationKey.commandsI18nCreateWarningNotDefaultLocale));
|
||||
const sourceLocale = await i18n.getLocale(fileUri.fsPath);
|
||||
if (!sourceLocale || !sourceLocale.locale) {
|
||||
Notifications.warning(l10n.t(LocalizationKey.commandsI18nCreateErrorNoLocaleDefinition));
|
||||
return;
|
||||
}
|
||||
|
||||
const translations = (await i18n.getTranslations(fileUri.fsPath)) || {};
|
||||
const targetLocales = i18nSettings.filter((i18nSetting) => {
|
||||
return (
|
||||
i18nSetting.path &&
|
||||
i18nSetting.locale !== sourceLocale.locale &&
|
||||
!translations[i18nSetting.locale]
|
||||
);
|
||||
});
|
||||
|
||||
if (targetLocales.length === 0) {
|
||||
Notifications.warning(l10n.t(LocalizationKey.commandsI18nCreateErrorNoLocales));
|
||||
return;
|
||||
}
|
||||
|
||||
const locale = await window.showQuickPick(
|
||||
i18nSettings.filter((i18n) => i18n.path).map((i18n) => i18n.title || i18n.locale),
|
||||
targetLocales.map((i18n) => i18n.title || i18n.locale),
|
||||
{
|
||||
title: l10n.t(LocalizationKey.commandsI18nCreateQuickPickTitle),
|
||||
placeHolder: l10n.t(LocalizationKey.commandsI18nCreateQuickPickPlaceHolder),
|
||||
@@ -249,10 +317,10 @@ export class i18n {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedI18n = i18nSettings.find(
|
||||
const targetLocale = i18nSettings.find(
|
||||
(i18n) => i18n.title === locale || i18n.locale === locale
|
||||
);
|
||||
if (!selectedI18n || !selectedI18n.path) {
|
||||
if (!targetLocale || !targetLocale.path) {
|
||||
Notifications.warning(l10n.t(LocalizationKey.commandsI18nCreateWarningNoConfig));
|
||||
return;
|
||||
}
|
||||
@@ -280,7 +348,7 @@ export class i18n {
|
||||
pageBundleDir = join(parse(pageBundleDir).dir);
|
||||
}
|
||||
|
||||
const i18nDir = join(dir, selectedI18n.path, pageBundleDir);
|
||||
const i18nDir = join(pageFolder.localeSourcePath, targetLocale.path, pageBundleDir);
|
||||
|
||||
if (!(await existsAsync(i18nDir))) {
|
||||
await workspace.fs.createDirectory(Uri.file(i18nDir));
|
||||
@@ -290,7 +358,8 @@ export class i18n {
|
||||
article,
|
||||
fileUri.fsPath,
|
||||
contentType,
|
||||
selectedI18n,
|
||||
sourceLocale,
|
||||
targetLocale,
|
||||
i18nDir
|
||||
);
|
||||
|
||||
@@ -300,9 +369,8 @@ export class i18n {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceLocale = await i18n.getLocale(fileUri.fsPath);
|
||||
if (sourceLocale?.locale) {
|
||||
article = await i18n.translate(article, sourceLocale, selectedI18n);
|
||||
article = await i18n.translate(article, sourceLocale, targetLocale);
|
||||
}
|
||||
|
||||
const newFileUri = Uri.file(newFilePath);
|
||||
@@ -318,7 +386,7 @@ export class i18n {
|
||||
Notifications.info(
|
||||
l10n.t(
|
||||
LocalizationKey.commandsI18nCreateSuccessCreated,
|
||||
selectedI18n.title || selectedI18n.locale
|
||||
sourceLocale.title || sourceLocale.locale
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -336,12 +404,6 @@ export class i18n {
|
||||
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,
|
||||
@@ -349,43 +411,26 @@ export class i18n {
|
||||
cancellable: false
|
||||
},
|
||||
async () => {
|
||||
const title = article.data.title;
|
||||
const description = article.data.description;
|
||||
const content = article.content;
|
||||
|
||||
try {
|
||||
const body = JSON.stringify({
|
||||
text: [title, description, content],
|
||||
source_lang: sourceLocale.locale,
|
||||
target_lang: targetLocale.locale
|
||||
});
|
||||
const title = article.data.title || '';
|
||||
const description = article.data.description || '';
|
||||
const content = article.content || '';
|
||||
|
||||
let host = authKey.endsWith(':fx') ? 'api-free.deepl.com' : 'api.deepl.com';
|
||||
const text = [title, description, content];
|
||||
const translations = await Translations.translate(
|
||||
text,
|
||||
sourceLocale.locale,
|
||||
targetLocale.locale
|
||||
);
|
||||
|
||||
const response = await fetch(`https://${host}/v2/translate`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `DeepL-Auth-Key ${authKey}`,
|
||||
'User-Agent': `FrontMatterCMS/${Extension.getInstance().version}`,
|
||||
'Content-Type': 'application/json',
|
||||
'content-length': body.length.toString(),
|
||||
Accept: 'application/json'
|
||||
},
|
||||
body
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`DeepL: ${response.statusText}`);
|
||||
if (!translations || translations.length < 3) {
|
||||
resolve(article);
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
article.data.title = article.data.title ? translations[0] : '';
|
||||
article.data.description = article.data.description ? translations[1] : '';
|
||||
article.content = article.content ? translations[2] : '';
|
||||
} catch (error) {
|
||||
Notifications.error(`${(error as Error).message}`);
|
||||
}
|
||||
@@ -460,7 +505,8 @@ export class i18n {
|
||||
* @param article - The parsed front matter of the article.
|
||||
* @param filePath - The path of the file containing the front matter.
|
||||
* @param contentType - The content type of the article.
|
||||
* @param i18nConfig - The configuration for internationalization.
|
||||
* @param sourceLocale - The source locale.
|
||||
* @param targetLocale - The target locale.
|
||||
* @param i18nDir - The directory where the i18n files are located.
|
||||
* @returns A Promise that resolves to the updated parsed front matter.
|
||||
*/
|
||||
@@ -468,7 +514,8 @@ export class i18n {
|
||||
article: ParsedFrontMatter,
|
||||
filePath: string,
|
||||
contentType: IContentType,
|
||||
i18nConfig: I18nConfig,
|
||||
sourceLocale: I18nConfig,
|
||||
targetLocale: I18nConfig,
|
||||
i18nDir: string
|
||||
): Promise<ParsedFrontMatter> {
|
||||
const imageFields = ContentType.findFieldsByTypeDeep(contentType.fields, 'image');
|
||||
|
||||
@@ -33,6 +33,12 @@ export const ExtensionState = {
|
||||
},
|
||||
|
||||
Secrets: {
|
||||
DeeplApiKey: `frontMatter:Secrets:DeeplApiKey`
|
||||
Deepl: {
|
||||
ApiKey: `frontMatter:Secrets:DeeplApiKey`
|
||||
},
|
||||
Azure: {
|
||||
TranslatorKey: `frontMatter:Secrets:AzureTranslatorKey`,
|
||||
TranslatorRegion: `frontMatter:Secrets:AzureTranslatorRegion`
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -21,6 +21,9 @@ export const GeneralCommands = {
|
||||
get: 'getSecret',
|
||||
set: 'setSecret'
|
||||
},
|
||||
content: {
|
||||
locales: 'getContentLocales'
|
||||
},
|
||||
runCommand: 'runCommand',
|
||||
getLocalization: 'getLocalization',
|
||||
openOnWebsite: 'openOnWebsite'
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export const SentryIgnore = [
|
||||
`ResizeObserver loop limit exceeded`,
|
||||
`Cannot read properties of undefined (reading 'unobserve')`,
|
||||
`TypeError: Cannot read properties of undefined (reading 'unobserve')`
|
||||
`TypeError: Cannot read properties of undefined (reading 'unobserve')`,
|
||||
`ResizeObserver loop completed with undelivered notifications.`
|
||||
];
|
||||
|
||||
@@ -8,7 +8,7 @@ export const CONTEXT = {
|
||||
isValidFile: 'frontMatter:file:isValid',
|
||||
isDevelopment: 'frontMatter:isDevelopment',
|
||||
|
||||
isI18nDefault: 'frontMatter:i18n:default',
|
||||
isI18nEnabled: 'frontMatter:i18n:enabled',
|
||||
|
||||
hasViewModes: 'frontMatter:has:modes',
|
||||
|
||||
|
||||
@@ -133,13 +133,3 @@ export const SETTING_CONTENT_FOLDERS = 'content.folders';
|
||||
* Use the `isPublishDate` property on the content type datetime field instead
|
||||
*/
|
||||
export const SETTING_DATE_FIELD = 'taxonomy.dateField';
|
||||
/**
|
||||
* @deprecated
|
||||
* Use the `isModifiedDate` property on the content type datetime field instead
|
||||
*/
|
||||
export const SETTING_MODIFIED_FIELD = 'taxonomy.modifiedField';
|
||||
/**
|
||||
* @deprecated
|
||||
* Use the `frontMatter.content.snippets` setting instead
|
||||
*/
|
||||
export const SETTING_DASHBOARD_MEDIA_SNIPPET = 'dashboard.mediaSnippet';
|
||||
|
||||
@@ -30,7 +30,7 @@ export interface IAppProps {
|
||||
export const App: React.FunctionComponent<IAppProps> = ({
|
||||
showWelcome
|
||||
}: React.PropsWithChildren<IAppProps>) => {
|
||||
const { pages, settings, localeReady } = useMessages();
|
||||
const { pages, settings } = useMessages();
|
||||
const view = useRecoilValue(DashboardViewSelector);
|
||||
const mode = useRecoilValue(ModeAtom);
|
||||
const [isDevMode, setIsDevMode] = useState(false);
|
||||
@@ -70,7 +70,7 @@ export const App: React.FunctionComponent<IAppProps> = ({
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!settings || !localeReady) {
|
||||
if (!settings) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
|
||||
@@ -230,7 +230,7 @@ export const ContentActions: React.FunctionComponent<IContentActionsProps> = ({
|
||||
}
|
||||
|
||||
{
|
||||
locale && isDefaultLocale && (
|
||||
locale && (
|
||||
<DropdownMenuItem onClick={() => runCommand(COMMAND_NAME.i18n.create)}>
|
||||
<LanguageIcon className={`mr-2 h-4 w-4`} aria-hidden={true} />
|
||||
<span>{l10n.t(LocalizationKey.dashboardContentsContentActionsTranslationsCreate)}</span>
|
||||
|
||||
@@ -18,7 +18,7 @@ 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 otherFilters = useMemo(() => settings?.filters?.filter((filter) => filter !== "contentFolders" && filter !== "tags" && filter !== "categories"), [settings?.filters]);
|
||||
|
||||
const otherFilterValues = useMemo(() => {
|
||||
return otherFilters?.map((filter) => {
|
||||
@@ -77,7 +77,7 @@ export const Filters: React.FunctionComponent<IFiltersProps> = (_: React.PropsWi
|
||||
<LanguageFilter />
|
||||
|
||||
{
|
||||
settings?.filters?.includes("pageFolders") && (
|
||||
settings?.filters?.includes("contentFolders") && (
|
||||
<FoldersFilter />
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { FolderAtom, SettingsSelector } from '../../state';
|
||||
import { MenuButton, MenuItem } from '../Menu';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
import { LocalizationKey } from '../../../localization';
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '../../../components/shadcn/Dropdown';
|
||||
import { DropdownMenu, DropdownMenuContent } from '../../../components/shadcn/Dropdown';
|
||||
|
||||
export interface IFoldersFilterProps { }
|
||||
|
||||
@@ -14,7 +14,11 @@ export const FoldersFilter: React.FunctionComponent<
|
||||
const DEFAULT_TYPE = l10n.t(LocalizationKey.dashboardHeaderFoldersDefault);
|
||||
const [crntFolder, setCrntFolder] = useRecoilState(FolderAtom);
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
const contentFolders = settings?.contentFolders || [];
|
||||
|
||||
const contentFolders = React.useMemo(() => {
|
||||
return settings?.contentFolders
|
||||
.filter((folder, index, self) => index === self.findIndex((t) => t.originalPath === folder.originalPath)) || [];
|
||||
}, [settings?.contentFolders]);
|
||||
|
||||
if (contentFolders.length <= 1) {
|
||||
return null;
|
||||
|
||||
@@ -128,7 +128,7 @@ export const DetailsSlideOver: React.FunctionComponent<IDetailsSlideOverProps> =
|
||||
</div>
|
||||
|
||||
<div className="relative mt-6 flex-1 px-4 sm:px-6">
|
||||
<div className="absolute inset-0 px-4 sm:px-6 space-y-8">
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
{(isImageFile || isVideoFile) && (
|
||||
<div className={`block w-full aspect-w-10 aspect-h-7 overflow-hidden border rounded border-[var(--frontmatter-border)] bg-[var(--vscode-editor-background)]`}>
|
||||
|
||||
@@ -10,26 +10,64 @@ export interface IIntegrationsViewProps { }
|
||||
|
||||
export const IntegrationsView: React.FunctionComponent<IIntegrationsViewProps> = ({ }: React.PropsWithChildren<IIntegrationsViewProps>) => {
|
||||
const [deeplApiKey, setDeeplApiKey] = React.useState<string>('');
|
||||
const [azureApiKey, setAzureApiKey] = React.useState<string>('');
|
||||
const [azureRegion, setAzureRegion] = React.useState<string>('');
|
||||
const [crntDeeplApiKey, setCrntDeeplApiKey] = React.useState<string>('');
|
||||
const [crntAzureApiKey, setCrntAzureApiKey] = React.useState<string>('');
|
||||
const [crntAzureRegion, setCrntAzureRegion] = 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]);
|
||||
if (crntDeeplApiKey !== deeplApiKey) {
|
||||
messageHandler.request<string>(GeneralCommands.toVSCode.secrets.set, {
|
||||
key: ExtensionState.Secrets.Deepl.ApiKey,
|
||||
value: crntDeeplApiKey
|
||||
}).then((apiKey: string) => {
|
||||
setDeeplApiKey(apiKey);
|
||||
});
|
||||
}
|
||||
|
||||
const onChange = (_: string, value: string) => {
|
||||
setCrntDeeplApiKey(value);
|
||||
if (crntAzureApiKey !== azureApiKey) {
|
||||
messageHandler.request<string>(GeneralCommands.toVSCode.secrets.set, {
|
||||
key: ExtensionState.Secrets.Azure.TranslatorKey,
|
||||
value: crntAzureApiKey
|
||||
}).then((apiKey: string) => {
|
||||
setAzureApiKey(apiKey);
|
||||
});
|
||||
}
|
||||
|
||||
if (crntAzureRegion !== azureRegion) {
|
||||
messageHandler.request<string>(GeneralCommands.toVSCode.secrets.set, {
|
||||
key: ExtensionState.Secrets.Azure.TranslatorRegion,
|
||||
value: crntAzureRegion
|
||||
}).then((apiKey: string) => {
|
||||
setAzureRegion(apiKey);
|
||||
});
|
||||
}
|
||||
}, [crntDeeplApiKey, deeplApiKey, crntAzureApiKey, azureApiKey, crntAzureRegion, azureRegion]);
|
||||
|
||||
const onChange = (key: string, value: string) => {
|
||||
if (key === ExtensionState.Secrets.Deepl.ApiKey) {
|
||||
setCrntDeeplApiKey(value);
|
||||
} else if (key === ExtensionState.Secrets.Azure.TranslatorKey) {
|
||||
setCrntAzureApiKey(value);
|
||||
} else if (key === ExtensionState.Secrets.Azure.TranslatorRegion) {
|
||||
setCrntAzureRegion(value);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
messageHandler.request<string>(GeneralCommands.toVSCode.secrets.get, ExtensionState.Secrets.DeeplApiKey).then((apiKey: string) => {
|
||||
messageHandler.request<string>(GeneralCommands.toVSCode.secrets.get, ExtensionState.Secrets.Deepl.ApiKey).then((apiKey: string) => {
|
||||
setDeeplApiKey(apiKey);
|
||||
setCrntDeeplApiKey(apiKey);
|
||||
});
|
||||
messageHandler.request<string>(GeneralCommands.toVSCode.secrets.get, ExtensionState.Secrets.Azure.TranslatorKey).then((apiKey: string) => {
|
||||
setAzureApiKey(apiKey);
|
||||
setCrntAzureApiKey(apiKey);
|
||||
});
|
||||
messageHandler.request<string>(GeneralCommands.toVSCode.secrets.get, ExtensionState.Secrets.Azure.TranslatorRegion).then((apiKey: string) => {
|
||||
setAzureRegion(apiKey);
|
||||
setCrntAzureRegion(apiKey);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
@@ -39,16 +77,37 @@ export const IntegrationsView: React.FunctionComponent<IIntegrationsViewProps> =
|
||||
|
||||
<SettingsInput
|
||||
label={l10n.t(LocalizationKey.settingsIntegrationsViewDeeplIntputLabel)}
|
||||
name={ExtensionState.Secrets.DeeplApiKey}
|
||||
name={ExtensionState.Secrets.Deepl.ApiKey}
|
||||
value={crntDeeplApiKey || ""}
|
||||
placeholder={l10n.t(LocalizationKey.settingsIntegrationsViewDeeplIntputPlaceholder)}
|
||||
onChange={onChange}
|
||||
/>
|
||||
|
||||
<h2 className='text-xl mb-2'>{l10n.t(LocalizationKey.settingsIntegrationsViewAzureTitle)}</h2>
|
||||
|
||||
<SettingsInput
|
||||
label={l10n.t(LocalizationKey.settingsIntegrationsViewAzureIntputLabel)}
|
||||
name={ExtensionState.Secrets.Azure.TranslatorKey}
|
||||
value={crntAzureApiKey || ""}
|
||||
placeholder={l10n.t(LocalizationKey.settingsIntegrationsViewAzureIntputPlaceholder)}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<SettingsInput
|
||||
label={l10n.t(LocalizationKey.settingsIntegrationsViewAzureRegionLabel)}
|
||||
name={ExtensionState.Secrets.Azure.TranslatorRegion}
|
||||
value={crntAzureRegion || ""}
|
||||
placeholder={l10n.t(LocalizationKey.settingsIntegrationsViewAzureRegionPlaceholder)}
|
||||
onChange={onChange}
|
||||
/>
|
||||
|
||||
<div className={`mt-4 flex gap-2`}>
|
||||
<VSCodeButton
|
||||
onClick={onSave}
|
||||
disabled={deeplApiKey === crntDeeplApiKey}>
|
||||
disabled={
|
||||
deeplApiKey === crntDeeplApiKey &&
|
||||
azureApiKey === crntAzureApiKey &&
|
||||
azureRegion === crntAzureRegion
|
||||
}>
|
||||
{l10n.t(LocalizationKey.commonSave)}
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,6 @@ import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { EventData } from '@estruyf/vscode/dist/models';
|
||||
import { NavigationType } from '../models';
|
||||
import { GeneralCommands } from '../../constants';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
|
||||
export default function useMessages() {
|
||||
const [loading, setLoading] = useRecoilState(LoadingAtom);
|
||||
@@ -25,7 +24,6 @@ export default function useMessages() {
|
||||
const [, setMode] = useRecoilState(ModeAtom);
|
||||
const [, setView] = useRecoilState(DashboardViewAtom);
|
||||
const [, setSearchReady] = useRecoilState(SearchReadyAtom);
|
||||
const [localeReady, setLocaleReady] = useState<boolean>(false);
|
||||
|
||||
const messageListener = (event: MessageEvent<EventData<any>>) => {
|
||||
const message = event.data;
|
||||
@@ -61,12 +59,6 @@ export default function useMessages() {
|
||||
case GeneralCommands.toWebview.setMode:
|
||||
setMode(message.payload);
|
||||
break;
|
||||
case GeneralCommands.toWebview.setLocalization:
|
||||
l10n.config({
|
||||
contents: message.payload
|
||||
});
|
||||
setLocaleReady(true);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -78,7 +70,6 @@ export default function useMessages() {
|
||||
Messenger.send(DashboardMessage.getTheme);
|
||||
Messenger.send(DashboardMessage.getData);
|
||||
Messenger.send(DashboardMessage.getMode);
|
||||
Messenger.send(GeneralCommands.toVSCode.getLocalization);
|
||||
|
||||
return () => {
|
||||
Messenger.unlisten(messageListener);
|
||||
@@ -89,7 +80,6 @@ export default function useMessages() {
|
||||
loading,
|
||||
pages,
|
||||
viewData,
|
||||
settings,
|
||||
localeReady
|
||||
settings
|
||||
};
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import { DashboardMessage } from '../DashboardMessage';
|
||||
import { EventData } from '@estruyf/vscode/dist/models';
|
||||
import { parseWinPath } from '../../helpers/parseWinPath';
|
||||
import { sortPages } from '../../utils/sortPages';
|
||||
import { ExtensionState } from '../../constants';
|
||||
import { ExtensionState, GeneralCommands } from '../../constants';
|
||||
import { SortingOption } from '../models';
|
||||
import { I18nConfig } from '../../models';
|
||||
import { usePrevious } from '../../panelWebView/hooks/usePrevious';
|
||||
@@ -204,7 +204,7 @@ export default function usePages(pages: Page[]) {
|
||||
setTabInfo(draftTypes);
|
||||
|
||||
if (Object.keys(filters).length === 0) {
|
||||
const availableFilters = (settings?.filters || []).filter((f) => f !== 'pageFolders' && f !== 'tags' && f !== 'categories');
|
||||
const availableFilters = (settings?.filters || []).filter((f) => f !== 'contentFolders' && f !== 'tags' && f !== 'categories');
|
||||
if (availableFilters.length > 0) {
|
||||
const allFilters: { [filter: string]: string[]; } = {};
|
||||
for (const filter of availableFilters) {
|
||||
@@ -220,16 +220,6 @@ export default function usePages(pages: Page[]) {
|
||||
}
|
||||
}
|
||||
|
||||
if (tabPrevious !== tab || !locales || locales.length === 0) {
|
||||
// Store the locale information
|
||||
const config: I18nConfig[] = [];
|
||||
crntPages.forEach((page) => {
|
||||
if (page.fmLocale && !config.some(locale => locale.locale === page.fmLocale?.locale)) {
|
||||
config.push(page.fmLocale);
|
||||
}
|
||||
});
|
||||
setLocales(config);
|
||||
}
|
||||
|
||||
// Set the pages
|
||||
setPageItems(crntPages);
|
||||
@@ -276,6 +266,12 @@ export default function usePages(pages: Page[]) {
|
||||
} else {
|
||||
startPageProcessing();
|
||||
}
|
||||
|
||||
if (pages && pages.length > 0) {
|
||||
messageHandler.request<I18nConfig[]>(GeneralCommands.toVSCode.content.locales).then((config) => {
|
||||
setLocales(config || []);
|
||||
});
|
||||
}
|
||||
}, [settings?.draftField, pages, sorting, search, tag, category, locale, filters, folder]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -3,8 +3,6 @@ import { render } from 'react-dom';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
import { App } from './components/App';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Integrations } from '@sentry/tracing';
|
||||
import { SENTRY_LINK, SentryIgnore } from '../constants';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import './styles.css';
|
||||
import { Preview } from './components/Preview';
|
||||
@@ -12,6 +10,8 @@ import { SettingsProvider } from './providers/SettingsProvider';
|
||||
import { CustomPanelViewResult } from '../models';
|
||||
import { Chatbot } from './components/Chatbot/Chatbot';
|
||||
import { updateCssVariables } from './utils';
|
||||
import { I10nProvider } from './providers/I10nProvider';
|
||||
import { SentryInit } from '../utils/sentryInit';
|
||||
|
||||
declare const acquireVsCodeApi: <T = unknown>() => {
|
||||
getState: () => T;
|
||||
@@ -50,7 +50,7 @@ export const routePaths: { [name: string]: string } = {
|
||||
settings: '/settings',
|
||||
};
|
||||
|
||||
const mutationObserver = new MutationObserver((mutationsList, observer) => {
|
||||
const mutationObserver = new MutationObserver((_, __) => {
|
||||
updateCssVariables();
|
||||
});
|
||||
|
||||
@@ -64,19 +64,13 @@ if (elm) {
|
||||
const url = elm?.getAttribute('data-url');
|
||||
const experimental = elm?.getAttribute('data-experimental');
|
||||
const webviewUrl = elm?.getAttribute('data-webview-url');
|
||||
const isCrashDisabled = elm?.getAttribute('data-is-crash-disabled');
|
||||
|
||||
updateCssVariables();
|
||||
mutationObserver.observe(document.body, { childList: false, attributes: true });
|
||||
|
||||
if (isProd === 'true') {
|
||||
Sentry.init({
|
||||
dsn: SENTRY_LINK,
|
||||
integrations: [new Integrations.BrowserTracing()],
|
||||
tracesSampleRate: 0, // No performance tracing required
|
||||
release: version || '',
|
||||
environment: environment || '',
|
||||
ignoreErrors: SentryIgnore
|
||||
});
|
||||
if (isProd === 'true' && isCrashDisabled === 'false') {
|
||||
Sentry.init(SentryInit(version, environment));
|
||||
|
||||
Sentry.setTag("type", "dashboard");
|
||||
if (document.body.getAttribute(`data-vscode-theme-id`)) {
|
||||
@@ -88,17 +82,21 @@ if (elm) {
|
||||
|
||||
if (type === 'preview') {
|
||||
render(
|
||||
<SettingsProvider experimental={experimental === 'true'} version={version || ""}>
|
||||
<Preview url={url} />
|
||||
</SettingsProvider>, elm);
|
||||
<I10nProvider>
|
||||
<SettingsProvider experimental={experimental === 'true'} version={version || ""}>
|
||||
<Preview url={url} />
|
||||
</SettingsProvider>
|
||||
</I10nProvider>, elm);
|
||||
} else if (type === 'chatbot') {
|
||||
render(
|
||||
<SettingsProvider
|
||||
aiUrl='https://frontmatter.codes'
|
||||
experimental={experimental === 'true'}
|
||||
version={version || ""}>
|
||||
<Chatbot />
|
||||
</SettingsProvider>, elm);
|
||||
<I10nProvider>
|
||||
<SettingsProvider
|
||||
aiUrl='https://frontmatter.codes'
|
||||
experimental={experimental === 'true'}
|
||||
version={version || ""}>
|
||||
<Chatbot />
|
||||
</SettingsProvider>
|
||||
</I10nProvider>, elm);
|
||||
} else {
|
||||
render(
|
||||
<RecoilRoot>
|
||||
@@ -106,9 +104,11 @@ if (elm) {
|
||||
initialEntries={Object.keys(routePaths).map((key: string) => routePaths[key]) as string[]}
|
||||
initialIndex={1}
|
||||
>
|
||||
<SettingsProvider experimental={experimental === 'true'} version={version || ""} webviewUrl={webviewUrl || ""}>
|
||||
<App showWelcome={!!welcome} />
|
||||
</SettingsProvider>
|
||||
<I10nProvider>
|
||||
<SettingsProvider experimental={experimental === 'true'} version={version || ""} webviewUrl={webviewUrl || ""}>
|
||||
<App showWelcome={!!welcome} />
|
||||
</SettingsProvider>
|
||||
</I10nProvider>
|
||||
</MemoryRouter>
|
||||
</RecoilRoot>,
|
||||
elm
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
CustomScript,
|
||||
CustomTaxonomy,
|
||||
DraftField,
|
||||
FilterType,
|
||||
Framework,
|
||||
GitSettings,
|
||||
MediaContentType,
|
||||
@@ -38,7 +39,7 @@ export interface Settings {
|
||||
framework: Framework | null | undefined;
|
||||
draftField: DraftField | null | undefined;
|
||||
customSorting: SortingSetting[] | undefined;
|
||||
filters: (string | { title: string; name: string })[] | undefined;
|
||||
filters: (FilterType | { title: string; name: string })[] | undefined;
|
||||
dashboardState: DashboardState;
|
||||
scripts: CustomScript[];
|
||||
dataFiles: DataFile[] | undefined;
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import * as React from 'react';
|
||||
import { messageHandler } from '@estruyf/vscode/dist/client';
|
||||
import { GeneralCommands } from '../../constants';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
|
||||
interface I10nProviderProps { }
|
||||
|
||||
const I10nContext = React.createContext<I10nProviderProps | undefined>(undefined);
|
||||
|
||||
const I10nProvider: React.FunctionComponent<I10nProviderProps> = ({ children }: React.PropsWithChildren<I10nProviderProps>) => {
|
||||
const [localeReady, setLocaleReady] = React.useState<boolean>(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
messageHandler.request<any>(GeneralCommands.toVSCode.getLocalization).then((contents) => {
|
||||
if (contents) {
|
||||
l10n.config({
|
||||
contents
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
setLocaleReady(true);
|
||||
}, 0);
|
||||
}
|
||||
}).catch(() => {
|
||||
setLocaleReady(false);
|
||||
throw new Error('Error getting localization');
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<I10nContext.Provider value={{}}>
|
||||
{
|
||||
localeReady && children
|
||||
}
|
||||
</I10nContext.Provider>
|
||||
)
|
||||
};
|
||||
|
||||
const useI10nContext = (): I10nProviderProps => {
|
||||
const loadFunc = React.useContext(I10nContext);
|
||||
|
||||
if (loadFunc === undefined) {
|
||||
throw new Error('useI10nContext must be used within the I10nProvider');
|
||||
}
|
||||
|
||||
return loadFunc;
|
||||
};
|
||||
|
||||
I10nContext.displayName = 'I10nContext';
|
||||
I10nProvider.displayName = 'I10nProvider';
|
||||
|
||||
export { I10nProvider, useI10nContext };
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
SETTING_SITE_BASEURL,
|
||||
SETTING_TAXONOMY_CONTENT_TYPES,
|
||||
SETTING_TEMPLATES_PREFIX,
|
||||
SETTING_MODIFIED_FIELD,
|
||||
DefaultFieldValues
|
||||
} from '../constants';
|
||||
import { DumpOptions } from 'js-yaml';
|
||||
@@ -387,11 +386,7 @@ export class ArticleHelper {
|
||||
const articleCt = ArticleHelper.getContentType(article);
|
||||
const modDateField = articleCt.fields.find((f) => f.isModifiedDate);
|
||||
|
||||
return (
|
||||
modDateField?.name ||
|
||||
(Settings.get(SETTING_MODIFIED_FIELD) as string) ||
|
||||
DefaultFields.LastModified
|
||||
);
|
||||
return modDateField?.name || DefaultFields.LastModified;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -95,7 +95,7 @@ export class ContentType {
|
||||
|
||||
const contentTypes = ContentType.getAll();
|
||||
const folders = Folders.get().filter((f) => !f.disableCreation);
|
||||
const folder = folders.find((f) => f.title === selectedFolder);
|
||||
const folder = folders.find((f) => f.path === selectedFolder.path);
|
||||
|
||||
if (!folder) {
|
||||
return;
|
||||
@@ -937,6 +937,13 @@ export class ContentType {
|
||||
contentType
|
||||
);
|
||||
|
||||
let isTypeSet = false;
|
||||
if (data.type) {
|
||||
isTypeSet = true;
|
||||
} else {
|
||||
data.type = contentType.name;
|
||||
}
|
||||
|
||||
const article: ParsedFrontMatter = {
|
||||
content: '',
|
||||
data: Object.assign({}, data),
|
||||
@@ -945,6 +952,10 @@ export class ContentType {
|
||||
|
||||
data = ArticleHelper.updateDates(article);
|
||||
|
||||
if (isTypeSet) {
|
||||
delete data.type;
|
||||
}
|
||||
|
||||
if (contentType.name !== DEFAULT_CONTENT_TYPE_NAME) {
|
||||
data['type'] = contentType.name;
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
CustomScript,
|
||||
DEFAULT_MEDIA_CONTENT_TYPE,
|
||||
DraftField,
|
||||
FilterType,
|
||||
MediaContentType,
|
||||
Snippets,
|
||||
SortingSetting,
|
||||
@@ -111,7 +112,8 @@ export class DashboardSettings {
|
||||
draftField: Settings.get<DraftField>(SETTING_CONTENT_DRAFT_FIELD),
|
||||
customSorting: Settings.get<SortingSetting[]>(SETTING_CONTENT_SORTING),
|
||||
contentFolders: Folders.get(),
|
||||
filters: Settings.get<string[]>(SETTING_CONTENT_FILTERS),
|
||||
filters:
|
||||
Settings.get<(FilterType | { title: string; name: string })[]>(SETTING_CONTENT_FILTERS),
|
||||
crntFramework: Settings.get<string>(SETTING_FRAMEWORK_ID),
|
||||
framework: !isInitialized && wsFolder ? await FrameworkDetector.get(wsFolder.fsPath) : null,
|
||||
scripts: Settings.get<CustomScript[]>(SETTING_CUSTOM_SCRIPTS) || [],
|
||||
|
||||
@@ -15,20 +15,15 @@ import { Template } from '../commands/Template';
|
||||
import {
|
||||
EXTENSION_NAME,
|
||||
GITHUB_LINK,
|
||||
SETTING_DATE_FIELD,
|
||||
SETTING_MODIFIED_FIELD,
|
||||
EXTENSION_BETA_ID,
|
||||
EXTENSION_ID,
|
||||
ExtensionState,
|
||||
CONFIG_KEY,
|
||||
SETTING_CONTENT_PAGE_FOLDERS,
|
||||
SETTING_DASHBOARD_MEDIA_SNIPPET,
|
||||
SETTING_CONTENT_SNIPPETS,
|
||||
SETTING_TEMPLATES_ENABLED,
|
||||
SETTING_TAXONOMY_TAGS,
|
||||
SETTING_TAXONOMY_CATEGORIES
|
||||
} from '../constants';
|
||||
import { ContentFolder, Snippet, TaxonomyType } from '../models';
|
||||
import { ContentFolder, TaxonomyType } from '../models';
|
||||
import { Notifications } from './Notifications';
|
||||
import { Settings } from './SettingsHelper';
|
||||
import { TaxonomyHelper } from './TaxonomyHelper';
|
||||
@@ -202,64 +197,6 @@ export class Extension {
|
||||
await Settings.createTeamSettings();
|
||||
}
|
||||
|
||||
const hideDateDeprecation = await Extension.getInstance().getState<boolean>(
|
||||
ExtensionState.Updates.v7_0_0.dateFields,
|
||||
'workspace'
|
||||
);
|
||||
if (!hideDateDeprecation) {
|
||||
// Migration scripts can be written here
|
||||
const publishField = Settings.inspect(SETTING_DATE_FIELD);
|
||||
const modifiedField = Settings.inspect(SETTING_MODIFIED_FIELD);
|
||||
|
||||
// Check for extension deprecations
|
||||
if (
|
||||
publishField?.workspaceValue ||
|
||||
publishField?.globalValue ||
|
||||
publishField?.teamValue ||
|
||||
modifiedField?.workspaceValue ||
|
||||
modifiedField?.globalValue ||
|
||||
modifiedField?.teamValue
|
||||
) {
|
||||
Notifications.warning(
|
||||
l10n.t(
|
||||
LocalizationKey.helpersExtensionMigrateSettingsDeprecatedWarning,
|
||||
`${CONFIG_KEY}.${SETTING_DATE_FIELD}`,
|
||||
`${CONFIG_KEY}.${SETTING_MODIFIED_FIELD}`
|
||||
),
|
||||
l10n.t(LocalizationKey.helpersExtensionMigrateSettingsDeprecatedWarningHide),
|
||||
l10n.t(LocalizationKey.helpersExtensionMigrateSettingsDeprecatedWarningSeeGuide)
|
||||
).then(async (value) => {
|
||||
if (
|
||||
value ===
|
||||
l10n.t(LocalizationKey.helpersExtensionMigrateSettingsDeprecatedWarningSeeGuide)
|
||||
) {
|
||||
const isProd = this.isProductionMode;
|
||||
commands.executeCommand(
|
||||
'vscode.open',
|
||||
Uri.parse(
|
||||
`https://${
|
||||
isProd ? '' : 'beta.'
|
||||
}frontmatter.codes/docs/troubleshooting#publish-and-modified-date-migration`
|
||||
)
|
||||
);
|
||||
await Extension.getInstance().setState<boolean>(
|
||||
ExtensionState.Updates.v7_0_0.dateFields,
|
||||
true,
|
||||
'workspace'
|
||||
);
|
||||
} else if (
|
||||
value === l10n.t(LocalizationKey.helpersExtensionMigrateSettingsDeprecatedWarningHide)
|
||||
) {
|
||||
await Extension.getInstance().setState<boolean>(
|
||||
ExtensionState.Updates.v7_0_0.dateFields,
|
||||
true,
|
||||
'workspace'
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (major < 7) {
|
||||
const contentFolders: ContentFolder[] = Settings.get(
|
||||
SETTING_CONTENT_PAGE_FOLDERS
|
||||
@@ -282,28 +219,6 @@ export class Extension {
|
||||
}
|
||||
|
||||
if (major <= 7 && minor < 3) {
|
||||
const mediaSnippet = Settings.get<string[]>(SETTING_DASHBOARD_MEDIA_SNIPPET);
|
||||
if (mediaSnippet && mediaSnippet.length > 0) {
|
||||
let snippet = mediaSnippet.join(`\n`);
|
||||
|
||||
snippet = snippet.replace(`{mediaUrl}`, `[[&mediaUrl]]`);
|
||||
snippet = snippet.replace(`{mediaHeight}`, `[[mediaHeight]]`);
|
||||
snippet = snippet.replace(`{mediaWidth}`, `[[mediaWidth]]`);
|
||||
snippet = snippet.replace(`{caption}`, `[[&caption]]`);
|
||||
snippet = snippet.replace(`{alt}`, `[[alt]]`);
|
||||
snippet = snippet.replace(`{filename}`, `[[filename]]`);
|
||||
snippet = snippet.replace(`{title}`, `[[title]]`);
|
||||
|
||||
const snippets = Settings.get<Snippet[]>(SETTING_CONTENT_SNIPPETS) || ({} as any);
|
||||
snippets[`Media snippet (migrated)`] = {
|
||||
body: snippet.split(`\n`),
|
||||
isMediaSnippet: true,
|
||||
description: `Migrated media snippet from frontMatter.dashboard.mediaSnippet setting`
|
||||
};
|
||||
|
||||
await Settings.update(SETTING_CONTENT_SNIPPETS, snippets, true);
|
||||
}
|
||||
|
||||
const templates = await Template.getTemplates();
|
||||
if (templates && templates.length > 0) {
|
||||
const answer = await window.showQuickPick(
|
||||
|
||||
@@ -8,6 +8,12 @@ import { Logger } from './Logger';
|
||||
import { SponsorAi } from '../services/SponsorAI';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
import { LocalizationKey } from '../localization';
|
||||
import { ContentFolder } from '../models';
|
||||
|
||||
interface FolderQuickPickItem extends QuickPickItem {
|
||||
path: string;
|
||||
locale?: string;
|
||||
}
|
||||
|
||||
export class Questions {
|
||||
/**
|
||||
@@ -124,13 +130,27 @@ export class Questions {
|
||||
*/
|
||||
public static async SelectContentFolder(
|
||||
showWarning: boolean = true
|
||||
): Promise<string | undefined> {
|
||||
): Promise<FolderQuickPickItem | undefined> {
|
||||
let folders = Folders.get().filter((f) => !f.disableCreation);
|
||||
|
||||
let selectedFolder: string | undefined;
|
||||
let selectedFolder: FolderQuickPickItem | undefined;
|
||||
if (folders.length > 1) {
|
||||
const folderOptions = folders.map((f: ContentFolder) => {
|
||||
if (f.locale) {
|
||||
return {
|
||||
label: `${f.title} (${f.localeTitle || f.locale})`,
|
||||
locale: f.locale,
|
||||
path: f.path
|
||||
} as FolderQuickPickItem;
|
||||
}
|
||||
return {
|
||||
label: f.title,
|
||||
path: f.path
|
||||
} as FolderQuickPickItem;
|
||||
});
|
||||
|
||||
selectedFolder = await window.showQuickPick(
|
||||
folders.map((f) => f.title),
|
||||
folderOptions,
|
||||
{
|
||||
title: l10n.t(LocalizationKey.helpersQuestionsSelectContentFolderQuickPickTitle),
|
||||
placeHolder: l10n.t(
|
||||
@@ -140,7 +160,10 @@ export class Questions {
|
||||
}
|
||||
);
|
||||
} else if (folders.length === 1) {
|
||||
selectedFolder = folders[0].title;
|
||||
selectedFolder = {
|
||||
label: folders[0].title,
|
||||
path: folders[0].path
|
||||
} as FolderQuickPickItem;
|
||||
} else {
|
||||
// When no page folders are found, the welcome dashboard is shown
|
||||
return;
|
||||
@@ -189,6 +212,11 @@ export class Questions {
|
||||
label: contentType.name
|
||||
}));
|
||||
|
||||
if (options.length === 0) {
|
||||
Notifications.error(LocalizationKey.helpersQuestionsSelectContentTypeQuickPickErrorNoContentTypes);
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedOption = await window.showQuickPick(options, {
|
||||
title: l10n.t(LocalizationKey.helpersQuestionsSelectContentTypeQuickPickTitle),
|
||||
placeHolder: l10n.t(LocalizationKey.helpersQuestionsSelectContentTypeQuickPickPlaceholder),
|
||||
|
||||
@@ -43,7 +43,8 @@ import {
|
||||
SETTING_CONFIG_DYNAMIC_FILE_PATH,
|
||||
SETTING_PROJECTS,
|
||||
SETTING_TAXONOMY_TAGS,
|
||||
SETTING_TAXONOMY_CATEGORIES
|
||||
SETTING_TAXONOMY_CATEGORIES,
|
||||
SETTING_CONTENT_FILTERS
|
||||
} from '../constants';
|
||||
import { Folders } from '../commands/Folders';
|
||||
import { join, basename, dirname, parse } from 'path';
|
||||
@@ -804,7 +805,8 @@ export class Settings {
|
||||
settingName === SETTING_GLOBAL_NOTIFICATIONS ||
|
||||
settingName === SETTING_GLOBAL_NOTIFICATIONS_DISABLED ||
|
||||
settingName === SETTING_MEDIA_SUPPORTED_MIMETYPES ||
|
||||
settingName === SETTING_COMMA_SEPARATED_FIELDS
|
||||
settingName === SETTING_COMMA_SEPARATED_FIELDS ||
|
||||
settingName === SETTING_CONTENT_FILTERS
|
||||
) {
|
||||
if (typeof originalConfig[key] === 'undefined') {
|
||||
Settings.globalConfig[key] = value;
|
||||
|
||||
@@ -24,6 +24,9 @@ export class SlugHelper {
|
||||
if (slugTemplate) {
|
||||
if (slugTemplate.includes('{{title}}')) {
|
||||
const regex = new RegExp('{{title}}', 'g');
|
||||
slugTemplate = slugTemplate.replace(regex, articleTitle.toLowerCase().replace(/\s/g, '-'));
|
||||
} else if (slugTemplate.includes('{{seoTitle}}')) {
|
||||
const regex = new RegExp('{{seoTitle}}', 'g');
|
||||
slugTemplate = slugTemplate.replace(regex, SlugHelper.slugify(articleTitle));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { workspace } from 'vscode';
|
||||
import { Extension, Settings } from '.';
|
||||
import { EXTENSION_BETA_ID, EXTENSION_ID, SETTING_TELEMETRY_DISABLE } from '../constants';
|
||||
|
||||
@@ -23,6 +24,24 @@ export class Telemetry {
|
||||
return Telemetry.instance;
|
||||
}
|
||||
|
||||
public static isVscodeEnabled(): boolean {
|
||||
const config = workspace.getConfiguration('telemetry');
|
||||
const isVscodeEnable = config.get<'off' | undefined>('enableTelemetry');
|
||||
return isVscodeEnable === 'off' ? false : true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if telemetry is enabled.
|
||||
* @returns {boolean} Returns true if telemetry is enabled, false otherwise.
|
||||
*/
|
||||
public static isEnabled(): boolean {
|
||||
const isVscodeEnable = Telemetry.isVscodeEnabled();
|
||||
|
||||
const isDisabled = Settings.get<boolean>(SETTING_TELEMETRY_DISABLE);
|
||||
|
||||
return isDisabled || isVscodeEnable ? false : true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send metrics to our own database
|
||||
* @param eventName
|
||||
@@ -30,8 +49,7 @@ export class Telemetry {
|
||||
* @returns
|
||||
*/
|
||||
public static send(eventName: string, properties?: any) {
|
||||
const isDisabled = Settings.get<boolean>(SETTING_TELEMETRY_DISABLE);
|
||||
if (isDisabled) {
|
||||
if (!Telemetry.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { GeneralCommands } from '../../constants';
|
||||
import { PostMessageData } from '../../models';
|
||||
import { BaseListener } from './BaseListener';
|
||||
import { getLocalizationFile } from '../../utils/getLocalizationFile';
|
||||
import { i18n } from '../../commands/i18n';
|
||||
|
||||
export class LocalizationListener extends BaseListener {
|
||||
/**
|
||||
@@ -11,14 +12,29 @@ export class LocalizationListener extends BaseListener {
|
||||
public static process(msg: PostMessageData) {
|
||||
switch (msg.command) {
|
||||
case GeneralCommands.toVSCode.getLocalization:
|
||||
this.getLocalization();
|
||||
this.getLocalization(msg.command, msg.requestId);
|
||||
break;
|
||||
case GeneralCommands.toVSCode.content.locales:
|
||||
this.getContentLocales(msg.command, msg.requestId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public static async getLocalization() {
|
||||
const fileContents = await getLocalizationFile();
|
||||
public static async getLocalization(command: string, requestId?: string) {
|
||||
if (!command || !requestId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.sendMsg(GeneralCommands.toWebview.setLocalization as any, fileContents);
|
||||
const fileContents = await getLocalizationFile();
|
||||
this.sendRequest(command as any, requestId, fileContents);
|
||||
}
|
||||
|
||||
private static async getContentLocales(command: string, requestId?: string) {
|
||||
if (!command || !requestId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const config = i18n.getAll();
|
||||
this.sendRequest(command as any, requestId, config);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,7 +306,6 @@ export class DataListener extends BaseListener {
|
||||
}
|
||||
}
|
||||
|
||||
const dateFields = ContentType.findFieldsByTypeDeep(contentType.fields, 'datetime');
|
||||
const imageFields = ContentType.findFieldsByTypeDeep(contentType.fields, 'image');
|
||||
const fileFields = ContentType.findFieldsByTypeDeep(contentType.fields, 'file');
|
||||
const fieldsWithEmojiEncoding = contentType.fields.filter((f) => f.encodeEmoji);
|
||||
@@ -314,13 +313,6 @@ export class DataListener extends BaseListener {
|
||||
// Support multi-level fields
|
||||
const parentObj = DataListener.getParentObject(article.data, article, parents, blockData);
|
||||
|
||||
const dateFieldsArray = dateFields.find((f: Field[]) => {
|
||||
const lastField = f?.[f.length - 1];
|
||||
if (lastField) {
|
||||
return lastField.name === field;
|
||||
}
|
||||
});
|
||||
|
||||
// Check multi-image fields
|
||||
const multiImageFieldsArray = imageFields.find((f: Field[]) => {
|
||||
const lastField = f?.[f.length - 1];
|
||||
@@ -338,13 +330,7 @@ export class DataListener extends BaseListener {
|
||||
});
|
||||
|
||||
// Check date fields
|
||||
if (dateFieldsArray && dateFieldsArray.length > 0) {
|
||||
for (const dateField of dateFieldsArray) {
|
||||
if (field === dateField.name && value) {
|
||||
parentObj[field] = Article.formatDate(new Date(value), dateField.dateFormat);
|
||||
}
|
||||
}
|
||||
} else if (multiImageFieldsArray || multiFileFieldsArray) {
|
||||
if (multiImageFieldsArray || multiFileFieldsArray) {
|
||||
const fields =
|
||||
multiImageFieldsArray && multiImageFieldsArray.length > 0
|
||||
? multiImageFieldsArray
|
||||
|
||||
@@ -11,14 +11,17 @@ export class LocalizationListener extends BaseListener {
|
||||
public static process(msg: PostMessageData) {
|
||||
switch (msg.command) {
|
||||
case GeneralCommands.toVSCode.getLocalization:
|
||||
this.getLocalization();
|
||||
this.getLocalization(msg.command, msg.requestId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public static async getLocalization() {
|
||||
const fileContents = await getLocalizationFile();
|
||||
public static async getLocalization(command: string, requestId?: string) {
|
||||
if (!command || !requestId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.sendMsg(GeneralCommands.toWebview.setLocalization as any, fileContents);
|
||||
const fileContents = await getLocalizationFile();
|
||||
this.sendRequest(command, requestId, fileContents);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,13 +248,33 @@ export enum LocalizationKey {
|
||||
*/
|
||||
settingsIntegrationsViewDeeplTitle = 'settings.integrationsView.deepl.title',
|
||||
/**
|
||||
* Authentication key
|
||||
* API key
|
||||
*/
|
||||
settingsIntegrationsViewDeeplIntputLabel = 'settings.integrationsView.deepl.intput.label',
|
||||
/**
|
||||
* Enter your DeepL authentication key
|
||||
* Enter your Azure Translator API key
|
||||
*/
|
||||
settingsIntegrationsViewDeeplIntputPlaceholder = 'settings.integrationsView.deepl.intput.placeholder',
|
||||
/**
|
||||
* Azure AI Translator Service
|
||||
*/
|
||||
settingsIntegrationsViewAzureTitle = 'settings.integrationsView.azure.title',
|
||||
/**
|
||||
* Subscription key
|
||||
*/
|
||||
settingsIntegrationsViewAzureIntputLabel = 'settings.integrationsView.azure.intput.label',
|
||||
/**
|
||||
* Enter your Azure AI Translator - Subscription key
|
||||
*/
|
||||
settingsIntegrationsViewAzureIntputPlaceholder = 'settings.integrationsView.azure.intput.placeholder',
|
||||
/**
|
||||
* Region
|
||||
*/
|
||||
settingsIntegrationsViewAzureRegionLabel = 'settings.integrationsView.azure.region.label',
|
||||
/**
|
||||
* Enter your Azure AI Translator - Region. Example: westeurope
|
||||
*/
|
||||
settingsIntegrationsViewAzureRegionPlaceholder = 'settings.integrationsView.azure.region.placeholder',
|
||||
/**
|
||||
* Developer mode
|
||||
*/
|
||||
@@ -1725,9 +1745,17 @@ export enum LocalizationKey {
|
||||
*/
|
||||
commandsI18nCreateWarningNoConfig = 'commands.i18n.create.warning.noConfig',
|
||||
/**
|
||||
* The current file cannot be used for i18n content creation.
|
||||
* Could not retrieve the locale for the current file.
|
||||
*/
|
||||
commandsI18nCreateWarningNotDefaultLocale = 'commands.i18n.create.warning.notDefaultLocale',
|
||||
commandsI18nCreateErrorNoLocaleDefinition = 'commands.i18n.create.error.noLocaleDefinition',
|
||||
/**
|
||||
* Current file has been translated to all available languages.
|
||||
*/
|
||||
commandsI18nCreateErrorNoLocales = 'commands.i18n.create.error.noLocales',
|
||||
/**
|
||||
* Could not define a content folder for the current file.
|
||||
*/
|
||||
commandsI18nCreateErrorNoContentFolder = 'commands.i18n.create.error.noContentFolder',
|
||||
/**
|
||||
* The i18n translation already exists.
|
||||
*/
|
||||
@@ -2148,18 +2176,6 @@ export enum LocalizationKey {
|
||||
* {0} has been updated to v{1} — check out what's new!
|
||||
*/
|
||||
helpersExtensionGetVersionUpdateNotification = 'helpers.extension.getVersion.update.notification',
|
||||
/**
|
||||
* The "{0}" and "{1}" settings have been deprecated. Please use the "isPublishDate" and "isModifiedDate" datetime field properties instead.
|
||||
*/
|
||||
helpersExtensionMigrateSettingsDeprecatedWarning = 'helpers.extension.migrateSettings.deprecated.warning',
|
||||
/**
|
||||
* Hide
|
||||
*/
|
||||
helpersExtensionMigrateSettingsDeprecatedWarningHide = 'helpers.extension.migrateSettings.deprecated.warning.hide',
|
||||
/**
|
||||
* See migration guide
|
||||
*/
|
||||
helpersExtensionMigrateSettingsDeprecatedWarningSeeGuide = 'helpers.extension.migrateSettings.deprecated.warning.seeGuide',
|
||||
/**
|
||||
* {0} - Templates
|
||||
*/
|
||||
@@ -2280,6 +2296,10 @@ export enum LocalizationKey {
|
||||
* No content type was selected.
|
||||
*/
|
||||
helpersQuestionsSelectContentTypeNoSelectionWarning = 'helpers.questions.selectContentType.noSelection.warning',
|
||||
/**
|
||||
* There are no matching content types configured for this folder.
|
||||
*/
|
||||
helpersQuestionsSelectContentTypeQuickPickErrorNoContentTypes = 'helpers.questions.selectContentType.quickPick.error.noContentTypes',
|
||||
/**
|
||||
* Article {0} is longer than {1} characters (current length: {2}). For SEO reasons, it would be better to make it less than {1} characters.
|
||||
*/
|
||||
|
||||
@@ -12,6 +12,10 @@ export interface ContentFolder {
|
||||
originalPath?: string;
|
||||
$schema?: string;
|
||||
extended?: boolean;
|
||||
|
||||
locale?: string;
|
||||
localeTitle?: string;
|
||||
localeSourcePath?: string;
|
||||
defaultLocale?: string;
|
||||
locales: I18nConfig[];
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export type FilterType = 'contentFolders' | 'tags' | 'categories';
|
||||
@@ -191,6 +191,8 @@ export interface FolderInfo {
|
||||
title: string;
|
||||
files: number;
|
||||
lastModified: FileInfo[];
|
||||
locale?: string;
|
||||
localeTitle?: string;
|
||||
}
|
||||
|
||||
export interface FileInfo extends FileStat {
|
||||
|
||||
@@ -11,6 +11,7 @@ export * from './DataFile';
|
||||
export * from './DataFolder';
|
||||
export * from './DataType';
|
||||
export * from './DraftField';
|
||||
export * from './FilterType';
|
||||
export * from './Framework';
|
||||
export * from './GitRepository';
|
||||
export * from './GitSettings';
|
||||
|
||||
@@ -286,7 +286,9 @@ export class PanelProvider implements WebviewViewProvider, Disposable {
|
||||
<body>
|
||||
<div id="app" data-isProd="${isProd}" data-environment="${
|
||||
isBeta ? 'BETA' : 'main'
|
||||
}" data-version="${version.usedVersion}"></div>
|
||||
}" data-version="${
|
||||
version.usedVersion
|
||||
}" data-is-crash-disabled="${!Telemetry.isVscodeEnabled()}"></div>
|
||||
|
||||
${(scriptsToLoad || [])
|
||||
.map((script) => {
|
||||
|
||||
@@ -33,7 +33,6 @@ export const ViewPanel: React.FunctionComponent<IViewPanelProps> = (
|
||||
folderAndFiles,
|
||||
focusElm,
|
||||
unsetFocus,
|
||||
localeReady,
|
||||
mode
|
||||
} = useMessages();
|
||||
const prevMediaSelection = usePrevious(mediaSelecting);
|
||||
@@ -83,7 +82,7 @@ export const ViewPanel: React.FunctionComponent<IViewPanelProps> = (
|
||||
);
|
||||
}
|
||||
|
||||
if (loading && !localeReady) {
|
||||
if (loading) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
|
||||
@@ -95,6 +95,13 @@ export const DataBlockField: React.FunctionComponent<IDataBlockFieldProps> = ({
|
||||
// Delete the field group to have it added at the end
|
||||
delete data['fieldGroup'];
|
||||
|
||||
// Remove the empty fields
|
||||
Object.keys(data).forEach((key) => {
|
||||
if (data[key] === undefined || data[key] === null || Object.keys(data[key]).length === 0) {
|
||||
delete data[key];
|
||||
}
|
||||
});
|
||||
|
||||
if (selectedIndex !== null && selectedIndex !== undefined && dataClone.length > 0) {
|
||||
dataClone[selectedIndex] = {
|
||||
...data,
|
||||
@@ -306,7 +313,7 @@ export const DataBlockField: React.FunctionComponent<IDataBlockFieldProps> = ({
|
||||
{selectedGroup?.fields &&
|
||||
fieldsRenderer(
|
||||
selectedGroup?.fields,
|
||||
selectedBlockData || {},
|
||||
Object.assign({}, selectedBlockData) || {},
|
||||
[...parentFields, field.name],
|
||||
{
|
||||
parentFields: [...parentFields, field.name],
|
||||
|
||||
@@ -11,7 +11,7 @@ import { LocalizationKey } from '../../../localization';
|
||||
|
||||
export interface IDateTimeFieldProps extends BaseFieldProps<Date | null> {
|
||||
format?: string;
|
||||
onChange: (date: Date) => void;
|
||||
onChange: (date: string) => void;
|
||||
}
|
||||
|
||||
type InputProps = JSX.IntrinsicElements['input'];
|
||||
@@ -37,7 +37,7 @@ export const DateTimeField: React.FunctionComponent<IDateTimeFieldProps> = ({
|
||||
|
||||
const onDateChange = React.useCallback((date: Date) => {
|
||||
setDateValue(date);
|
||||
onChange(date);
|
||||
onChange(DateHelper.format(date, format || DEFAULT_FORMAT) || "");
|
||||
}, [format, onChange]);
|
||||
|
||||
const showRequiredState = useMemo(() => {
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import * as React from 'react';
|
||||
import { BlockFieldData, Field, PanelSettings } from '../../../models';
|
||||
import { IMetadata } from '../Metadata';
|
||||
import { FieldTitle } from './FieldTitle';
|
||||
|
||||
export interface IFieldCollectionProps {
|
||||
field: Field;
|
||||
parent: IMetadata;
|
||||
parentFields: string[];
|
||||
blockData: BlockFieldData | undefined;
|
||||
settings: PanelSettings;
|
||||
renderFields: (
|
||||
ctFields: Field[],
|
||||
parent: IMetadata,
|
||||
parentFields: string[],
|
||||
blockData?: BlockFieldData,
|
||||
onFieldUpdate?: (field: string | undefined, value: any, parents: string[]) => void,
|
||||
parentBlock?: string | null
|
||||
) => (JSX.Element | null)[] | undefined;
|
||||
onChange: (field: string | undefined, value: any, parents: string[]) => void;
|
||||
}
|
||||
|
||||
export const FieldCollection: React.FunctionComponent<IFieldCollectionProps> = ({
|
||||
field,
|
||||
parent,
|
||||
parentFields,
|
||||
blockData,
|
||||
settings,
|
||||
renderFields,
|
||||
onChange
|
||||
}: React.PropsWithChildren<IFieldCollectionProps>) => {
|
||||
const [fields, setFields] = React.useState<Field[]>([]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!settings.fieldGroups) {
|
||||
return
|
||||
}
|
||||
|
||||
const group = settings.fieldGroups.find((group) => group.id === field.fieldGroup);
|
||||
if (group) {
|
||||
setFields(group.fields);
|
||||
}
|
||||
}, [field, settings?.fieldGroups]);
|
||||
|
||||
if (!fields || fields.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`metadata_field__box`}>
|
||||
<FieldTitle
|
||||
className={`metadata_field__label_parent`}
|
||||
label={field.title || field.name}
|
||||
icon={undefined}
|
||||
required={field.required}
|
||||
/>
|
||||
|
||||
{field.description && (
|
||||
<p className={`metadata_field__description`}>{field.description}</p>
|
||||
)}
|
||||
|
||||
{renderFields(
|
||||
fields,
|
||||
parent,
|
||||
[...parentFields, field.name],
|
||||
blockData,
|
||||
onChange
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -26,7 +26,8 @@ import {
|
||||
PreviewImageField,
|
||||
PreviewImageValue,
|
||||
NumberField,
|
||||
CustomField
|
||||
CustomField,
|
||||
FieldCollection
|
||||
} from '.';
|
||||
import { fieldWhenClause } from '../../../utils/fieldWhenClause';
|
||||
import { ContentTypeRelationshipField } from './ContentTypeRelationshipField';
|
||||
@@ -521,6 +522,27 @@ export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else if (field.type === "fieldCollection") {
|
||||
if (!parent[field.name]) {
|
||||
parent[field.name] = {};
|
||||
}
|
||||
|
||||
const subMetadata = parent[field.name] as IMetadata;
|
||||
|
||||
return (
|
||||
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
|
||||
<FieldCollection
|
||||
key={field.name}
|
||||
field={field}
|
||||
parent={subMetadata}
|
||||
parentFields={parentFields}
|
||||
renderFields={renderFields}
|
||||
settings={settings}
|
||||
blockData={blockData}
|
||||
onChange={onSendUpdate}
|
||||
/>
|
||||
</FieldBoundary>
|
||||
);
|
||||
} else {
|
||||
console.warn(l10n.t(LocalizationKey.panelFieldsWrapperFieldUnknown, field.type));
|
||||
return null;
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
export * from './ChoiceButton';
|
||||
export * from './ChoiceField';
|
||||
export * from './ContentTypeRelationshipField';
|
||||
export * from './CustomField';
|
||||
export * from './DataFileField';
|
||||
export * from './DateTimeField';
|
||||
export * from './DraftField';
|
||||
export * from './FieldCollection';
|
||||
export * from './FieldMessage';
|
||||
export * from './FieldTitle';
|
||||
export * from './FileField';
|
||||
|
||||
@@ -29,7 +29,7 @@ const FolderAndFiles: React.FunctionComponent<IFolderAndFilesProps> = ({
|
||||
{folder.lastModified ? (
|
||||
<div key={`${folder.title}-${idx}`}>
|
||||
<FileList
|
||||
folderName={folder.title}
|
||||
folderName={folder.locale ? `${folder.title} (${folder.localeTitle || folder.locale})` : folder.title}
|
||||
totalFiles={folder.files}
|
||||
files={folder.lastModified}
|
||||
/>
|
||||
|
||||
@@ -10,13 +10,11 @@ import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { EventData } from '@estruyf/vscode/dist/models';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { PanelSettingsAtom } from '../state';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
|
||||
export default function useMessages() {
|
||||
const [metadata, setMetadata] = useState<any>({});
|
||||
const [settings, setSettings] = useRecoilState(PanelSettingsAtom);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [localeReady, setLocaleReady] = useState<boolean>(false);
|
||||
const [focusElm, setFocus] = useState<TagType | null>(null);
|
||||
const [folderAndFiles, setFolderAndFiles] = useState<FolderInfo[] | undefined>(undefined);
|
||||
const [mediaSelecting, setMediaSelecting] = useState<DashboardData | undefined>(undefined);
|
||||
@@ -52,12 +50,6 @@ export default function useMessages() {
|
||||
case GeneralCommands.toWebview.setMode:
|
||||
setMode(message.payload);
|
||||
break;
|
||||
case GeneralCommands.toWebview.setLocalization:
|
||||
l10n.config({
|
||||
contents: message.payload
|
||||
})
|
||||
setLocaleReady(true)
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -99,7 +91,6 @@ export default function useMessages() {
|
||||
loading,
|
||||
mediaSelecting,
|
||||
mode,
|
||||
localeReady,
|
||||
unsetFocus
|
||||
};
|
||||
}
|
||||
|
||||
+11
-15
@@ -1,10 +1,10 @@
|
||||
import * as React from 'react';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { render } from 'react-dom';
|
||||
import { ViewPanel } from './ViewPanel';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Integrations } from '@sentry/tracing';
|
||||
import { SENTRY_LINK, SentryIgnore } from '../constants';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
import { I10nProvider } from '../dashboardWebView/providers/I10nProvider';
|
||||
import { SentryInit } from '../utils/sentryInit';
|
||||
|
||||
import './styles.css';
|
||||
|
||||
@@ -33,16 +33,10 @@ if (elm) {
|
||||
const version = elm?.getAttribute('data-version');
|
||||
const environment = elm?.getAttribute('data-environment');
|
||||
const isProd = elm?.getAttribute('data-isProd');
|
||||
const isCrashDisabled = elm?.getAttribute('data-is-crash-disabled');
|
||||
|
||||
if (isProd === 'true') {
|
||||
Sentry.init({
|
||||
dsn: SENTRY_LINK,
|
||||
integrations: [new Integrations.BrowserTracing()],
|
||||
tracesSampleRate: 0, // No performance tracing required
|
||||
release: version || '',
|
||||
environment: environment || '',
|
||||
ignoreErrors: SentryIgnore
|
||||
});
|
||||
if (isProd === 'true' && isCrashDisabled === 'false') {
|
||||
Sentry.init(SentryInit(version, environment));
|
||||
|
||||
Sentry.setTag("type", "panel");
|
||||
if (document.body.getAttribute(`data-vscode-theme-id`)) {
|
||||
@@ -51,9 +45,11 @@ if (elm) {
|
||||
}
|
||||
|
||||
render(
|
||||
<RecoilRoot>
|
||||
<ViewPanel />
|
||||
</RecoilRoot>,
|
||||
<I10nProvider>
|
||||
<RecoilRoot>
|
||||
<ViewPanel />
|
||||
</RecoilRoot>
|
||||
</I10nProvider>,
|
||||
elm
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import { ExtensionState } from '../constants';
|
||||
import { Extension } from '../helpers';
|
||||
|
||||
export class Translations {
|
||||
/**
|
||||
* Translates an array of text from a source language to a target language.
|
||||
* @param text - The array of text to be translated.
|
||||
* @param source - The source language code.
|
||||
* @param target - The target language code.
|
||||
* @returns A Promise that resolves to an array of translated text, or undefined if translation is not possible.
|
||||
*/
|
||||
public static async translate(
|
||||
text: string[],
|
||||
source: string,
|
||||
target: string
|
||||
): Promise<string[] | undefined> {
|
||||
const deeplAuthKey = await Extension.getInstance().getSecret(
|
||||
ExtensionState.Secrets.Deepl.ApiKey
|
||||
);
|
||||
const azureAuthKey = await Extension.getInstance().getSecret(
|
||||
ExtensionState.Secrets.Azure.TranslatorKey
|
||||
);
|
||||
const azureRegion = await Extension.getInstance().getSecret(
|
||||
ExtensionState.Secrets.Azure.TranslatorRegion
|
||||
);
|
||||
|
||||
if (azureAuthKey && azureRegion) {
|
||||
return this.translateAzure(text, source, target, azureAuthKey, azureRegion);
|
||||
}
|
||||
|
||||
if (deeplAuthKey) {
|
||||
return this.translateDeepL(text, source, target, deeplAuthKey);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates an array of text using Azure Cognitive Services Translator API.
|
||||
* @param text - The array of text to be translated.
|
||||
* @param source - The source language code.
|
||||
* @param target - The target language code.
|
||||
* @param azureAuthKey - The Azure authentication key.
|
||||
* @param azureRegion - The Azure region for the translation service.
|
||||
* @returns A promise that resolves to an array of translated text.
|
||||
* @throws An error if the translation fails.
|
||||
*/
|
||||
private static async translateAzure(
|
||||
text: string[],
|
||||
source: string,
|
||||
target: string,
|
||||
azureAuthKey: string,
|
||||
azureRegion: string
|
||||
): Promise<string[]> {
|
||||
try {
|
||||
const body = JSON.stringify(text.map((t) => ({ Text: t })));
|
||||
|
||||
const response = await fetch(
|
||||
`https://api.cognitive.microsofttranslator.com/translate?api-version=3.0&to=${target}&from=${source}&textType=html`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Ocp-Apim-Subscription-Key': azureAuthKey,
|
||||
'Ocp-Apim-Subscription-Region': azureRegion,
|
||||
'Content-Type': 'application/json; charset=UTF-8'
|
||||
},
|
||||
body
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
return data.map((t: { translations: { text: string }[] }) => t.translations[0].text);
|
||||
} catch (error) {
|
||||
throw new Error(`Azure: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates an array of text using the DeepL translation service.
|
||||
* @param text - The text to be translated.
|
||||
* @param source - The source language of the text.
|
||||
* @param target - The target language for the translation.
|
||||
* @param deeplAuthKey - The authentication key for accessing the DeepL API.
|
||||
* @returns A Promise that resolves to an array of translated text.
|
||||
* @throws If there is an error during the translation process.
|
||||
*/
|
||||
private static async translateDeepL(
|
||||
text: string[],
|
||||
source: string,
|
||||
target: string,
|
||||
deeplAuthKey: string
|
||||
): Promise<string[]> {
|
||||
try {
|
||||
const body = JSON.stringify({
|
||||
text,
|
||||
source_lang: source,
|
||||
target_lang: target
|
||||
});
|
||||
|
||||
let host = deeplAuthKey.endsWith(':fx') ? 'api-free.deepl.com' : 'api.deepl.com';
|
||||
|
||||
const response = await fetch(`https://${host}/v2/translate`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `DeepL-Auth-Key ${deeplAuthKey}`,
|
||||
'User-Agent': `FrontMatterCMS/${Extension.getInstance().version}`,
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json'
|
||||
},
|
||||
body
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (!data.translations || data.translations.length < 3) {
|
||||
throw new Error('Invalid response');
|
||||
}
|
||||
|
||||
return data.translations.map((t: { text: string }) => t.text);
|
||||
} catch (error) {
|
||||
throw new Error(`DeepL: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Integrations } from '@sentry/tracing';
|
||||
import { SENTRY_LINK, SentryIgnore } from '../constants';
|
||||
|
||||
export const SentryInit = (
|
||||
version: string | null,
|
||||
environment: string | null
|
||||
): Sentry.BrowserOptions => ({
|
||||
dsn: SENTRY_LINK,
|
||||
integrations: [new Integrations.BrowserTracing()],
|
||||
tracesSampleRate: 0, // No performance tracing required
|
||||
release: version || '',
|
||||
environment: environment || '',
|
||||
ignoreErrors: SentryIgnore,
|
||||
beforeSend(event) {
|
||||
if (event.user) {
|
||||
delete event.user.ip_address;
|
||||
}
|
||||
return event;
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user