#756 - i18n content creation

This commit is contained in:
Elio Struyf
2024-02-18 16:12:01 +01:00
parent 0428e561a8
commit 5f0fd29cca
16 changed files with 605 additions and 111 deletions
+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" width="16" height="16" viewBox="0 0 24 24" stroke-width="2" stroke="#C5C5C5">
<path stroke-linecap="round" stroke-linejoin="round" d="m10.5 21 5.25-11.25L21 21m-9-3h7.5M3 5.621a48.474 48.474 0 0 1 6-.371m0 0c1.12 0 2.233.038 3.334.114M9 5.25V3m3.334 2.364C11.176 10.658 7.69 15.08 3 17.502m9.334-12.138c.896.061 1.785.147 2.666.257m-4.589 8.495a18.023 18.023 0 0 1-3.827-5.802" />
</svg>

After

Width:  |  Height:  |  Size: 442 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" width="16" height="16" viewBox="0 0 24 24" stroke-width="2" stroke="#424242">
<path stroke-linecap="round" stroke-linejoin="round" d="m10.5 21 5.25-11.25L21 21m-9-3h7.5M3 5.621a48.474 48.474 0 0 1 6-.371m0 0c1.12 0 2.233.038 3.334.114M9 5.25V3m3.334 2.364C11.176 10.658 7.69 15.08 3 17.502m9.334-12.138c.896.061 1.785.147 2.666.257m-4.589 8.495a18.023 18.023 0 0 1-3.827-5.802" />
</svg>

After

Width:  |  Height:  |  Size: 442 B

