mirror of
https://github.com/strapi/strapi.git
synced 2025-09-26 08:52:26 +00:00
Merge branch 'features/deits' into deits/ts-workflow
This commit is contained in:
commit
897bb4a87c
@ -31,7 +31,7 @@
|
|||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
"setup": "yarn && yarn clean && yarn build:ts && yarn build",
|
"setup": "yarn && yarn clean && yarn build:ts && yarn build",
|
||||||
"clean": "lerna run --stream clean --no-private",
|
"clean": "lerna run --stream clean --no-private",
|
||||||
"watch": "lerna run --stream watch --no-private",
|
"watch": "lerna run --stream watch --no-private --parallel",
|
||||||
"build": "lerna run --stream build --no-private",
|
"build": "lerna run --stream build --no-private",
|
||||||
"build:ts": "lerna run --stream build:ts --no-private",
|
"build:ts": "lerna run --stream build:ts --no-private",
|
||||||
"generate": "plop --plopfile ./packages/generators/admin/plopfile.js",
|
"generate": "plop --plopfile ./packages/generators/admin/plopfile.js",
|
||||||
|
@ -458,7 +458,7 @@ const ProfilePage = () => {
|
|||||||
href="https://docs.strapi.io/developer-docs/latest/development/admin-customization.html#locales"
|
href="https://docs.strapi.io/developer-docs/latest/development/admin-customization.html#locales"
|
||||||
>
|
>
|
||||||
{formatMessage({
|
{formatMessage({
|
||||||
id: 'Settings.profile.form.section.experience.documentation',
|
id: 'Settings.profile.form.section.experience.here',
|
||||||
defaultMessage: 'here',
|
defaultMessage: 'here',
|
||||||
})}
|
})}
|
||||||
</DocumentationLink>
|
</DocumentationLink>
|
||||||
|
@ -125,11 +125,10 @@
|
|||||||
"Settings.permissions.users.tabs.label": "Permisos de pestanyes",
|
"Settings.permissions.users.tabs.label": "Permisos de pestanyes",
|
||||||
"Settings.profile.form.notify.data.loaded": "S'han carregat les dades del vostre perfil",
|
"Settings.profile.form.notify.data.loaded": "S'han carregat les dades del vostre perfil",
|
||||||
"Settings.profile.form.section.experience.clear.select": "Esborrar l'idioma d'interfície seleccionat",
|
"Settings.profile.form.section.experience.clear.select": "Esborrar l'idioma d'interfície seleccionat",
|
||||||
"Settings.profile.form.section.experience.documentation": "documentació",
|
"Settings.profile.form.section.experience.here": "documentació",
|
||||||
"Settings.profile.form.section.experience.here": "aquí",
|
|
||||||
"Settings.profile.form.section.experience.interfaceLanguage": "Idioma d'interfície",
|
"Settings.profile.form.section.experience.interfaceLanguage": "Idioma d'interfície",
|
||||||
"Settings.profile.form.section.experience.interfaceLanguage.hint": "Això només mostrarà la vostra pròpia interfície en l'idioma escollit.",
|
"Settings.profile.form.section.experience.interfaceLanguage.hint": "Això només mostrarà la vostra pròpia interfície en l'idioma escollit.",
|
||||||
"Settings.profile.form.section.experience.interfaceLanguageHelp": "La selecció canviarà l'idioma de la interfície només per a vosaltres. Consulteu aquesta {documentation} perquè altres idiomes estiguin disponibles per al vostre ordinador.",
|
"Settings.profile.form.section.experience.interfaceLanguageHelp": "La selecció canviarà l'idioma de la interfície només per a vosaltres. Consulteu aquesta {here} perquè altres idiomes estiguin disponibles per al vostre ordinador.",
|
||||||
"Settings.profile.form.section.experience.mode.hint": "Mostra la vostra interfície en el mode escollit.",
|
"Settings.profile.form.section.experience.mode.hint": "Mostra la vostra interfície en el mode escollit.",
|
||||||
"Settings.profile.form.section.experience.mode.label": "Mode d'interfície",
|
"Settings.profile.form.section.experience.mode.label": "Mode d'interfície",
|
||||||
"Settings.profile.form.section.experience.mode.option-label": "mode {nom}",
|
"Settings.profile.form.section.experience.mode.option-label": "mode {nom}",
|
||||||
|
@ -123,10 +123,10 @@
|
|||||||
"Settings.permissions.users.tabs.label": "Tabs Tilladelser",
|
"Settings.permissions.users.tabs.label": "Tabs Tilladelser",
|
||||||
"Settings.profile.form.notify.data.loaded": "Dine profildata er blevet hentet",
|
"Settings.profile.form.notify.data.loaded": "Dine profildata er blevet hentet",
|
||||||
"Settings.profile.form.section.experience.clear.select": "Nulstil det valgte interface sprog",
|
"Settings.profile.form.section.experience.clear.select": "Nulstil det valgte interface sprog",
|
||||||
"Settings.profile.form.section.experience.documentation": "dokumentation",
|
"Settings.profile.form.section.experience.here": "dokumentation",
|
||||||
"Settings.profile.form.section.experience.interfaceLanguage": "Interface sprog",
|
"Settings.profile.form.section.experience.interfaceLanguage": "Interface sprog",
|
||||||
"Settings.profile.form.section.experience.interfaceLanguage.hint": "Dette vil kun vise dit eget interface i det valgte sprog.",
|
"Settings.profile.form.section.experience.interfaceLanguage.hint": "Dette vil kun vise dit eget interface i det valgte sprog.",
|
||||||
"Settings.profile.form.section.experience.interfaceLanguageHelp": "Valget vil kun ændre sproget for dig. Referér venligst til dette {documentation} for at gøre andre sprog tilgængelige for dit hold.",
|
"Settings.profile.form.section.experience.interfaceLanguageHelp": "Valget vil kun ændre sproget for dig. Referér venligst til dette {here} for at gøre andre sprog tilgængelige for dit hold.",
|
||||||
"Settings.profile.form.section.experience.title": "Oplevelse",
|
"Settings.profile.form.section.experience.title": "Oplevelse",
|
||||||
"Settings.profile.form.section.helmet.title": "Bruger profil",
|
"Settings.profile.form.section.helmet.title": "Bruger profil",
|
||||||
"Settings.profile.form.section.profile.page.title": "Profil side",
|
"Settings.profile.form.section.profile.page.title": "Profil side",
|
||||||
|
@ -123,10 +123,10 @@
|
|||||||
"Settings.permissions.users.tabs.label": "Permisos de pestañas",
|
"Settings.permissions.users.tabs.label": "Permisos de pestañas",
|
||||||
"Settings.profile.form.notify.data.loaded": "Se han cargado los datos de tu perfil",
|
"Settings.profile.form.notify.data.loaded": "Se han cargado los datos de tu perfil",
|
||||||
"Settings.profile.form.section.experience.clear.select": "Borrar el idioma de interfaz seleccionado",
|
"Settings.profile.form.section.experience.clear.select": "Borrar el idioma de interfaz seleccionado",
|
||||||
"Settings.profile.form.section.experience.documentation": "documentación",
|
"Settings.profile.form.section.experience.here": "documentación",
|
||||||
"Settings.profile.form.section.experience.interfaceLanguage": "Idioma de interfaz",
|
"Settings.profile.form.section.experience.interfaceLanguage": "Idioma de interfaz",
|
||||||
"Settings.profile.form.section.experience.interfaceLanguage.hint": "Esto solo mostrará su propia interfaz en el idioma elegido.",
|
"Settings.profile.form.section.experience.interfaceLanguage.hint": "Esto solo mostrará su propia interfaz en el idioma elegido.",
|
||||||
"Settings.profile.form.section.experience.interfaceLanguageHelp": "La selección cambiará el idioma de la interfaz solo para usted. Consulte esta {documentation} para que otros idiomas estén disponibles para su equipo.",
|
"Settings.profile.form.section.experience.interfaceLanguageHelp": "La selección cambiará el idioma de la interfaz solo para usted. Consulte esta {here} para que otros idiomas estén disponibles para su equipo.",
|
||||||
"Settings.profile.form.section.experience.title": "Experiencia",
|
"Settings.profile.form.section.experience.title": "Experiencia",
|
||||||
"Settings.profile.form.section.helmet.title": "Perfil de usuario",
|
"Settings.profile.form.section.helmet.title": "Perfil de usuario",
|
||||||
"Settings.profile.form.section.profile.page.title": "Página de perfil",
|
"Settings.profile.form.section.profile.page.title": "Página de perfil",
|
||||||
|
@ -123,10 +123,10 @@
|
|||||||
"Settings.permissions.users.tabs.label": "Onglet Autorisations",
|
"Settings.permissions.users.tabs.label": "Onglet Autorisations",
|
||||||
"Settings.profile.form.notify.data.loaded": "Les données de votre profil ont été chargées",
|
"Settings.profile.form.notify.data.loaded": "Les données de votre profil ont été chargées",
|
||||||
"Settings.profile.form.section.experience.clear.select": "Vider la langue de l'interface sélectionnée",
|
"Settings.profile.form.section.experience.clear.select": "Vider la langue de l'interface sélectionnée",
|
||||||
"Settings.profile.form.section.experience.documentation": "documentation",
|
"Settings.profile.form.section.experience.here": "documentation",
|
||||||
"Settings.profile.form.section.experience.interfaceLanguage": "Langue de l'interface",
|
"Settings.profile.form.section.experience.interfaceLanguage": "Langue de l'interface",
|
||||||
"Settings.profile.form.section.experience.interfaceLanguage.hint": "Cela affichera seulement votre propre interface dans la langue sélectionnée",
|
"Settings.profile.form.section.experience.interfaceLanguage.hint": "Cela affichera seulement votre propre interface dans la langue sélectionnée",
|
||||||
"Settings.profile.form.section.experience.interfaceLanguageHelp": "La sélection changera la langue de l'interface uniquement pour vous. Veuillez vous référer à cette {documentation} pour rendre d'autres langues disponibles pour votre équipe.",
|
"Settings.profile.form.section.experience.interfaceLanguageHelp": "La sélection changera la langue de l'interface uniquement pour vous. Veuillez vous référer à cette {here} pour rendre d'autres langues disponibles pour votre équipe.",
|
||||||
"Settings.profile.form.section.experience.title": "Expérience",
|
"Settings.profile.form.section.experience.title": "Expérience",
|
||||||
"Settings.profile.form.section.helmet.title": "Profil utilisateur",
|
"Settings.profile.form.section.helmet.title": "Profil utilisateur",
|
||||||
"Settings.profile.form.section.profile.page.title": "Page de profil",
|
"Settings.profile.form.section.profile.page.title": "Page de profil",
|
||||||
|
@ -123,10 +123,10 @@
|
|||||||
"Settings.permissions.users.tabs.label": "Hozzáférések Tab",
|
"Settings.permissions.users.tabs.label": "Hozzáférések Tab",
|
||||||
"Settings.profile.form.notify.data.loaded": "Profiladatok betöltve",
|
"Settings.profile.form.notify.data.loaded": "Profiladatok betöltve",
|
||||||
"Settings.profile.form.section.experience.clear.select": "A kiválasztott felület nyelvének törlése",
|
"Settings.profile.form.section.experience.clear.select": "A kiválasztott felület nyelvének törlése",
|
||||||
"Settings.profile.form.section.experience.documentation": "dokumentáció",
|
"Settings.profile.form.section.experience.here": "dokumentáció",
|
||||||
"Settings.profile.form.section.experience.interfaceLanguage": "A felület nyelve",
|
"Settings.profile.form.section.experience.interfaceLanguage": "A felület nyelve",
|
||||||
"Settings.profile.form.section.experience.interfaceLanguage.hint": "Ez csak a saját felületét jeleníti meg a kiválasztott nyelven.",
|
"Settings.profile.form.section.experience.interfaceLanguage.hint": "Ez csak a saját felületét jeleníti meg a kiválasztott nyelven.",
|
||||||
"Settings.profile.form.section.experience.interfaceLanguageHelp": "A kiválasztás csak az Ön számára módosítja a felület nyelvét. Kérjük, olvassa el ezt a {document}, hogy más nyelveket a csapata számára is elérhetővé tehesse.",
|
"Settings.profile.form.section.experience.interfaceLanguageHelp": "A kiválasztás csak az Ön számára módosítja a felület nyelvét. Kérjük, olvassa el ezt a {here}, hogy más nyelveket a csapata számára is elérhetővé tehesse.",
|
||||||
"Settings.profile.form.section.experience.title": "Tapasztalat",
|
"Settings.profile.form.section.experience.title": "Tapasztalat",
|
||||||
"Settings.profile.form.section.helmet.title": "Felhasználói profil",
|
"Settings.profile.form.section.helmet.title": "Felhasználói profil",
|
||||||
"Settings.profile.form.section.profile.page.title": "Profil oldal",
|
"Settings.profile.form.section.profile.page.title": "Profil oldal",
|
||||||
|
@ -123,10 +123,10 @@
|
|||||||
"Settings.permissions.users.tabs.label": "Tabs Permissions",
|
"Settings.permissions.users.tabs.label": "Tabs Permissions",
|
||||||
"Settings.profile.form.notify.data.loaded": "Your profile data has been loaded",
|
"Settings.profile.form.notify.data.loaded": "Your profile data has been loaded",
|
||||||
"Settings.profile.form.section.experience.clear.select": "Clear the interface language selected",
|
"Settings.profile.form.section.experience.clear.select": "Clear the interface language selected",
|
||||||
"Settings.profile.form.section.experience.documentation": "documentation",
|
"Settings.profile.form.section.experience.here": "documentation",
|
||||||
"Settings.profile.form.section.experience.interfaceLanguage": "Interface language",
|
"Settings.profile.form.section.experience.interfaceLanguage": "Interface language",
|
||||||
"Settings.profile.form.section.experience.interfaceLanguage.hint": "This will only display your own interface in the chosen language.",
|
"Settings.profile.form.section.experience.interfaceLanguage.hint": "This will only display your own interface in the chosen language.",
|
||||||
"Settings.profile.form.section.experience.interfaceLanguageHelp": "Selection will change the interface language only for you. Please refer to this {documentation} to make other languages available for your team.",
|
"Settings.profile.form.section.experience.interfaceLanguageHelp": "Selection will change the interface language only for you. Please refer to this {here} to make other languages available for your team.",
|
||||||
"Settings.profile.form.section.experience.title": "Experience",
|
"Settings.profile.form.section.experience.title": "Experience",
|
||||||
"Settings.profile.form.section.helmet.title": "ユーザープロフィール",
|
"Settings.profile.form.section.helmet.title": "ユーザープロフィール",
|
||||||
"Settings.profile.form.section.profile.page.title": "プロフィールページ",
|
"Settings.profile.form.section.profile.page.title": "プロフィールページ",
|
||||||
|
@ -177,10 +177,10 @@
|
|||||||
"Settings.permissions.users.strapi-author": "Auteur",
|
"Settings.permissions.users.strapi-author": "Auteur",
|
||||||
"Settings.profile.form.notify.data.loaded": "Je profielgegevens zijn geladen",
|
"Settings.profile.form.notify.data.loaded": "Je profielgegevens zijn geladen",
|
||||||
"Settings.profile.form.section.experience.clear.select": "Wis de geselecteerde interfacetaal",
|
"Settings.profile.form.section.experience.clear.select": "Wis de geselecteerde interfacetaal",
|
||||||
"Settings.profile.form.section.experience.documentation": "documentatie",
|
"Settings.profile.form.section.experience.here": "documentatie",
|
||||||
"Settings.profile.form.section.experience.interfaceLanguage": "Interfacetaal",
|
"Settings.profile.form.section.experience.interfaceLanguage": "Interfacetaal",
|
||||||
"Settings.profile.form.section.experience.interfaceLanguage.hint": "Hierdoor wordt alleen je eigen interface in de gekozen taal weergegeven.",
|
"Settings.profile.form.section.experience.interfaceLanguage.hint": "Hierdoor wordt alleen je eigen interface in de gekozen taal weergegeven.",
|
||||||
"Settings.profile.form.section.experience.interfaceLanguageHelp": "Selectie zal de interfacetaal alleen voor jou veranderen. Raadpleeg deze {documentation} om andere talen beschikbaar te maken voor uw team.",
|
"Settings.profile.form.section.experience.interfaceLanguageHelp": "Selectie zal de interfacetaal alleen voor jou veranderen. Raadpleeg deze {here} om andere talen beschikbaar te maken voor uw team.",
|
||||||
"Settings.profile.form.section.experience.mode.label": "Interface modus",
|
"Settings.profile.form.section.experience.mode.label": "Interface modus",
|
||||||
"Settings.profile.form.section.experience.mode.hint": "Toont uw interface in de gekozen modus.",
|
"Settings.profile.form.section.experience.mode.hint": "Toont uw interface in de gekozen modus.",
|
||||||
"Settings.profile.form.section.experience.mode.option-label": "{name} modus",
|
"Settings.profile.form.section.experience.mode.option-label": "{name} modus",
|
||||||
|
@ -132,7 +132,7 @@
|
|||||||
"Settings.permissions.users.strapi-author": "作者",
|
"Settings.permissions.users.strapi-author": "作者",
|
||||||
"Settings.profile.form.notify.data.loaded": "你的个人数据已经加载完成",
|
"Settings.profile.form.notify.data.loaded": "你的个人数据已经加载完成",
|
||||||
"Settings.profile.form.section.experience.clear.select": "清除已选择的界面语言",
|
"Settings.profile.form.section.experience.clear.select": "清除已选择的界面语言",
|
||||||
"Settings.profile.form.section.experience.documentation": "文档",
|
"Settings.profile.form.section.experience.here": "文档",
|
||||||
"Settings.profile.form.section.experience.interfaceLanguage": "界面语言",
|
"Settings.profile.form.section.experience.interfaceLanguage": "界面语言",
|
||||||
"Settings.profile.form.section.experience.interfaceLanguage.hint": "将会用所选择的语言显示你的界面",
|
"Settings.profile.form.section.experience.interfaceLanguage.hint": "将会用所选择的语言显示你的界面",
|
||||||
"Settings.profile.form.section.experience.interfaceLanguageHelp": "当前的语言选择只会更改你当前帐号界面语言。 请参考此 {here} 为你的团队提供其他语言。",
|
"Settings.profile.form.section.experience.interfaceLanguageHelp": "当前的语言选择只会更改你当前帐号界面语言。 请参考此 {here} 为你的团队提供其他语言。",
|
||||||
|
@ -177,10 +177,10 @@
|
|||||||
"Settings.permissions.users.strapi-author": "作者",
|
"Settings.permissions.users.strapi-author": "作者",
|
||||||
"Settings.profile.form.notify.data.loaded": "您的個人檔案資料已經載入",
|
"Settings.profile.form.notify.data.loaded": "您的個人檔案資料已經載入",
|
||||||
"Settings.profile.form.section.experience.clear.select": "清除已選的介面語言",
|
"Settings.profile.form.section.experience.clear.select": "清除已選的介面語言",
|
||||||
"Settings.profile.form.section.experience.here": "here",
|
"Settings.profile.form.section.experience.here": "此文檔",
|
||||||
"Settings.profile.form.section.experience.interfaceLanguage": "介面語言",
|
"Settings.profile.form.section.experience.interfaceLanguage": "介面語言",
|
||||||
"Settings.profile.form.section.experience.interfaceLanguage.hint": "將會用所選擇的語言顯示您的介面",
|
"Settings.profile.form.section.experience.interfaceLanguage.hint": "將會用所選擇的語言顯示您的介面",
|
||||||
"Settings.profile.form.section.experience.interfaceLanguageHelp": "只有您的介面會變為所選擇的語言。如果要為您的團隊提供其他語言,請參考此 {documentation}。",
|
"Settings.profile.form.section.experience.interfaceLanguageHelp": "只有您的介面會變為所選擇的語言。如果要為您的團隊提供其他語言,請參考{here}。",
|
||||||
"Settings.profile.form.section.experience.mode.label": "介面模式",
|
"Settings.profile.form.section.experience.mode.label": "介面模式",
|
||||||
"Settings.profile.form.section.experience.mode.hint": "在選擇的模式中顯示您的介面。",
|
"Settings.profile.form.section.experience.mode.hint": "在選擇的模式中顯示您的介面。",
|
||||||
"Settings.profile.form.section.experience.mode.option-label": "{name} 模式",
|
"Settings.profile.form.section.experience.mode.option-label": "{name} 模式",
|
||||||
|
@ -82,6 +82,7 @@
|
|||||||
"jsonwebtoken": "8.5.1",
|
"jsonwebtoken": "8.5.1",
|
||||||
"koa-compose": "4.1.0",
|
"koa-compose": "4.1.0",
|
||||||
"koa-passport": "5.0.0",
|
"koa-passport": "5.0.0",
|
||||||
|
"koa2-ratelimit": "^1.1.2",
|
||||||
"koa-static": "5.0.0",
|
"koa-static": "5.0.0",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"markdown-it": "^12.3.2",
|
"markdown-it": "^12.3.2",
|
||||||
|
@ -10,6 +10,7 @@ const routes = require('./routes');
|
|||||||
const services = require('./services');
|
const services = require('./services');
|
||||||
const controllers = require('./controllers');
|
const controllers = require('./controllers');
|
||||||
const contentTypes = require('./content-types');
|
const contentTypes = require('./content-types');
|
||||||
|
const middlewares = require('./middlewares');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
register,
|
register,
|
||||||
@ -21,4 +22,5 @@ module.exports = {
|
|||||||
services,
|
services,
|
||||||
controllers,
|
controllers,
|
||||||
contentTypes,
|
contentTypes,
|
||||||
|
middlewares,
|
||||||
};
|
};
|
||||||
|
7
packages/core/admin/server/middlewares/index.js
Normal file
7
packages/core/admin/server/middlewares/index.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const rateLimit = require('./rateLimit');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
rateLimit,
|
||||||
|
};
|
43
packages/core/admin/server/middlewares/rateLimit.js
Normal file
43
packages/core/admin/server/middlewares/rateLimit.js
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const utils = require('@strapi/utils');
|
||||||
|
const { has, toLower } = require('lodash/fp');
|
||||||
|
|
||||||
|
const { RateLimitError } = utils.errors;
|
||||||
|
|
||||||
|
module.exports =
|
||||||
|
(config, { strapi }) =>
|
||||||
|
async (ctx, next) => {
|
||||||
|
let rateLimitConfig = strapi.config.get('admin.rateLimit');
|
||||||
|
|
||||||
|
if (!rateLimitConfig) {
|
||||||
|
rateLimitConfig = {
|
||||||
|
enabled: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!has('enabled', rateLimitConfig)) {
|
||||||
|
rateLimitConfig.enabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rateLimitConfig.enabled === true) {
|
||||||
|
const rateLimit = require('koa2-ratelimit').RateLimit;
|
||||||
|
|
||||||
|
const userEmail = toLower(ctx.request.body.email) || 'unknownEmail';
|
||||||
|
|
||||||
|
const loadConfig = {
|
||||||
|
interval: { min: 5 },
|
||||||
|
max: 5,
|
||||||
|
prefixKey: `${userEmail}:${ctx.request.path}:${ctx.request.ip}`,
|
||||||
|
handler() {
|
||||||
|
throw new RateLimitError();
|
||||||
|
},
|
||||||
|
...rateLimitConfig,
|
||||||
|
...config,
|
||||||
|
};
|
||||||
|
|
||||||
|
return rateLimit.middleware(loadConfig)(ctx, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
return next();
|
||||||
|
};
|
@ -5,7 +5,10 @@ module.exports = [
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: '/login',
|
path: '/login',
|
||||||
handler: 'authentication.login',
|
handler: 'authentication.login',
|
||||||
config: { auth: false },
|
config: {
|
||||||
|
auth: false,
|
||||||
|
middlewares: ['admin::rateLimit'],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
@ -159,7 +159,7 @@ class TransferEngine<
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#emitTransferUpdate(type: 'start' | 'finish' | 'error', payload?: object) {
|
#emitTransferUpdate(type: 'init' | 'start' | 'finish' | 'error', payload?: object) {
|
||||||
this.progress.stream.emit(`transfer::${type}`, payload);
|
this.progress.stream.emit(`transfer::${type}`, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -336,9 +336,8 @@ class TransferEngine<
|
|||||||
// reset data between transfers
|
// reset data between transfers
|
||||||
this.progress.data = {};
|
this.progress.data = {};
|
||||||
|
|
||||||
this.#emitTransferUpdate('start');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
this.#emitTransferUpdate('init');
|
||||||
await this.bootstrap();
|
await this.bootstrap();
|
||||||
await this.init();
|
await this.init();
|
||||||
|
|
||||||
@ -351,6 +350,8 @@ class TransferEngine<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.#emitTransferUpdate('start');
|
||||||
|
|
||||||
await this.beforeTransfer();
|
await this.beforeTransfer();
|
||||||
|
|
||||||
// Run the transfer stages
|
// Run the transfer stages
|
||||||
|
@ -49,6 +49,8 @@ class LocalFileSourceProvider implements ISourceProvider {
|
|||||||
|
|
||||||
options: ILocalFileSourceProviderOptions;
|
options: ILocalFileSourceProviderOptions;
|
||||||
|
|
||||||
|
#metadata?: IMetadata;
|
||||||
|
|
||||||
constructor(options: ILocalFileSourceProviderOptions) {
|
constructor(options: ILocalFileSourceProviderOptions) {
|
||||||
this.options = options;
|
this.options = options;
|
||||||
|
|
||||||
@ -60,15 +62,16 @@ class LocalFileSourceProvider implements ISourceProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pre flight checks regarding the provided options (making sure that the provided path is correct, etc...)
|
* Pre flight checks regarding the provided options, making sure that the file can be opened (decrypted, decompressed), etc.
|
||||||
*/
|
*/
|
||||||
async bootstrap() {
|
async bootstrap() {
|
||||||
const { path: filePath } = this.options.file;
|
const { path: filePath } = this.options.file;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// This is only to show a nicer error, it doesn't ensure the file will still exist when we try to open it later
|
// Read the metadata to ensure the file can be parsed
|
||||||
await fs.access(filePath, fs.constants.R_OK);
|
this.#metadata = await this.getMetadata();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(`Can't access file "${filePath}".`);
|
throw new Error(`Can't read file "${filePath}".`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@
|
|||||||
"build": "yarn build:ts",
|
"build": "yarn build:ts",
|
||||||
"build:ts": "tsc -p tsconfig.json",
|
"build:ts": "tsc -p tsconfig.json",
|
||||||
"build:clean": "yarn clean && yarn build",
|
"build:clean": "yarn clean && yarn build",
|
||||||
"watch": "yarn build:ts -w",
|
"watch": "yarn build:ts -w --preserveWatchOutput",
|
||||||
"test:unit": "jest --verbose",
|
"test:unit": "jest --verbose",
|
||||||
"prepublishOnly": "yarn build:clean"
|
"prepublishOnly": "yarn build:clean"
|
||||||
},
|
},
|
||||||
|
@ -1,98 +1,146 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const utils = require('../transfer/utils');
|
|
||||||
|
|
||||||
const mockDataTransfer = {
|
|
||||||
createLocalFileDestinationProvider: jest.fn(),
|
|
||||||
createLocalStrapiSourceProvider: jest.fn(),
|
|
||||||
createTransferEngine: jest.fn().mockReturnValue({
|
|
||||||
transfer: jest.fn().mockReturnValue(Promise.resolve({})),
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
jest.mock(
|
|
||||||
'@strapi/data-transfer',
|
|
||||||
() => {
|
|
||||||
return mockDataTransfer;
|
|
||||||
},
|
|
||||||
{ virtual: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
const exportCommand = require('../transfer/export');
|
|
||||||
|
|
||||||
const exit = jest.spyOn(process, 'exit').mockImplementation(() => {});
|
|
||||||
jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
||||||
|
|
||||||
jest.mock('../transfer/utils');
|
|
||||||
|
|
||||||
const defaultFileName = 'defaultFilename';
|
|
||||||
|
|
||||||
describe('export', () => {
|
describe('export', () => {
|
||||||
beforeEach(() => {
|
const defaultFileName = 'defaultFilename';
|
||||||
jest.resetAllMocks();
|
|
||||||
});
|
// mock @strapi/data-transfer
|
||||||
|
const mockDataTransfer = {
|
||||||
|
createLocalFileDestinationProvider: jest.fn().mockReturnValue({ name: 'testDest' }),
|
||||||
|
createLocalStrapiSourceProvider: jest.fn().mockReturnValue({ name: 'testSource' }),
|
||||||
|
createTransferEngine() {
|
||||||
|
return {
|
||||||
|
transfer: jest.fn().mockReturnValue(Promise.resolve({})),
|
||||||
|
progress: {
|
||||||
|
on: jest.fn(),
|
||||||
|
stream: {
|
||||||
|
on: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sourceProvider: { name: 'testSource' },
|
||||||
|
destinationProvider: { name: 'testDestination' },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
jest.mock(
|
||||||
|
'@strapi/data-transfer',
|
||||||
|
() => {
|
||||||
|
return mockDataTransfer;
|
||||||
|
},
|
||||||
|
{ virtual: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// mock utils
|
||||||
|
const mockUtils = {
|
||||||
|
createStrapiInstance() {
|
||||||
|
return {
|
||||||
|
telemetry: {
|
||||||
|
send: jest.fn(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getDefaultExportName: jest.fn(() => defaultFileName),
|
||||||
|
};
|
||||||
|
jest.mock(
|
||||||
|
'../transfer/utils',
|
||||||
|
() => {
|
||||||
|
return mockUtils;
|
||||||
|
},
|
||||||
|
{ virtual: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// other spies=
|
||||||
|
jest.spyOn(console, 'log').mockImplementation(() => {});
|
||||||
|
jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||||
|
jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
|
||||||
|
// Now that everything is mocked, import export command
|
||||||
|
const exportCommand = require('../transfer/export');
|
||||||
|
|
||||||
|
const expectExit = async (code, fn) => {
|
||||||
|
const exit = jest.spyOn(process, 'exit').mockImplementation((number) => {
|
||||||
|
throw new Error(`process.exit: ${number}`);
|
||||||
|
});
|
||||||
|
await expect(async () => {
|
||||||
|
await fn();
|
||||||
|
}).rejects.toThrow();
|
||||||
|
expect(exit).toHaveBeenCalledWith(code);
|
||||||
|
exit.mockRestore();
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {});
|
||||||
|
|
||||||
it('uses path provided by user', async () => {
|
it('uses path provided by user', async () => {
|
||||||
const filename = 'testfile';
|
const filename = 'test';
|
||||||
|
|
||||||
await exportCommand({ file: filename });
|
await expectExit(1, async () => {
|
||||||
|
await exportCommand({ file: filename });
|
||||||
|
});
|
||||||
|
|
||||||
expect(mockDataTransfer.createLocalFileDestinationProvider).toHaveBeenCalledWith(
|
expect(mockDataTransfer.createLocalFileDestinationProvider).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
file: { path: filename },
|
file: { path: filename },
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
expect(utils.getDefaultExportName).not.toHaveBeenCalled();
|
expect(mockUtils.getDefaultExportName).not.toHaveBeenCalled();
|
||||||
expect(exit).toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('uses default path if not provided by user', async () => {
|
it('uses default path if not provided by user', async () => {
|
||||||
utils.getDefaultExportName.mockReturnValue(defaultFileName);
|
await expectExit(1, async () => {
|
||||||
|
await exportCommand({});
|
||||||
await exportCommand({});
|
});
|
||||||
|
|
||||||
|
expect(mockUtils.getDefaultExportName).toHaveBeenCalledTimes(1);
|
||||||
expect(mockDataTransfer.createLocalFileDestinationProvider).toHaveBeenCalledWith(
|
expect(mockDataTransfer.createLocalFileDestinationProvider).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
file: { path: defaultFileName },
|
file: { path: defaultFileName },
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(utils.getDefaultExportName).toHaveBeenCalled();
|
|
||||||
expect(exit).toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('encrypts the output file if specified', async () => {
|
it('encrypts the output file if specified', async () => {
|
||||||
const encrypt = true;
|
const encrypt = true;
|
||||||
await exportCommand({ encrypt });
|
await expectExit(1, async () => {
|
||||||
|
await exportCommand({ encrypt });
|
||||||
|
});
|
||||||
|
|
||||||
expect(mockDataTransfer.createLocalFileDestinationProvider).toHaveBeenCalledWith(
|
expect(mockDataTransfer.createLocalFileDestinationProvider).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
encryption: { enabled: encrypt },
|
encryption: { enabled: encrypt },
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
expect(exit).toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('encrypts the output file with the given key', async () => {
|
it('encrypts the output file with the given key', async () => {
|
||||||
const key = 'secret-key';
|
const key = 'secret-key';
|
||||||
const encrypt = true;
|
const encrypt = true;
|
||||||
|
await expectExit(1, async () => {
|
||||||
|
await exportCommand({ encrypt, key });
|
||||||
|
});
|
||||||
|
|
||||||
await exportCommand({ encrypt, key });
|
|
||||||
expect(mockDataTransfer.createLocalFileDestinationProvider).toHaveBeenCalledWith(
|
expect(mockDataTransfer.createLocalFileDestinationProvider).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
encryption: { enabled: encrypt, key },
|
encryption: { enabled: encrypt, key },
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
expect(exit).toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('compresses the output file if specified', async () => {
|
it('uses compress option', async () => {
|
||||||
const compress = true;
|
await expectExit(1, async () => {
|
||||||
await exportCommand({ compress });
|
await exportCommand({ compress: false });
|
||||||
|
});
|
||||||
|
|
||||||
expect(mockDataTransfer.createLocalFileDestinationProvider).toHaveBeenCalledWith(
|
expect(mockDataTransfer.createLocalFileDestinationProvider).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
compression: { enabled: compress },
|
compression: { enabled: false },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await expectExit(1, async () => {
|
||||||
|
await exportCommand({ compress: true });
|
||||||
|
});
|
||||||
|
expect(mockDataTransfer.createLocalFileDestinationProvider).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
compression: { enabled: true },
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
expect(exit).toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -72,32 +72,23 @@ module.exports = async (opts) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
const progress = engine.progress.stream;
|
||||||
logger.log(`Starting export...`);
|
|
||||||
|
|
||||||
const progress = engine.progress.stream;
|
const getTelemetryPayload = (/* payload */) => {
|
||||||
|
return {
|
||||||
const telemetryPayload = (/* payload */) => {
|
eventProperties: {
|
||||||
return {
|
source: engine.sourceProvider.name,
|
||||||
eventProperties: {
|
destination: engine.destinationProvider.name,
|
||||||
source: engine.sourceProvider.name,
|
},
|
||||||
destination: engine.destinationProvider.name,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
progress.on('transfer::start', (payload) => {
|
progress.on('transfer::start', async () => {
|
||||||
strapi.telemetry.send('didDEITSProcessStart', telemetryPayload(payload));
|
logger.log(`Starting export...`);
|
||||||
});
|
await strapi.telemetry.send('didDEITSProcessStart', getTelemetryPayload());
|
||||||
|
});
|
||||||
progress.on('transfer::finish', (payload) => {
|
|
||||||
strapi.telemetry.send('didDEITSProcessFinish', telemetryPayload(payload));
|
|
||||||
});
|
|
||||||
|
|
||||||
progress.on('transfer::error', (payload) => {
|
|
||||||
strapi.telemetry.send('didDEITSProcessFail', telemetryPayload(payload));
|
|
||||||
});
|
|
||||||
|
|
||||||
|
try {
|
||||||
const results = await engine.transfer();
|
const results = await engine.transfer();
|
||||||
const outFile = results.destination.file.path;
|
const outFile = results.destination.file.path;
|
||||||
|
|
||||||
@ -111,11 +102,15 @@ module.exports = async (opts) => {
|
|||||||
|
|
||||||
logger.log(`${chalk.bold('Export process has been completed successfully!')}`);
|
logger.log(`${chalk.bold('Export process has been completed successfully!')}`);
|
||||||
logger.log(`Export archive is in ${chalk.green(outFile)}`);
|
logger.log(`Export archive is in ${chalk.green(outFile)}`);
|
||||||
process.exit(0);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
await strapi.telemetry.send('didDEITSProcessFail', getTelemetryPayload());
|
||||||
logger.error('Export process failed unexpectedly:', e.toString());
|
logger.error('Export process failed unexpectedly:', e.toString());
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Note: Telemetry can't be sent in a finish event, because it runs async after this block but we can't await it, so if process.exit is used it won't send
|
||||||
|
await strapi.telemetry.send('didDEITSProcessFinish', getTelemetryPayload());
|
||||||
|
process.exit(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -75,44 +75,40 @@ module.exports = async (opts) => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const engine = createTransferEngine(source, destination, engineOptions);
|
const engine = createTransferEngine(source, destination, engineOptions);
|
||||||
|
|
||||||
try {
|
const progress = engine.progress.stream;
|
||||||
logger.info('Starting import...');
|
const getTelemetryPayload = () => {
|
||||||
|
return {
|
||||||
const progress = engine.progress.stream;
|
eventProperties: {
|
||||||
const telemetryPayload = (/* payload */) => {
|
source: engine.sourceProvider.name,
|
||||||
return {
|
destination: engine.destinationProvider.name,
|
||||||
eventProperties: {
|
},
|
||||||
source: engine.sourceProvider.name,
|
|
||||||
destination: engine.destinationProvider.name,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
progress.on('transfer::start', (payload) => {
|
progress.on('transfer::start', async () => {
|
||||||
strapiInstance.telemetry.send('didDEITSProcessStart', telemetryPayload(payload));
|
logger.info('Starting import...');
|
||||||
});
|
await strapiInstance.telemetry.send('didDEITSProcessStart', getTelemetryPayload());
|
||||||
|
});
|
||||||
progress.on('transfer::finish', (payload) => {
|
|
||||||
strapiInstance.telemetry.send('didDEITSProcessFinish', telemetryPayload(payload));
|
|
||||||
});
|
|
||||||
|
|
||||||
progress.on('transfer::error', (payload) => {
|
|
||||||
strapiInstance.telemetry.send('didDEITSProcessFail', telemetryPayload(payload));
|
|
||||||
});
|
|
||||||
|
|
||||||
|
try {
|
||||||
const results = await engine.transfer();
|
const results = await engine.transfer();
|
||||||
const table = buildTransferTable(results.engine);
|
const table = buildTransferTable(results.engine);
|
||||||
logger.info(table.toString());
|
logger.info(table.toString());
|
||||||
|
|
||||||
logger.info('Import process has been completed successfully!');
|
logger.info('Import process has been completed successfully!');
|
||||||
process.exit(0);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
await strapiInstance.telemetry.send('didDEITSProcessFail', getTelemetryPayload());
|
||||||
logger.error('Import process failed unexpectedly:');
|
logger.error('Import process failed unexpectedly:');
|
||||||
logger.error(e);
|
logger.error(e);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Note: Telemetry can't be sent in a finish event, because it runs async after this block but we can't await it, so if process.exit is used it won't send
|
||||||
|
await strapi.telemetry.send('didDEITSProcessFinish', getTelemetryPayload());
|
||||||
|
process.exit(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const createError = require('http-errors');
|
const createError = require('http-errors');
|
||||||
const { NotFoundError, UnauthorizedError, ForbiddenError, PayloadTooLargeError } =
|
const { NotFoundError, UnauthorizedError, ForbiddenError, PayloadTooLargeError, RateLimitError } =
|
||||||
require('@strapi/utils').errors;
|
require('@strapi/utils').errors;
|
||||||
|
|
||||||
const mapErrorsAndStatus = [
|
const mapErrorsAndStatus = [
|
||||||
@ -21,6 +21,10 @@ const mapErrorsAndStatus = [
|
|||||||
classError: PayloadTooLargeError,
|
classError: PayloadTooLargeError,
|
||||||
status: 413,
|
status: 413,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
classError: RateLimitError,
|
||||||
|
status: 429,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const formatApplicationError = (error) => {
|
const formatApplicationError = (error) => {
|
||||||
|
@ -55,14 +55,6 @@ class ForbiddenError extends ApplicationError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PayloadTooLargeError extends ApplicationError {
|
|
||||||
constructor(message, details) {
|
|
||||||
super(message, details);
|
|
||||||
this.name = 'PayloadTooLargeError';
|
|
||||||
this.message = message || 'Entity too large';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class UnauthorizedError extends ApplicationError {
|
class UnauthorizedError extends ApplicationError {
|
||||||
constructor(message, details) {
|
constructor(message, details) {
|
||||||
super(message, details);
|
super(message, details);
|
||||||
@ -71,6 +63,23 @@ class UnauthorizedError extends ApplicationError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class RateLimitError extends ApplicationError {
|
||||||
|
constructor(message, details) {
|
||||||
|
super(message, details);
|
||||||
|
this.name = 'RateLimitError';
|
||||||
|
this.message = message || 'Too many requests, please try again later.';
|
||||||
|
this.details = details || {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PayloadTooLargeError extends ApplicationError {
|
||||||
|
constructor(message, details) {
|
||||||
|
super(message, details);
|
||||||
|
this.name = 'PayloadTooLargeError';
|
||||||
|
this.message = message || 'Entity too large';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class PolicyError extends ForbiddenError {
|
class PolicyError extends ForbiddenError {
|
||||||
constructor(message, details) {
|
constructor(message, details) {
|
||||||
super(message, details);
|
super(message, details);
|
||||||
@ -88,7 +97,8 @@ module.exports = {
|
|||||||
PaginationError,
|
PaginationError,
|
||||||
NotFoundError,
|
NotFoundError,
|
||||||
ForbiddenError,
|
ForbiddenError,
|
||||||
PayloadTooLargeError,
|
|
||||||
UnauthorizedError,
|
UnauthorizedError,
|
||||||
|
RateLimitError,
|
||||||
|
PayloadTooLargeError,
|
||||||
PolicyError,
|
PolicyError,
|
||||||
};
|
};
|
||||||
|
@ -29,7 +29,7 @@
|
|||||||
"pages.PluginPage.table.icon.regenerate": "Regenerér {target}",
|
"pages.PluginPage.table.icon.regenerate": "Regenerér {target}",
|
||||||
"pages.PluginPage.table.icon.show": "Åben {target}",
|
"pages.PluginPage.table.icon.show": "Åben {target}",
|
||||||
"pages.PluginPage.table.version": "Version",
|
"pages.PluginPage.table.version": "Version",
|
||||||
"pages.SettingsPage.Button.description": "Konfigurér dokumentations pluginnet",
|
"pages.SettingsPage.header.description": "Konfigurér dokumentations pluginnet",
|
||||||
"pages.SettingsPage.header.save": "Gem",
|
"pages.SettingsPage.header.save": "Gem",
|
||||||
"pages.SettingsPage.toggle.hint": "Gør dokumentationens endpoint privat",
|
"pages.SettingsPage.toggle.hint": "Gør dokumentationens endpoint privat",
|
||||||
"pages.SettingsPage.toggle.label": "Begrænset adgang",
|
"pages.SettingsPage.toggle.label": "Begrænset adgang",
|
||||||
|
@ -29,7 +29,7 @@
|
|||||||
"pages.PluginPage.table.icon.regenerate": "Regenerate {target}",
|
"pages.PluginPage.table.icon.regenerate": "Regenerate {target}",
|
||||||
"pages.PluginPage.table.icon.show": "Open {target}",
|
"pages.PluginPage.table.icon.show": "Open {target}",
|
||||||
"pages.PluginPage.table.version": "Version",
|
"pages.PluginPage.table.version": "Version",
|
||||||
"pages.SettingsPage.Button.description": "Configure the documentation plugin",
|
"pages.SettingsPage.header.description": "Configure the documentation plugin",
|
||||||
"pages.SettingsPage.header.save": "Save",
|
"pages.SettingsPage.header.save": "Save",
|
||||||
"pages.SettingsPage.toggle.hint": "Make the documentation endpoint private",
|
"pages.SettingsPage.toggle.hint": "Make the documentation endpoint private",
|
||||||
"pages.SettingsPage.toggle.label": "Restricted Access",
|
"pages.SettingsPage.toggle.label": "Restricted Access",
|
||||||
|
@ -29,7 +29,7 @@
|
|||||||
"pages.PluginPage.table.icon.regenerate": "Regenerar {target}",
|
"pages.PluginPage.table.icon.regenerate": "Regenerar {target}",
|
||||||
"pages.PluginPage.table.icon.show": "Abrir {target}",
|
"pages.PluginPage.table.icon.show": "Abrir {target}",
|
||||||
"pages.PluginPage.table.version": "Versión",
|
"pages.PluginPage.table.version": "Versión",
|
||||||
"pages.SettingsPage.Button.description": "Configura el plugin de documentación",
|
"pages.SettingsPage.header.description": "Configura el plugin de documentación",
|
||||||
"pages.SettingsPage.header.save": "Guardar",
|
"pages.SettingsPage.header.save": "Guardar",
|
||||||
"pages.SettingsPage.toggle.hint": "Hacer que la documentación sea privada",
|
"pages.SettingsPage.toggle.hint": "Hacer que la documentación sea privada",
|
||||||
"pages.SettingsPage.toggle.label": "Acceso restringido",
|
"pages.SettingsPage.toggle.label": "Acceso restringido",
|
||||||
|
@ -29,7 +29,7 @@
|
|||||||
"pages.PluginPage.table.icon.regenerate": "{target} 재생성",
|
"pages.PluginPage.table.icon.regenerate": "{target} 재생성",
|
||||||
"pages.PluginPage.table.icon.show": "{target} 열기",
|
"pages.PluginPage.table.icon.show": "{target} 열기",
|
||||||
"pages.PluginPage.table.version": "버전",
|
"pages.PluginPage.table.version": "버전",
|
||||||
"pages.SettingsPage.Button.description": "도큐멘테이션 플러그인 설정",
|
"pages.SettingsPage.header.description": "도큐멘테이션 플러그인 설정",
|
||||||
"pages.SettingsPage.header.save": "저장",
|
"pages.SettingsPage.header.save": "저장",
|
||||||
"pages.SettingsPage.toggle.hint": "도큐멘테이션 엔드포인트를 비공개로 설정합니다.",
|
"pages.SettingsPage.toggle.hint": "도큐멘테이션 엔드포인트를 비공개로 설정합니다.",
|
||||||
"pages.SettingsPage.toggle.label": "액세스 제한",
|
"pages.SettingsPage.toggle.label": "액세스 제한",
|
||||||
|
@ -29,7 +29,7 @@
|
|||||||
"pages.PluginPage.table.icon.regenerate": "Wygeneruj ponownie {target}",
|
"pages.PluginPage.table.icon.regenerate": "Wygeneruj ponownie {target}",
|
||||||
"pages.PluginPage.table.icon.show": "Otwórz {target}",
|
"pages.PluginPage.table.icon.show": "Otwórz {target}",
|
||||||
"pages.PluginPage.table.version": "Wersja",
|
"pages.PluginPage.table.version": "Wersja",
|
||||||
"pages.SettingsPage.Button.description": "Skonfiguruj plugin dokumentacji",
|
"pages.SettingsPage.header.description": "Skonfiguruj plugin dokumentacji",
|
||||||
"pages.SettingsPage.header.save": "Zapisz",
|
"pages.SettingsPage.header.save": "Zapisz",
|
||||||
"pages.SettingsPage.toggle.hint": "Ustaw endpoint dokumentacji na prywatny",
|
"pages.SettingsPage.toggle.hint": "Ustaw endpoint dokumentacji na prywatny",
|
||||||
"pages.SettingsPage.toggle.label": "Dostęp ograniczony",
|
"pages.SettingsPage.toggle.label": "Dostęp ograniczony",
|
||||||
|
@ -29,7 +29,7 @@
|
|||||||
"pages.PluginPage.table.icon.regenerate": "Återskapa {target}",
|
"pages.PluginPage.table.icon.regenerate": "Återskapa {target}",
|
||||||
"pages.PluginPage.table.icon.show": "Öppna {target}",
|
"pages.PluginPage.table.icon.show": "Öppna {target}",
|
||||||
"pages.PluginPage.table.version": "Version",
|
"pages.PluginPage.table.version": "Version",
|
||||||
"pages.SettingsPage.Button.description": "Konfigurera dokumentationspluginet",
|
"pages.SettingsPage.header.description": "Konfigurera dokumentationspluginet",
|
||||||
"pages.SettingsPage.header.save": "Spara",
|
"pages.SettingsPage.header.save": "Spara",
|
||||||
"pages.SettingsPage.toggle.hint": "Gör dokumentationensrutten privat",
|
"pages.SettingsPage.toggle.hint": "Gör dokumentationensrutten privat",
|
||||||
"pages.SettingsPage.toggle.label": "Begränsad åtkomst",
|
"pages.SettingsPage.toggle.label": "Begränsad åtkomst",
|
||||||
|
@ -29,7 +29,7 @@
|
|||||||
"pages.PluginPage.table.icon.regenerate": "Yeniden üret: {target}",
|
"pages.PluginPage.table.icon.regenerate": "Yeniden üret: {target}",
|
||||||
"pages.PluginPage.table.icon.show": "Aç: {target}",
|
"pages.PluginPage.table.icon.show": "Aç: {target}",
|
||||||
"pages.PluginPage.table.version": "Versiyon",
|
"pages.PluginPage.table.version": "Versiyon",
|
||||||
"pages.SettingsPage.Button.description": "Dokümantasyon eklentisini ayarla",
|
"pages.SettingsPage.header.description": "Dokümantasyon eklentisini ayarla",
|
||||||
"pages.SettingsPage.header.save": "Kaydet",
|
"pages.SettingsPage.header.save": "Kaydet",
|
||||||
"pages.SettingsPage.toggle.hint": "Dokümantasyon uç noktasını gizli yap",
|
"pages.SettingsPage.toggle.hint": "Dokümantasyon uç noktasını gizli yap",
|
||||||
"pages.SettingsPage.toggle.label": "Kısıtlı Erişim",
|
"pages.SettingsPage.toggle.label": "Kısıtlı Erişim",
|
||||||
|
@ -29,7 +29,7 @@
|
|||||||
"pages.PluginPage.table.icon.regenerate": "重新產生 {target}",
|
"pages.PluginPage.table.icon.regenerate": "重新產生 {target}",
|
||||||
"pages.PluginPage.table.icon.show": "開啟 {target}",
|
"pages.PluginPage.table.icon.show": "開啟 {target}",
|
||||||
"pages.PluginPage.table.version": "版本",
|
"pages.PluginPage.table.version": "版本",
|
||||||
"pages.SettingsPage.Button.description": "設定說明文件外掛程式",
|
"pages.SettingsPage.header.description": "設定說明文件外掛程式",
|
||||||
"pages.SettingsPage.header.save": "儲存",
|
"pages.SettingsPage.header.save": "儲存",
|
||||||
"pages.SettingsPage.toggle.hint": "將說明文件端點設為私人",
|
"pages.SettingsPage.toggle.hint": "將說明文件端點設為私人",
|
||||||
"pages.SettingsPage.toggle.label": "受限存取",
|
"pages.SettingsPage.toggle.label": "受限存取",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user