Merge branch 'dev' of github.com:estruyf/vscode-front-matter into dev

This commit is contained in:
Elio Struyf
2024-02-27 09:43:51 +01:00
55 changed files with 867 additions and 388 deletions
+3 -3
View File
@@ -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
+27
View File
@@ -54,6 +54,12 @@ A couple of our extension highlights that hopefully get you interested in giving
> If you see something missing in your article creation flow, please feel free to reach out.
**Version 10**
In version 10, we introduced the new i18n/multilingual support for your content. You can now manage your content in multiple languages, more information can be found in the [multilingual](https://frontmatter.codes/docs/content-creation/multilingual) section of our documentation.
![Multilingual support](https://beta.frontmatter.codes/releases/v10.0.0/multilingual-content.png)
**Version 9**
The extension is now available in multiple languages: English, German, and Japanese. Want to add your language? Check out the [localization the extension](https://frontmatter.codes/docs/contributing#translating-the-extension).
@@ -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)
+27
View File
@@ -52,6 +52,12 @@ A couple of our extension highlights that hopefully get you interested in giving
> If you see something missing in your article creation flow, please feel free to reach out.
**Version 10**
In version 10, we introduced the new i18n/multilingual support for your content. You can now manage your content in multiple languages, more information can be found in the [multilingual](https://frontmatter.codes/docs/content-creation/multilingual) section of our documentation.
![Multilingual support](https://beta.frontmatter.codes/releases/v10.0.0/multilingual-content.png)
**Version 9**
The extension is now available in multiple languages: English, German, and Japanese. Want to add your language? Check out the [localization the extension](https://frontmatter.codes/docs/contributing#translating-the-extension).
@@ -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
View File
@@ -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.",
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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)) {
+1 -1
View 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
View File
@@ -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
};
}
}
+3 -3
View File
@@ -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
View File
@@ -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');
+7 -1
View File
@@ -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`
}
}
};
+3
View File
@@ -21,6 +21,9 @@ export const GeneralCommands = {
get: 'getSecret',
set: 'setSecret'
},
content: {
locales: 'getContentLocales'
},
runCommand: 'runCommand',
getLocalization: 'getLocalization',
openOnWebsite: 'openOnWebsite'
+2 -1
View File
@@ -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.`
];
+1 -1
View File
@@ -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',
-10
View File
@@ -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';
+2 -2
View File
@@ -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>
+1 -11
View File
@@ -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
};
}
+8 -12
View File
@@ -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(() => {
+24 -24
View File
@@ -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
+2 -1
View File
@@ -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 };
+1 -6
View File
@@ -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;
}
/**
+12 -1
View File
@@ -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;
}
+3 -1
View File
@@ -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) || [],
+1 -86
View File
@@ -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(
+32 -4
View File
@@ -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),
+4 -2
View File
@@ -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;
+3
View File
@@ -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));
}
+20 -2
View File
@@ -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);
}
}
+1 -15
View File
@@ -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
+7 -4
View File
@@ -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);
}
}
+36 -16
View File
@@ -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.
*/
+4
View File
@@ -12,6 +12,10 @@ export interface ContentFolder {
originalPath?: string;
$schema?: string;
extended?: boolean;
locale?: string;
localeTitle?: string;
localeSourcePath?: string;
defaultLocale?: string;
locales: I18nConfig[];
}
+1
View File
@@ -0,0 +1 @@
export type FilterType = 'contentFolders' | 'tags' | 'categories';
+2
View File
@@ -191,6 +191,8 @@ export interface FolderInfo {
title: string;
files: number;
lastModified: FileInfo[];
locale?: string;
localeTitle?: string;
}
export interface FileInfo extends FileStat {
+1
View File
@@ -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';
+3 -1
View File
@@ -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) => {
+1 -2
View File
@@ -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}
/>
-9
View File
@@ -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
View File
@@ -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
);
}
+132
View File
@@ -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}`);
}
}
}
+21
View File
@@ -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;
}
});