Merge branch 'azure-translator' into dev

This commit is contained in:
Elio Struyf
2024-02-22 15:28:36 +01:00
6 changed files with 256 additions and 68 deletions
+8 -2
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",
+16 -39
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: {
@@ -380,12 +380,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,
@@ -393,42 +387,25 @@ 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',
Accept: 'application/json'
},
body
});
if (!response.ok) {
throw new Error(`DeepL: ${response.statusText}`);
if (!translations || translations.length < 3) {
throw new Error('Invalid response');
}
const data = await response.json();
if (!data.translations || data.translations.length < 3) {
throw new Error('DeepL: Invalid response');
}
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 : '';
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}`);
}
+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`
}
}
};
@@ -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>
+22 -14
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
*/
@@ -2156,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
*/
+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}`);
}
}
}