+20 -4
View File
@@ -257,7 +257,7 @@
"markdownDescription": "%setting.frontMatter.content.hideFmMessage.markdownDescription%",
"scope": "Content"
},
"frontMatter.content.pageFolderscontent.pageFolders": {
"frontMatter.content.pageFolders": {
"type": "array",
"default": [],
"markdownDescription": "%setting.frontMatter.content.pageFolders.markdownDescription%",
@@ -303,6 +303,10 @@
"type": "boolean",
"default": false,
"description": "%setting.frontMatter.content.pageFolders.items.properties.disableCreation.description%"
},
"defaultLocale": {
"type": "string",
"description": "%setting.frontMatter.content.pageFolders.items.properties.defaultLocale.description%"
}
},
"additionalProperties": false,
@@ -335,8 +339,7 @@
},
"additionalProperties": false,
"required": [
"locale",
"path"
"locale"
]
},
"scope": "Content"
@@ -2311,7 +2314,11 @@
{
"command": "frontMatter.i18n.create",
"title": "%command.frontMatter.i18n.create%",
"category": "Front Matter"
"category": "Front Matter",
"icon": {
"light": "assets/icons/i18n-light.svg",
"dark": "assets/icons/i18n-dark.svg"
}
}
],
"submenus": [{
@@ -2353,6 +2360,11 @@
"group": "navigation@-128",
"when": "frontMatter:file:isValid == true"
},
{
"command": "frontMatter.i18n.create",
"group": "navigation@-127",
"when": "frontMatter:file:isValid && frontMatter:i18n:default"
},
{
"command": "frontMatter.markup.options",
"group": "navigation@-126",
@@ -2452,6 +2464,10 @@
"command": "frontMatter.git.sync",
"when": "frontMatter:git:enabled"
},
{
"command": "frontMatter.i18n.create",
"when": "frontMatter:i18n:default"
},
{
"command": "frontMatter.collapseSections",
"when": "false"
+2
View File
@@ -48,6 +48,7 @@
"command.frontMatter.markup.unorderedlist": "Unordered list",
"command.frontMatter.git.sync": "Sync",
"command.frontMatter.cache.clear": "Clear cache",
"command.frontMatter.i18n.create": "Create new translation",
"settings.configuration.title": "Front Matter: use frontmatter.json for shared team settings",
"setting.frontMatter.projects.markdownDescription": "Specify the list of projects to load in the Front Matter CMS. [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.projects)",
"setting.frontMatter.projects.items.properties.name.markdownDescription": "Specify the name of the project.",
@@ -75,6 +76,7 @@
"setting.frontMatter.content.pageFolders.items.properties.filePrefix.description": "Defines a prefix for the file name.",
"setting.frontMatter.content.pageFolders.items.properties.contentTypes.description": "Defines which content types can be used for the current location. If not defined, all content types will be available.",
"setting.frontMatter.content.pageFolders.items.properties.disableCreation.description": "Disable the creation of new content in the folder.",
"setting.frontMatter.content.pageFolders.items.properties.defaultLocale.description": "Set the page folder as a default locale for the content. All content from this folder is translatable to the languages defined in the `frontMatter.content.i18n` setting.",
"setting.frontMatter.content.placeholders.markdownDescription": "This array of placeholders defines the placeholders that you can use in your content types and templates for automatically populating your content its front matter. [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.content.placeholders)",
"setting.frontMatter.content.placeholders.items.properties.id.description": "ID of the placeholder, in your content type or template, use it as follows: {{placeholder}}",
"setting.frontMatter.content.placeholders.items.properties.value.description": "The placeholder its value",
+115 -56
View File
@@ -1,6 +1,7 @@
import { STATIC_FOLDER_PLACEHOLDER } from './../constants/StaticFolderPlaceholder';
import { Questions } from './../helpers/Questions';
import {
SETTING_CONTENT_I18N,
SETTING_CONTENT_PAGE_FOLDERS,
SETTING_CONTENT_STATIC_FOLDER,
SETTING_CONTENT_SUPPORTED_FILETYPES,
@@ -9,7 +10,7 @@ import {
} from './../constants';
import { commands, Uri, workspace, window } from 'vscode';
import { basename, dirname, join, relative, sep } from 'path';
import { ContentFolder, FileInfo, FolderInfo, StaticFolder } from '../models';
import { ContentFolder, FileInfo, FolderInfo, I18nConfig, StaticFolder } from '../models';
import uniqBy = require('lodash.uniqby');
import { Template } from './Template';
import { Notifications } from '../helpers/Notifications';
@@ -288,67 +289,34 @@ export class Folders {
const folderInfo: FolderInfo[] = [];
for (const folder of folders) {
try {
const folderPath = parseWinPath(folder.path);
const crntFolderInfo = await Folders.getFilesByFolder(folder, supportedFiles, limit);
if (crntFolderInfo) {
folderInfo.push(crntFolderInfo);
}
if (typeof folderPath === 'string') {
let files: Uri[] = [];
// Process localization folders
if (folder.defaultLocale) {
const i18nConfig = folder.locales || Settings.get<I18nConfig[]>(SETTING_CONTENT_I18N);
if (i18nConfig) {
for (const i18n of i18nConfig) {
if (i18n.locale !== folder.defaultLocale && i18n.path) {
const i18nFolder = {
...folder,
path: join(folder.path, i18n.path),
title: `${folder.title} (${i18n.title})`
} as ContentFolder;
for (const fileType of supportedFiles || DEFAULT_FILE_TYPES) {
let filePath = join(
folderPath,
folder.excludeSubdir ? '/' : '**',
`*${fileType.startsWith('.') ? '' : '.'}${fileType}`
);
if (folderPath === '' && folder.excludeSubdir) {
filePath = `*${fileType.startsWith('.') ? '' : '.'}${fileType}`;
}
let foundFiles = await Folders.findFiles(filePath);
// Make sure these file are coming from the folder path (this could be an issue in multi-root workspaces)
foundFiles = foundFiles.filter((f) => parseWinPath(f.fsPath).startsWith(folderPath));
files = [...files, ...foundFiles];
}
if (files) {
let fileStats: FileInfo[] = [];
for (const file of files) {
try {
const fileName = basename(file.fsPath);
const folderName = dirname(file.fsPath).split(sep).pop();
const stats = await workspace.fs.stat(file);
fileStats.push({
filePath: file.fsPath,
fileName,
folderName,
...stats
});
} catch (error) {
// Skip the file
const crntFolderInfo = await Folders.getFilesByFolder(
i18nFolder,
supportedFiles,
limit
);
if (crntFolderInfo) {
folderInfo.push(crntFolderInfo);
}
}
fileStats = fileStats.sort((a, b) => b.mtime - a.mtime);
if (limit) {
fileStats = fileStats.slice(0, limit);
}
folderInfo.push({
title: folder.title,
files: files.length,
lastModified: fileStats
});
}
}
} catch (e) {
// Skip the current folder
}
}
@@ -603,6 +571,97 @@ export class Folders {
return;
}
/**
* Retrieves the page folder that matches the given file path.
*
* @param filePath - The file path to match against the page folders.
* @returns The page folder that matches the file path, or undefined if no match is found.
*/
public static getPageFolderByFilePath(filePath: string): ContentFolder | undefined {
const folders = Folders.get();
const parsedPath = parseWinPath(filePath);
const pageFolderMatches = folders
.filter((folder) => parsedPath && folder.path && parsedPath.includes(folder.path))
.sort((a, b) => b.path.length - a.path.length);
if (pageFolderMatches.length > 0 && pageFolderMatches[0]) {
return pageFolderMatches[0];
}
return;
}
private static async getFilesByFolder(
folder: ContentFolder,
supportedFiles: string[] | undefined,
limit?: number
): Promise<FolderInfo | undefined> {
try {
const folderPath = parseWinPath(folder.path);
if (typeof folderPath === 'string') {
let files: Uri[] = [];
for (const fileType of supportedFiles || DEFAULT_FILE_TYPES) {
let filePath = join(
folderPath,
folder.excludeSubdir ? '/' : '**',
`*${fileType.startsWith('.') ? '' : '.'}${fileType}`
);
if (folderPath === '' && folder.excludeSubdir) {
filePath = `*${fileType.startsWith('.') ? '' : '.'}${fileType}`;
}
let foundFiles = await Folders.findFiles(filePath);
// Make sure these file are coming from the folder path (this could be an issue in multi-root workspaces)
foundFiles = foundFiles.filter((f) => parseWinPath(f.fsPath).startsWith(folderPath));
files = [...files, ...foundFiles];
}
if (files) {
let fileStats: FileInfo[] = [];
for (const file of files) {
try {
const fileName = basename(file.fsPath);
const folderName = dirname(file.fsPath).split(sep).pop();
const stats = await workspace.fs.stat(file);
fileStats.push({
filePath: file.fsPath,
fileName,
folderName,
...stats
});
} catch (error) {
// Skip the file
}
}
fileStats = fileStats.sort((a, b) => b.mtime - a.mtime);
if (limit) {
fileStats = fileStats.slice(0, limit);
}
return {
title: folder.title,
files: files.length,
lastModified: fileStats
};
}
}
} catch (e) {
// Skip the current folder
}
return;
}
/**
* Retrieve all content folders
* @param pattern
+6
View File
@@ -19,6 +19,7 @@ import { Field } from '../models';
import { Preview } from './Preview';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../localization';
import { i18n } from './i18n';
export class StatusListener {
/**
@@ -42,6 +43,10 @@ export class StatusListener {
try {
commands.executeCommand('setContext', CONTEXT.isValidFile, true);
// Check i18n
const isI18nDefault = await i18n.isDefaultLanguage(document.uri.fsPath);
commands.executeCommand('setContext', CONTEXT.isI18nDefault, isI18nDefault);
const article = editor
? ArticleHelper.getFrontMatter(editor)
: await ArticleHelper.getFrontMatterByPath(document.uri.fsPath);
@@ -83,6 +88,7 @@ export class StatusListener {
}
} else {
commands.executeCommand('setContext', CONTEXT.isValidFile, false);
commands.executeCommand('setContext', CONTEXT.isI18nDefault, false);
const panel = PanelProvider.getInstance();
if (panel && panel.visible) {
+310 -18
View File
@@ -1,43 +1,233 @@
import { commands, window } from 'vscode';
import { ArticleHelper, Extension, Notifications, Settings } from '../helpers';
import { Uri, commands, window, workspace } from 'vscode';
import {
ArticleHelper,
ContentType,
Extension,
FrameworkDetector,
Notifications,
Settings,
openFileInEditor,
parseWinPath
} from '../helpers';
import { COMMAND_NAME, SETTING_CONTENT_I18N } from '../constants';
import { I18nConfig } from '../models';
import { ContentFolder, Field, I18nConfig, ContentType as IContentType } from '../models';
import { join, parse } from 'path';
import { existsAsync } from '../utils';
import { Folders } from '.';
import { ParsedFrontMatter } from '../parsers';
// TODO:
// Allow sponsors to automatically translate the content
// Support page bundles
// Filter on locale
// Locale settings on the page folder level and global level
// Show the i18n content -> if default locale is in subfolder, the other content is not found
// Update the page folder setting to include the locales property (use #ref)
export class i18n {
/**
* Registers the i18n commands.
*/
public static register() {
const subscriptions = Extension.getInstance().subscriptions;
subscriptions.push(commands.registerCommand(COMMAND_NAME.i18n.create, i18n.create));
}
public static getSettings(): I18nConfig[] | undefined {
const i18nSettings = Settings.get<I18nConfig[]>(SETTING_CONTENT_I18N);
/**
* Retrieves the I18nConfig settings from the application.
* @returns An array of I18nConfig objects if settings are found, otherwise undefined.
*/
public static async getSettings(filePath: string): Promise<I18nConfig[] | undefined> {
if (!filePath) {
return;
}
const i18nSettings = Settings.get<I18nConfig[]>(SETTING_CONTENT_I18N);
let pageFolder = Folders.getPageFolderByFilePath(filePath);
if (!pageFolder) {
const folders = Folders.get();
const localeFolders = folders?.filter((folder) => folder.defaultLocale);
if (!localeFolders) {
return;
}
const fileName = parse(filePath).base;
for (const folder of localeFolders) {
const defaultFile = join(folder.path, fileName);
if (await existsAsync(defaultFile)) {
pageFolder = folder;
break;
}
}
}
if (!pageFolder || !pageFolder.locales) {
return i18nSettings;
}
return pageFolder.locales;
}
/**
* Checks if the given file path corresponds to the default language.
* @param filePath - The file path to check.
* @returns True if the file path corresponds to the default language, false otherwise.
*/
public static async isDefaultLanguage(filePath: string): Promise<boolean> {
const i18nSettings = await i18n.getSettings(filePath);
if (!i18nSettings) {
return false;
}
const pageFolder = Folders.getPageFolderByFilePath(filePath);
if (!pageFolder || !pageFolder.defaultLocale) {
return false;
}
const fileInfo = parse(filePath);
const dir = fileInfo.dir;
if (pageFolder.path) {
return parseWinPath(dir).toLowerCase() === parseWinPath(pageFolder.path).toLowerCase();
}
return false;
}
/**
* Retrieves the I18nConfig for a given file path.
* @param filePath - The path of the file.
* @returns The I18nConfig object if found, otherwise undefined.
*/
public static async getLocale(filePath: string): Promise<I18nConfig | undefined> {
const i18nSettings = await i18n.getSettings(filePath);
if (!i18nSettings) {
return;
}
return i18nSettings;
const pageFolder = Folders.getPageFolderByFilePath(filePath);
const fileInfo = parse(filePath);
if (pageFolder && pageFolder.defaultLocale) {
if (
pageFolder.path &&
parseWinPath(fileInfo.dir).toLowerCase() === parseWinPath(pageFolder.path).toLowerCase()
) {
return i18nSettings.find((i18n) => i18n.locale === pageFolder.defaultLocale);
}
}
const folders = Folders.get();
if (!folders) {
return;
}
const fileName = fileInfo.base;
const defaultLanguageFolders = folders.filter((folder) => folder.defaultLocale);
for (const folder of defaultLanguageFolders) {
for (const locale of i18nSettings) {
if (locale.path && folder.defaultLocale !== locale.locale) {
const translation = join(folder.path, locale.path, fileName);
if (parseWinPath(translation).toLowerCase() === parseWinPath(filePath).toLowerCase()) {
return locale;
}
}
}
}
return;
}
private static async create(filePath?: string) {
const i18nSettings = i18n.getSettings();
/**
* Retrieves translations for a given file path.
* @param filePath - The path of the file for which translations are requested.
* @returns A promise that resolves to an object containing translations for each locale, or undefined if i18n settings are not available.
*/
public static async getTranslations(
filePath: string
): Promise<{ [locale: string]: string } | undefined> {
const i18nSettings = await i18n.getSettings(filePath);
if (!i18nSettings) {
return;
}
const translations: { [locale: string]: string } = {};
const pageFolder = Folders.getPageFolderByFilePath(filePath);
if (pageFolder && pageFolder.defaultLocale) {
for (const i18n of i18nSettings) {
if (i18n.path) {
const translation = join(pageFolder.path, i18n.path, filePath);
if (await existsAsync(translation)) {
translations[i18n.locale] = translation;
}
}
}
return translations;
}
const folders = Folders.get();
if (!folders) {
return;
}
const fileName = parse(filePath).base;
const defaultLanguageFolders = folders.filter((folder) => folder.defaultLocale);
let defaultLanguageFolder: ContentFolder | undefined;
for (const folder of defaultLanguageFolders) {
const defaultFile = join(folder.path, fileName);
if (await existsAsync(defaultFile)) {
defaultLanguageFolder = folder;
break;
}
}
if (!defaultLanguageFolder) {
return translations;
}
for (const i18n of i18nSettings) {
const translation = join(defaultLanguageFolder.path, i18n.path || '', fileName);
if (await existsAsync(translation)) {
translations[i18n.locale] = translation;
}
}
return translations;
}
/**
* Creates a new content file for a specific locale based on the i18n configuration.
* If a file path is provided, the new content file will be created in the same directory.
* If no file path is provided, the active file in the editor will be used.
* @param filePath The path of the file where the new content file should be created.
*/
private static async create(fileUri?: Uri) {
if (!fileUri) {
const filePath = ArticleHelper.getActiveFile();
fileUri = filePath ? Uri.file(filePath) : undefined;
}
if (!fileUri) {
Notifications.warning('No file selected');
return;
}
const i18nSettings = await i18n.getSettings(fileUri.fsPath);
if (!i18nSettings) {
Notifications.warning('No i18n configuration found');
return;
}
if (!filePath) {
filePath = ArticleHelper.getActiveFile();
}
if (!filePath) {
Notifications.warning('No file selected');
const isDefaultLanguage = await i18n.isDefaultLanguage(fileUri.fsPath);
if (!isDefaultLanguage) {
Notifications.warning('The current file cannot be used for i18n content creation');
return;
}
const locale = await window.showQuickPick(
i18nSettings.map((i18n) => i18n.title || i18n.locale),
i18nSettings.filter((i18n) => i18n.path).map((i18n) => i18n.title || i18n.locale),
{
title: 'Create content for locale',
placeHolder: 'To which locale do you want to create a new content?',
@@ -52,12 +242,114 @@ export class i18n {
const selectedI18n = i18nSettings.find(
(i18n) => i18n.title === locale || i18n.locale === locale
);
if (!selectedI18n) {
if (!selectedI18n || !selectedI18n.path) {
Notifications.warning('No i18n configuration found');
return;
}
// TODO: start from a page, or use a path
Notifications.info('Not implemented yet');
let article = await ArticleHelper.getFrontMatterByPath(fileUri.fsPath);
if (!article) {
Notifications.warning('No content found');
return;
}
const contentType = ArticleHelper.getContentType(article);
if (!contentType) {
Notifications.warning('No content type found');
return;
}
// Get the directory of the file
const fileInfo = parse(fileUri.fsPath);
const i18nDir = join(fileInfo.dir, selectedI18n.path);
if (!(await existsAsync(i18nDir))) {
await workspace.fs.createDirectory(Uri.file(i18nDir));
}
article = await i18n.updateFrontMatter(
article,
fileUri.fsPath,
contentType,
selectedI18n,
i18nDir
);
const newFilePath = join(i18nDir, fileInfo.base);
if (await existsAsync(newFilePath)) {
Notifications.warning('File already exists');
return;
}
const newFileUri = Uri.file(newFilePath);
await workspace.fs.writeFile(
newFileUri,
Buffer.from(ArticleHelper.stringifyFrontMatter(article.content, article.data))
);
await openFileInEditor(newFilePath);
Notifications.info(`Created "${selectedI18n.title || selectedI18n.locale}" i18n content file`);
}
/**
* Updates the front matter of an article with internationalization (i18n) support.
*
* @param article - The parsed front matter of the article.
* @param filePath - The path of the file containing the front matter.
* @param contentType - The content type of the article.
* @param i18nConfig - The configuration for internationalization.
* @param i18nDir - The directory where the i18n files are located.
* @returns A Promise that resolves to the updated parsed front matter.
*/
private static async updateFrontMatter(
article: ParsedFrontMatter,
filePath: string,
contentType: IContentType,
i18nConfig: I18nConfig,
i18nDir: string
): Promise<ParsedFrontMatter> {
const imageFields = ContentType.findFieldsByTypeDeep(contentType.fields, 'image');
if (imageFields.length > 0) {
article.data = await i18n.processImageFields(article.data, filePath, imageFields, i18nDir);
}
return article;
}
/**
* Processes the image fields in the provided data object.
* Replaces the image field values with the relative path to the image file.
*
* @param data - The data object containing the field values.
* @param filePath - The absolute file path of the data object.
* @param fields - The array of field arrays to process.
* @param i18nDir - The directory path for internationalization.
* @returns The updated data object with image field values replaced by relative paths.
*/
private static async processImageFields(
data: { [key: string]: any },
filePath: string,
fields: Field[][],
i18nDir: string
) {
for (const field of fields) {
if (!field) {
continue;
}
for (const f of field) {
if (f.type === 'image') {
const value = data[f.name];
if (value) {
let imgPath = FrameworkDetector.getAbsPathByFile(value, filePath);
imgPath = FrameworkDetector.getRelPathByFileDir(imgPath, i18nDir);
data[f.name] = imgPath;
}
}
}
}
return data;
}
}
+2
View File
@@ -8,6 +8,8 @@ export const CONTEXT = {
isValidFile: 'frontMatter:file:isValid',
isDevelopment: 'frontMatter:isDevelopment',
isI18nDefault: 'frontMatter:i18n:default',
hasViewModes: 'frontMatter:has:modes',
isSnippetsDashboardEnabled: 'frontMatter:dashboard:snippets:enabled',
@@ -0,0 +1,22 @@
import * as React from 'react';
import { Page } from '../../models';
import { LanguageIcon } from '@heroicons/react/24/outline';
export interface II18nLabelProps {
page: Page;
}
export const I18nLabel: React.FunctionComponent<II18nLabelProps> = ({
page
}: React.PropsWithChildren<II18nLabelProps>) => {
if (!page.fmLocale) {
return null;
}
return (
<div className="mb-2 flex items-center">
<LanguageIcon className="mr-1 h-4 w-4 inline-block" />
<span className="text-xs">{page.fmLocale.title || page.fmLocale.locale}</span>
</div>
);
};
@@ -16,6 +16,7 @@ import { LocalizationKey } from '../../../localization';
import { useNavigate } from 'react-router-dom';
import { routePaths } from '../..';
import useCard from '../../hooks/useCard';
import { I18nLabel } from './I18nLabel';
export interface IItemProps extends Page { }
@@ -132,6 +133,8 @@ export const Item: React.FunctionComponent<IItemProps> = ({
onOpen={openFile}
/>
<I18nLabel page={pageData} />
<button
title={escapedTitle ? l10n.t(LocalizationKey.commonOpenWithValue, escapedTitle) : l10n.t(LocalizationKey.commonOpen)}
onClick={openFile}
@@ -140,25 +143,29 @@ export const Item: React.FunctionComponent<IItemProps> = ({
titleHtml ? (
<div dangerouslySetInnerHTML={{ __html: titleHtml }} />
) : (
<h2 className="mb-2 font-bold">
{escapedTitle}
<h2 className="font-bold">
<span>{escapedTitle}</span>
</h2>
)
}
</button>
<button
title={escapedTitle ? l10n.t(LocalizationKey.commonOpenWithValue, escapedTitle) : l10n.t(LocalizationKey.commonOpen)}
onClick={openFile}
className={`text-left block`}>
{
descriptionHtml ? (
<div dangerouslySetInnerHTML={{ __html: descriptionHtml }} />
) : (
<p className={`text-xs text-[vara(--vscode-titleBar-activeForeground)]`}>{escapedDescription}</p>
)
}
</button>
{
(escapedDescription || descriptionHtml) && (
<button
title={escapedTitle ? l10n.t(LocalizationKey.commonOpenWithValue, escapedTitle) : l10n.t(LocalizationKey.commonOpen)}
onClick={openFile}
className={`mt-2 text-left block`}>
{
descriptionHtml ? (
<div dangerouslySetInnerHTML={{ __html: descriptionHtml }} />
) : (
<p className={`text-xs text-[vara(--vscode-titleBar-activeForeground)]`}>{escapedDescription}</p>
)
}
</button>
)
}
{
tagsHtml ? (
+7
View File
@@ -1,3 +1,5 @@
import { I18nConfig } from '../../models';
export interface Page {
// Properties for caching
fmCachePath: string;
@@ -19,6 +21,11 @@ export interface Page {
fmContentType: string;
fmDateFormat: string | undefined;
// i18n fields
fmDefaultLocale?: boolean;
fmLocale?: I18nConfig;
fmTranslations?: { [locale: string]: string };
title: string;
slug: string;
date: string | Date;
+25 -17
View File
@@ -123,8 +123,14 @@ export class ArticleHelper {
* Retrieve the file's front matter by its path
* @param filePath
*/
public static async getFrontMatterByPath(filePath: string) {
const file = await readFileAsync(filePath, { encoding: 'utf-8' });
public static async getFrontMatterByPath(
filePath: string
): Promise<ParsedFrontMatter | undefined> {
const file = await ArticleHelper.getContents(filePath);
if (!file) {
return undefined;
}
const article = ArticleHelper.parseFile(file, filePath);
if (!article) {
return undefined;
@@ -136,6 +142,20 @@ export class ArticleHelper {
};
}
/**
* Reads the contents of a file asynchronously.
* @param filePath - The path of the file to read.
* @returns A promise that resolves to the contents of the file, or undefined if the file does not exist.
*/
public static async getContents(filePath: string): Promise<string | undefined> {
const file = await workspace.fs.readFile(Uri.file(parseWinPath(filePath)));
if (!file) {
return undefined;
}
return new TextDecoder().decode(file);
}
/**
* Store the new information in the file
*
@@ -370,21 +390,9 @@ export class ArticleHelper {
if (article.data.type) {
contentType = contentTypes.find((ct) => ct.name === article.data.type);
} else if (!contentType && article.path) {
// Get the content type by the folder name
let folders = Folders.get();
let parsedPath = parseWinPath(article.path);
let pageFolderMatches = folders.filter(
(folder) => parsedPath && folder.path && parsedPath.includes(folder.path)
);
// Sort by longest path
pageFolderMatches = pageFolderMatches.sort((a, b) => b.path.length - a.path.length);
if (
pageFolderMatches.length > 0 &&
pageFolderMatches[0].contentTypes &&
pageFolderMatches[0].contentTypes.length === 1
) {
const contentTypeName = pageFolderMatches[0].contentTypes[0];
const pageFolder = Folders.getPageFolderByFilePath(article.path);
if (pageFolder && pageFolder.contentTypes?.length === 1) {
const contentTypeName = pageFolder.contentTypes[0];
contentType = contentTypes.find((ct) => ct.name === contentTypeName);
}
}
+54
View File
@@ -151,6 +151,60 @@ export class FrameworkDetector {
return parseWinPath(relAssetPath);
}
/**
* Returns the absolute path by combining the relative path and the file path.
* If a static folder is configured, it will be taken into account.
* @param relAssetPath The relative path.
* @param filePath The file path.
* @returns The absolute path.
*/
public static getAbsPathByFile(relAssetPath: string, filePath: string): string {
const staticFolderValue = Settings.get<string | StaticFolder>(SETTING_CONTENT_STATIC_FOLDER);
const staticFolder = Folders.getStaticFolderRelativePath();
if (
staticFolderValue &&
staticFolder &&
typeof staticFolderValue !== 'string' &&
staticFolderValue.relative
) {
const fileDir = dirname(filePath);
return parseWinPath(join(fileDir, relAssetPath));
}
return relAssetPath;
}
/**
* Returns the relative path of an asset file based on the provided absolute asset path and file path.
* If the static folder setting is configured and the static folder is available, the relative path is calculated based on the file and asset directories.
* Otherwise, the absolute asset path is returned as is.
*
* @param absAssetPath The absolute path of the asset file.
* @param fileDir The path of the directory
* @returns The relative path of the asset file.
*/
public static getRelPathByFileDir(absAssetPath: string, fileDir: string): string {
const staticFolderValue = Settings.get<string | StaticFolder>(SETTING_CONTENT_STATIC_FOLDER);
const staticFolder = Folders.getStaticFolderRelativePath();
if (
staticFolderValue &&
staticFolder &&
typeof staticFolderValue !== 'string' &&
staticFolderValue.relative
) {
const assetDir = dirname(absAssetPath);
const fileName = parse(absAssetPath);
let relAssetPath = relative(fileDir, assetDir);
relAssetPath = join(relAssetPath, `${fileName.name}${fileName.ext}`);
return parseWinPath(relAssetPath);
}
return absAssetPath;
}
/**
* Define the default settings for Hexo
*/
+4
View File
@@ -1,3 +1,5 @@
import { I18nConfig } from './i18nConfig';
export interface ContentFolder {
title: string;
path: string;
@@ -10,4 +12,6 @@ export interface ContentFolder {
originalPath?: string;
$schema?: string;
extended?: boolean;
defaultLocale?: string;
locales: I18nConfig[];
}
+2 -2
View File
@@ -1,5 +1,5 @@
export interface I18nConfig {
title?: string;
locale: string;
path: string;
title?: string;
path?: string;
}
+9
View File
@@ -1,3 +1,4 @@
import { i18n } from './../commands/i18n';
import { STATIC_FOLDER_PLACEHOLDER } from './../constants/StaticFolderPlaceholder';
import { parseWinPath } from './../helpers/parseWinPath';
import { dirname, extname, join } from 'path';
@@ -209,6 +210,10 @@ export class PagesParser {
escapedDescription = '<invalid title>';
}
const isDefaultLanguage = await i18n.isDefaultLanguage(filePath);
const locale = await i18n.getLocale(filePath);
const translations = await i18n.getTranslations(filePath);
const page: Page = {
...article.data,
// Cache properties
@@ -230,6 +235,10 @@ export class PagesParser {
fmContentType: contentType.name || DEFAULT_CONTENT_TYPE_NAME,
fmBody: article?.content || '',
fmDateFormat: dateFormat,
// i18n properties
fmDefaultLocale: isDefaultLanguage,
fmLocale: locale,
fmTranslations: translations,
// Make sure these are always set
title: escapedTitle,
description: escapedDescription,