#756: Support for creating translation from any page + change path logic

This commit is contained in:
Elio Struyf
2024-02-22 09:52:59 +01:00
parent 03bc7e72fd
commit 241e660694
10 changed files with 144 additions and 65 deletions
+1
View File
@@ -7,6 +7,7 @@
- [#731](https://github.com/estruyf/vscode-front-matter/issues/731): Added the ability to map/unmap taxonomy to multiple pages at once
- [#746](https://github.com/estruyf/vscode-front-matter/issues/746): Placeholder support added to to the `slug` field
- [#749](https://github.com/estruyf/vscode-front-matter/issues/749): Ability to set your own filters on the content dashboard with the `frontMatter.content.filters` setting
- [[#756](https://github.com/estruyf/vscode-front-matter/issues/756): i18n/multilingual content support
### 🎨 Enhancements
+3 -1
View File
@@ -539,7 +539,9 @@
"commands.i18n.create.warning.noFile": "The file could not be retrieved.",
"commands.i18n.create.warning.noContentType": "Content type could not be retrieved for the current file.",
"commands.i18n.create.warning.noConfig": "No i18n configuration found.",
"commands.i18n.create.warning.notDefaultLocale": "The current file cannot be used for i18n content creation.",
"commands.i18n.create.error.noLocaleDefinition": "Could not retrieve the locale for the current file.",
"commands.i18n.create.error.noLocales": "Current file has been translated to all available languages.",
"commands.i18n.create.error.noContentFolder": "Could not define a content folder for the current file.",
"commands.i18n.create.error.fileExists": "The i18n translation already exists.",
"commands.i18n.create.success.created": "Created \"{0}\" i18n content file.",
"commands.i18n.create.quickPick.title": "Create content for locale",
+4 -3
View File
@@ -347,7 +347,8 @@
},
"additionalProperties": false,
"required": [
"locale"
"locale",
"path"
]
},
"scope": "Content"
@@ -2371,7 +2372,7 @@
{
"command": "frontMatter.i18n.create",
"group": "navigation@-127",
"when": "frontMatter:file:isValid && frontMatter:i18n:default"
"when": "frontMatter:file:isValid && frontMatter:i18n:enabled"
},
{
"command": "frontMatter.markup.options",
@@ -2474,7 +2475,7 @@
},
{
"command": "frontMatter.i18n.create",
"when": "frontMatter:i18n:default"
"when": "frontMatter:i18n:enabled"
},
{
"command": "frontMatter.collapseSections",
+50 -31
View File
@@ -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} (${i18n.title})`,
locale: i18n.locale,
localeSourcePath: sourcePath,
path: join(folderPath, i18n.path)
});
}
}
}
const defaultTitle = defaultLocale?.title
? `${folder.title} (${defaultLocale.title})`
: folder.title;
contentFolders.push({
...folder,
title: defaultTitle,
locale: folder.defaultLocale,
originalPath: folder.path,
localeSourcePath: sourcePath,
path: join(folderPath, defaultLocale?.path || '')
});
contentFolders.push(...localeFolders);
} else {
contentFolders.push({
...folder,
locale: folder.defaultLocale,
originalPath: folder.path,
path: folderPath
});
}
});
return contentFolders.filter((folder) => folder !== null) as ContentFolder[];
+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) {
+68 -23
View File
@@ -65,6 +65,25 @@ export class i18n {
return pageFolder.locales;
}
/**
* Checks if the locale is enabled for the given file path.
* @param filePath - The file path to check.
* @returns A promise that resolves to a boolean indicating whether the locale is enabled or not.
*/
public static async isLocaleEnabled(filePath: string): Promise<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 +103,10 @@ export class i18n {
const fileInfo = await i18n.getFileInfo(filePath);
if (pageFolder.path) {
if (pageFolder.locale) {
return pageFolder.locale === pageFolder.defaultLocale;
}
let pageFolderPath = parseWinPath(pageFolder.path);
if (!pageFolderPath.endsWith('/')) {
pageFolderPath += '/';
@@ -120,9 +143,10 @@ export class i18n {
if (
pageFolder.path &&
pageFolder.locale &&
parseWinPath(fileInfo.dir).toLowerCase() === parseWinPath(pageFolderPath).toLowerCase()
) {
return i18nSettings.find((i18n) => i18n.locale === pageFolder?.defaultLocale);
return i18nSettings.find((i18n) => i18n.locale === pageFolder?.locale);
}
}
@@ -172,9 +196,9 @@ export class i18n {
let pageFolder = Folders.getPageFolderByFilePath(filePath);
const fileInfo = await i18n.getFileInfo(filePath);
if (pageFolder && pageFolder.defaultLocale) {
if (pageFolder && pageFolder.defaultLocale && pageFolder.localeSourcePath) {
for (const i18n of i18nSettings) {
const translation = join(pageFolder.path, i18n.path || '', fileInfo.filename);
const translation = join(pageFolder.localeSourcePath, i18n.path || '', fileInfo.filename);
if (await existsAsync(translation)) {
translations[i18n.locale] = {
locale: i18n,
@@ -224,20 +248,40 @@ export class i18n {
fileUri = Uri.file(fileUri);
}
const pageFolder = Folders.getPageFolderByFilePath(fileUri.fsPath);
if (!pageFolder || !pageFolder.localeSourcePath) {
Notifications.error(l10n.t(LocalizationKey.commandsI18nCreateErrorNoContentFolder));
return;
}
const i18nSettings = await i18n.getSettings(fileUri.fsPath);
if (!i18nSettings) {
Notifications.warning(l10n.t(LocalizationKey.commandsI18nCreateWarningNoConfig));
return;
}
const isDefaultLanguage = await i18n.isDefaultLanguage(fileUri.fsPath);
if (!isDefaultLanguage) {
Notifications.warning(l10n.t(LocalizationKey.commandsI18nCreateWarningNotDefaultLocale));
const sourceLocale = await i18n.getLocale(fileUri.fsPath);
if (!sourceLocale || !sourceLocale.locale) {
Notifications.warning(l10n.t(LocalizationKey.commandsI18nCreateErrorNoLocaleDefinition));
return;
}
const translations = (await i18n.getTranslations(fileUri.fsPath)) || {};
const targetLocales = i18nSettings.filter((i18nSetting) => {
return (
i18nSetting.path &&
i18nSetting.locale !== sourceLocale.locale &&
!translations[i18nSetting.locale]
);
});
if (targetLocales.length === 0) {
Notifications.warning(l10n.t(LocalizationKey.commandsI18nCreateErrorNoLocales));
return;
}
const locale = await window.showQuickPick(
i18nSettings.filter((i18n) => i18n.path).map((i18n) => i18n.title || i18n.locale),
targetLocales.map((i18n) => i18n.title || i18n.locale),
{
title: l10n.t(LocalizationKey.commandsI18nCreateQuickPickTitle),
placeHolder: l10n.t(LocalizationKey.commandsI18nCreateQuickPickPlaceHolder),
@@ -249,10 +293,10 @@ export class i18n {
return;
}
const selectedI18n = i18nSettings.find(
const targetLocale = i18nSettings.find(
(i18n) => i18n.title === locale || i18n.locale === locale
);
if (!selectedI18n || !selectedI18n.path) {
if (!targetLocale || !targetLocale.path) {
Notifications.warning(l10n.t(LocalizationKey.commandsI18nCreateWarningNoConfig));
return;
}
@@ -280,7 +324,7 @@ export class i18n {
pageBundleDir = join(parse(pageBundleDir).dir);
}
const i18nDir = join(dir, selectedI18n.path, pageBundleDir);
const i18nDir = join(pageFolder.localeSourcePath, targetLocale.path, pageBundleDir);
if (!(await existsAsync(i18nDir))) {
await workspace.fs.createDirectory(Uri.file(i18nDir));
@@ -290,7 +334,8 @@ export class i18n {
article,
fileUri.fsPath,
contentType,
selectedI18n,
sourceLocale,
targetLocale,
i18nDir
);
@@ -300,9 +345,8 @@ export class i18n {
return;
}
const sourceLocale = await i18n.getLocale(fileUri.fsPath);
if (sourceLocale?.locale) {
article = await i18n.translate(article, sourceLocale, selectedI18n);
article = await i18n.translate(article, sourceLocale, targetLocale);
}
const newFileUri = Uri.file(newFilePath);
@@ -318,7 +362,7 @@ export class i18n {
Notifications.info(
l10n.t(
LocalizationKey.commandsI18nCreateSuccessCreated,
selectedI18n.title || selectedI18n.locale
sourceLocale.title || sourceLocale.locale
)
);
}
@@ -349,9 +393,9 @@ export class i18n {
cancellable: false
},
async () => {
const title = article.data.title;
const description = article.data.description;
const content = article.content;
const title = article.data.title || '';
const description = article.data.description || '';
const content = article.content || '';
try {
const body = JSON.stringify({
@@ -368,7 +412,6 @@ export class i18n {
Authorization: `DeepL-Auth-Key ${authKey}`,
'User-Agent': `FrontMatterCMS/${Extension.getInstance().version}`,
'Content-Type': 'application/json',
'content-length': body.length.toString(),
Accept: 'application/json'
},
body
@@ -383,9 +426,9 @@ export class i18n {
throw new Error('DeepL: Invalid response');
}
article.data.title = data.translations[0].text;
article.data.description = data.translations[1].text;
article.content = data.translations[2].text;
article.data.title = article.data.title ? data.translations[0].text : '';
article.data.description = article.data.description ? data.translations[1].text : '';
article.content = article.content ? data.translations[2].text : '';
} catch (error) {
Notifications.error(`${(error as Error).message}`);
}
@@ -460,7 +503,8 @@ export class i18n {
* @param article - The parsed front matter of the article.
* @param filePath - The path of the file containing the front matter.
* @param contentType - The content type of the article.
* @param i18nConfig - The configuration for internationalization.
* @param sourceLocale - The source locale.
* @param targetLocale - The target locale.
* @param i18nDir - The directory where the i18n files are located.
* @returns A Promise that resolves to the updated parsed front matter.
*/
@@ -468,7 +512,8 @@ export class i18n {
article: ParsedFrontMatter,
filePath: string,
contentType: IContentType,
i18nConfig: I18nConfig,
sourceLocale: I18nConfig,
targetLocale: I18nConfig,
i18nDir: string
): Promise<ParsedFrontMatter> {
const imageFields = ContentType.findFieldsByTypeDeep(contentType.fields, 'image');
+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',
@@ -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>
+10 -2
View File
@@ -1725,9 +1725,17 @@ export enum LocalizationKey {
*/
commandsI18nCreateWarningNoConfig = 'commands.i18n.create.warning.noConfig',
/**
* The current file cannot be used for i18n content creation.
* Could not retrieve the locale for the current file.
*/
commandsI18nCreateWarningNotDefaultLocale = 'commands.i18n.create.warning.notDefaultLocale',
commandsI18nCreateErrorNoLocaleDefinition = 'commands.i18n.create.error.noLocaleDefinition',
/**
* Current file has been translated to all available languages.
*/
commandsI18nCreateErrorNoLocales = 'commands.i18n.create.error.noLocales',
/**
* Could not define a content folder for the current file.
*/
commandsI18nCreateErrorNoContentFolder = 'commands.i18n.create.error.noContentFolder',
/**
* The i18n translation already exists.
*/
+3
View File
@@ -12,6 +12,9 @@ export interface ContentFolder {
originalPath?: string;
$schema?: string;
extended?: boolean;
locale?: string;
localeSourcePath?: string;
defaultLocale?: string;
locales: I18nConfig[];
}