fix(uploads): apply configured max file size to guest uploads (#613 follow-up)
The admin's Settings → General → "Max File Size (MB)" value (general_max_file_size_mb) never applied to guest gallery uploads — the guest route hardcoded multer's per-file cap at 50MB (gallery.js) and the guest UI hardcoded the same 50MB client-side guard and "max 50MB" hint text. So a guest could not upload a large video even when the admin raised the limit (reported by mat1990dj on #613). Same class as the file-count miss fixed in #614, for size. - uploadSettings.js: new getMaxFileSizeMb()/getMaxFileSizeBytes() reading general_max_file_size_mb (default 50MB, cached 60s, clamped to a 10GB ceiling), mirroring getMaxFilesPerUpload. - gallery.js (guest upload): multer limits.fileSize now resolves from the setting; a LIMIT_FILE_SIZE error returns an actionable "max N MB" message. - publicSettings.js: exposes general_max_file_size_mb (default 50) so the gallery UI can render the real limit and guard client-side before an oversized POST. - UserPhotoUpload.tsx: reads the limit, uses it for the client-side size guard, and passes it to the requirements hint. The "max 50MB" literal in upload.fileRequirements is now interpolated ({{sizeLimit}}) across all 8 locales; adds upload.fileTooLarge (en/de; others fall back to en). Scope: guest path only (the reported gap). The admin path keeps its generous 10GB cap — admins are trusted and default 50MB would otherwise regress large admin video uploads. Format and batch-size limits already work correctly and are untouched. Adds SQLite-backed unit tests for the new getter. Verified end-to-end on a booted instance: admin sets 500MB → persisted → public settings exposes 500 → guest multer sources its cap from it.
This commit is contained in:
@@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* Unit tests for the per-file upload size limit getter (general_max_file_size_mb),
|
||||||
|
* added so the admin's "Max File Size (MB)" setting applies to guest uploads
|
||||||
|
* (#613 follow-up — mat1990dj). Real in-memory SQLite app_settings so the
|
||||||
|
* read/parse/cache path runs exactly as in production.
|
||||||
|
*/
|
||||||
|
const knex = require('knex');
|
||||||
|
|
||||||
|
let db;
|
||||||
|
let svc;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
db = knex({ client: 'sqlite3', connection: { filename: ':memory:' }, useNullAsDefault: true });
|
||||||
|
await db.schema.createTable('app_settings', (t) => {
|
||||||
|
t.increments('id');
|
||||||
|
t.string('setting_key').notNullable().unique();
|
||||||
|
t.text('setting_value');
|
||||||
|
t.string('setting_type');
|
||||||
|
t.timestamp('updated_at');
|
||||||
|
});
|
||||||
|
jest.resetModules();
|
||||||
|
jest.doMock('../../src/database/db', () => ({ db }));
|
||||||
|
svc = require('../../src/services/uploadSettings');
|
||||||
|
svc.clearMaxFileSizeCache();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
jest.dontMock('../../src/database/db');
|
||||||
|
await db.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function setLimit(mb) {
|
||||||
|
await db('app_settings')
|
||||||
|
.insert({ setting_key: 'general_max_file_size_mb', setting_value: JSON.stringify(mb), setting_type: 'general', updated_at: new Date() })
|
||||||
|
.onConflict('setting_key').merge({ setting_value: JSON.stringify(mb) });
|
||||||
|
svc.clearMaxFileSizeCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
test('defaults to 50MB when the setting is absent', async () => {
|
||||||
|
expect(await svc.getMaxFileSizeMb()).toBe(50);
|
||||||
|
expect(await svc.getMaxFileSizeBytes()).toBe(50 * 1024 * 1024);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('honours a configured value (e.g. 500MB video)', async () => {
|
||||||
|
await setLimit(500);
|
||||||
|
expect(await svc.getMaxFileSizeMb()).toBe(500);
|
||||||
|
expect(await svc.getMaxFileSizeBytes()).toBe(500 * 1024 * 1024);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clamps a nonsense value to the default and caps absurd values at the ceiling', async () => {
|
||||||
|
await setLimit(0);
|
||||||
|
expect(await svc.getMaxFileSizeMb()).toBe(50); // 0 → default
|
||||||
|
await setLimit(99_999_999);
|
||||||
|
expect(await svc.getMaxFileSizeMb()).toBe(svc.MAX_ALLOWED_FILE_SIZE_MB); // ceiling
|
||||||
|
});
|
||||||
|
|
||||||
|
test('caches for the TTL — a mid-window DB change is not seen until the cache is cleared', async () => {
|
||||||
|
await setLimit(200);
|
||||||
|
expect(await svc.getMaxFileSizeMb()).toBe(200);
|
||||||
|
// change the DB but do NOT clear cache
|
||||||
|
await db('app_settings').where({ setting_key: 'general_max_file_size_mb' }).update({ setting_value: JSON.stringify(300) });
|
||||||
|
expect(await svc.getMaxFileSizeMb()).toBe(200); // still cached
|
||||||
|
svc.clearMaxFileSizeCache();
|
||||||
|
expect(await svc.getMaxFileSizeMb()).toBe(300); // refreshed
|
||||||
|
});
|
||||||
@@ -1833,7 +1833,7 @@ router.post('/:eventId/upload', verifyGalleryAccess, denySlideshowToken, async (
|
|||||||
|
|
||||||
// Import multer and photo processing
|
// Import multer and photo processing
|
||||||
const multer = require('multer');
|
const multer = require('multer');
|
||||||
const { getAllowedMimeTypes, getMaxFilesPerUpload } = require('../services/uploadSettings');
|
const { getAllowedMimeTypes, getMaxFilesPerUpload, getMaxFileSizeBytes, DEFAULT_MAX_FILE_SIZE_MB } = require('../services/uploadSettings');
|
||||||
const { validateFileType } = require('../utils/fileSecurityUtils');
|
const { validateFileType } = require('../utils/fileSecurityUtils');
|
||||||
|
|
||||||
// Resolve allowed MIME types from settings
|
// Resolve allowed MIME types from settings
|
||||||
@@ -1859,10 +1859,22 @@ router.post('/:eventId/upload', verifyGalleryAccess, denySlideshowToken, async (
|
|||||||
maxFilesPerUpload = 500;
|
maxFilesPerUpload = 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Per-file size cap was hardcoded to 50MB here, so the admin's Settings →
|
||||||
|
// General → "Max File Size (MB)" value (general_max_file_size_mb) never
|
||||||
|
// applied to guest uploads — a guest could not upload a large video even
|
||||||
|
// when the admin allowed it (reported on #613 by mat1990dj). Resolve it from
|
||||||
|
// settings like the count above; fall back to the 50MB default on read error.
|
||||||
|
let maxFileSizeBytes;
|
||||||
|
try {
|
||||||
|
maxFileSizeBytes = await getMaxFileSizeBytes();
|
||||||
|
} catch {
|
||||||
|
maxFileSizeBytes = DEFAULT_MAX_FILE_SIZE_MB * 1024 * 1024;
|
||||||
|
}
|
||||||
|
|
||||||
const upload = multer({
|
const upload = multer({
|
||||||
dest: tempUploadDir,
|
dest: tempUploadDir,
|
||||||
limits: {
|
limits: {
|
||||||
fileSize: 50 * 1024 * 1024, // 50MB per file (separate concern from #613)
|
fileSize: maxFileSizeBytes,
|
||||||
files: maxFilesPerUpload
|
files: maxFilesPerUpload
|
||||||
},
|
},
|
||||||
fileFilter: (req, file, cb) => {
|
fileFilter: (req, file, cb) => {
|
||||||
@@ -1878,6 +1890,12 @@ router.post('/:eventId/upload', verifyGalleryAccess, denySlideshowToken, async (
|
|||||||
upload(req, res, async (err) => {
|
upload(req, res, async (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
logger.error('Upload error:', err);
|
logger.error('Upload error:', err);
|
||||||
|
// Turn multer's generic "File too large" into an actionable message
|
||||||
|
// that names the configured limit.
|
||||||
|
if (err.code === 'LIMIT_FILE_SIZE') {
|
||||||
|
const limitMb = Math.floor(maxFileSizeBytes / (1024 * 1024));
|
||||||
|
return res.status(400).json({ error: `File too large. Maximum size is ${limitMb} MB per file.` });
|
||||||
|
}
|
||||||
return res.status(400).json({ error: err.message });
|
return res.status(400).json({ error: err.message });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,11 @@ router.get('/', async (req, res) => {
|
|||||||
// but a client-side guard saves a 4MB+ round-trip when the
|
// but a client-side guard saves a 4MB+ round-trip when the
|
||||||
// limit is small.
|
// limit is small.
|
||||||
'general_max_files_per_upload',
|
'general_max_files_per_upload',
|
||||||
|
// Same rationale for the per-file size limit — the gallery upload
|
||||||
|
// component renders it in the requirements hint and guards
|
||||||
|
// client-side before posting an oversized file. Backend enforces
|
||||||
|
// via getMaxFileSizeBytes regardless.
|
||||||
|
'general_max_file_size_mb',
|
||||||
// #798 — the admin login page needs to know whether to show
|
// #798 — the admin login page needs to know whether to show
|
||||||
// the "Sign in with SSO" button (and its label). Only these
|
// the "Sign in with SSO" button (and its label). Only these
|
||||||
// two oidc_* keys are public; issuer/client stay admin-only.
|
// two oidc_* keys are public; issuer/client stay admin-only.
|
||||||
@@ -207,6 +212,12 @@ router.get('/', async (req, res) => {
|
|||||||
general_max_files_per_upload: Number.isFinite(Number(settingsObject.general_max_files_per_upload))
|
general_max_files_per_upload: Number.isFinite(Number(settingsObject.general_max_files_per_upload))
|
||||||
? Number(settingsObject.general_max_files_per_upload)
|
? Number(settingsObject.general_max_files_per_upload)
|
||||||
: 500,
|
: 500,
|
||||||
|
// Per-file size limit (MB). Default mirrors uploadSettings.js
|
||||||
|
// DEFAULT_MAX_FILE_SIZE_MB so the gallery UI shows a sensible number on
|
||||||
|
// installs that never set it explicitly.
|
||||||
|
general_max_file_size_mb: Number.isFinite(Number(settingsObject.general_max_file_size_mb))
|
||||||
|
? Number(settingsObject.general_max_file_size_mb)
|
||||||
|
: 50,
|
||||||
// SEO meta tag flags (safe to expose - these are intended for crawlers)
|
// SEO meta tag flags (safe to expose - these are intended for crawlers)
|
||||||
seo_meta_noindex: settingsObject.seo_meta_noindex === true,
|
seo_meta_noindex: settingsObject.seo_meta_noindex === true,
|
||||||
seo_meta_nofollow: settingsObject.seo_meta_nofollow === true,
|
seo_meta_nofollow: settingsObject.seo_meta_nofollow === true,
|
||||||
|
|||||||
@@ -5,9 +5,18 @@ const DEFAULT_MAX_FILES_PER_UPLOAD = 500;
|
|||||||
const MAX_ALLOWED_FILES_PER_UPLOAD = 2000;
|
const MAX_ALLOWED_FILES_PER_UPLOAD = 2000;
|
||||||
const CACHE_TTL_MS = 60_000;
|
const CACHE_TTL_MS = 60_000;
|
||||||
|
|
||||||
|
// Per-file upload size limit (general_max_file_size_mb). The admin sets this in
|
||||||
|
// Settings → General; the default mirrors the frontend's default (50 MB). A
|
||||||
|
// hard ceiling keeps a fat-fingered value from disabling multer's guard.
|
||||||
|
const DEFAULT_MAX_FILE_SIZE_MB = 50;
|
||||||
|
const MAX_ALLOWED_FILE_SIZE_MB = 10 * 1024; // 10 GB — matches the admin path's cap
|
||||||
|
|
||||||
let cachedValue = DEFAULT_MAX_FILES_PER_UPLOAD;
|
let cachedValue = DEFAULT_MAX_FILES_PER_UPLOAD;
|
||||||
let cacheExpiresAt = 0;
|
let cacheExpiresAt = 0;
|
||||||
|
|
||||||
|
let cachedFileSizeMb = DEFAULT_MAX_FILE_SIZE_MB;
|
||||||
|
let fileSizeCacheExpiresAt = 0;
|
||||||
|
|
||||||
// Map of file extension to MIME type(s)
|
// Map of file extension to MIME type(s)
|
||||||
const EXTENSION_TO_MIME = {
|
const EXTENSION_TO_MIME = {
|
||||||
'jpg': 'image/jpeg',
|
'jpg': 'image/jpeg',
|
||||||
@@ -99,6 +108,52 @@ const clearMaxFilesPerUploadCache = () => {
|
|||||||
cacheExpiresAt = 0;
|
cacheExpiresAt = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const normalizeFileSizeMb = (value) => {
|
||||||
|
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
|
||||||
|
return DEFAULT_MAX_FILE_SIZE_MB;
|
||||||
|
}
|
||||||
|
const intValue = Math.floor(value);
|
||||||
|
if (intValue < 1) return DEFAULT_MAX_FILE_SIZE_MB;
|
||||||
|
if (intValue > MAX_ALLOWED_FILE_SIZE_MB) return MAX_ALLOWED_FILE_SIZE_MB;
|
||||||
|
return intValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per-file upload size limit in MB (general_max_file_size_mb). Cached 60s, same
|
||||||
|
* as the other upload settings. Falls back to the default on a read error.
|
||||||
|
*/
|
||||||
|
const getMaxFileSizeMb = async () => {
|
||||||
|
if (Date.now() < fileSizeCacheExpiresAt) {
|
||||||
|
return cachedFileSizeMb;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const setting = await db('app_settings')
|
||||||
|
.where({ setting_key: 'general_max_file_size_mb' })
|
||||||
|
.first();
|
||||||
|
|
||||||
|
const parsedValue = normalizeFileSizeMb(parseSettingValue(setting));
|
||||||
|
cachedFileSizeMb = parsedValue;
|
||||||
|
fileSizeCacheExpiresAt = Date.now() + CACHE_TTL_MS;
|
||||||
|
return parsedValue;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to read max file size setting:', error.message);
|
||||||
|
cachedFileSizeMb = DEFAULT_MAX_FILE_SIZE_MB;
|
||||||
|
fileSizeCacheExpiresAt = Date.now() + CACHE_TTL_MS;
|
||||||
|
return DEFAULT_MAX_FILE_SIZE_MB;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Per-file upload size limit in bytes — convenience for multer `limits.fileSize`. */
|
||||||
|
const getMaxFileSizeBytes = async () => {
|
||||||
|
const mb = await getMaxFileSizeMb();
|
||||||
|
return mb * 1024 * 1024;
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearMaxFileSizeCache = () => {
|
||||||
|
fileSizeCacheExpiresAt = 0;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a comma-separated list of file extensions into an array of MIME types.
|
* Convert a comma-separated list of file extensions into an array of MIME types.
|
||||||
* Unknown extensions are silently ignored.
|
* Unknown extensions are silently ignored.
|
||||||
@@ -164,11 +219,16 @@ const clearAllowedTypesCache = () => {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
getMaxFilesPerUpload,
|
getMaxFilesPerUpload,
|
||||||
clearMaxFilesPerUploadCache,
|
clearMaxFilesPerUploadCache,
|
||||||
|
getMaxFileSizeMb,
|
||||||
|
getMaxFileSizeBytes,
|
||||||
|
clearMaxFileSizeCache,
|
||||||
getAllowedMimeTypes,
|
getAllowedMimeTypes,
|
||||||
clearAllowedTypesCache,
|
clearAllowedTypesCache,
|
||||||
extensionsToMimeTypes,
|
extensionsToMimeTypes,
|
||||||
EXTENSION_TO_MIME,
|
EXTENSION_TO_MIME,
|
||||||
DEFAULT_MAX_FILES_PER_UPLOAD,
|
DEFAULT_MAX_FILES_PER_UPLOAD,
|
||||||
MAX_ALLOWED_FILES_PER_UPLOAD,
|
MAX_ALLOWED_FILES_PER_UPLOAD,
|
||||||
|
DEFAULT_MAX_FILE_SIZE_MB,
|
||||||
|
MAX_ALLOWED_FILE_SIZE_MB,
|
||||||
DEFAULT_ALLOWED_FILE_TYPES
|
DEFAULT_ALLOWED_FILE_TYPES
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -43,6 +43,14 @@ export const UserPhotoUpload: React.FC<UserPhotoUploadProps> = ({
|
|||||||
? Number(publicSettings?.general_max_files_per_upload)
|
? Number(publicSettings?.general_max_files_per_upload)
|
||||||
: 500;
|
: 500;
|
||||||
|
|
||||||
|
// Per-file size limit (MB). Was hardcoded to 50MB below, so the admin's
|
||||||
|
// "Max File Size" setting never applied to guests (#613 follow-up). Surfaced
|
||||||
|
// via publicSettings (default 50); the backend enforces the same value.
|
||||||
|
const maxFileSizeMb = Number.isFinite(Number(publicSettings?.general_max_file_size_mb))
|
||||||
|
? Number(publicSettings?.general_max_file_size_mb)
|
||||||
|
: 50;
|
||||||
|
const maxFileSizeBytes = maxFileSizeMb * 1024 * 1024;
|
||||||
|
|
||||||
const allowedMimeTypes = useMemo(
|
const allowedMimeTypes = useMemo(
|
||||||
() => extensionsToMimeTypes(publicSettings?.allowed_file_types),
|
() => extensionsToMimeTypes(publicSettings?.allowed_file_types),
|
||||||
[publicSettings?.allowed_file_types]
|
[publicSettings?.allowed_file_types]
|
||||||
@@ -60,9 +68,9 @@ export const UserPhotoUpload: React.FC<UserPhotoUploadProps> = ({
|
|||||||
toast.error(`Invalid file type: ${file.name}`);
|
toast.error(`Invalid file type: ${file.name}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Check file size (50MB max)
|
// Check file size against the configured per-file limit.
|
||||||
if (file.size > 50 * 1024 * 1024) {
|
if (file.size > maxFileSizeBytes) {
|
||||||
toast.error(`File too large: ${file.name}`);
|
toast.error(t('upload.fileTooLarge', { name: file.name, limit: maxFileSizeMb }));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -228,7 +236,7 @@ export const UserPhotoUpload: React.FC<UserPhotoUploadProps> = ({
|
|||||||
{/* #613 — pass { limit } so `{{limit}}` interpolates
|
{/* #613 — pass { limit } so `{{limit}}` interpolates
|
||||||
with the real number from settings instead of
|
with the real number from settings instead of
|
||||||
rendering literally. */}
|
rendering literally. */}
|
||||||
{t('upload.fileRequirements', { limit: maxFilesPerUpload })}
|
{t('upload.fileRequirements', { limit: maxFilesPerUpload, sizeLimit: maxFileSizeMb })}
|
||||||
</p>
|
</p>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
|
|||||||
@@ -159,7 +159,7 @@
|
|||||||
"noCategory": "Keine Kategorie",
|
"noCategory": "Keine Kategorie",
|
||||||
"eventSpecific": "(Veranstaltungsspezifisch)",
|
"eventSpecific": "(Veranstaltungsspezifisch)",
|
||||||
"clickToUpload": "Klicken zum Hochladen oder per Drag & Drop",
|
"clickToUpload": "Klicken zum Hochladen oder per Drag & Drop",
|
||||||
"fileRequirements": "JPEG, PNG oder WebP (max. 50MB pro Datei, {{limit}} Dateien pro Upload)",
|
"fileRequirements": "JPEG, PNG oder WebP (max. {{sizeLimit}}MB pro Datei, {{limit}} Dateien pro Upload)",
|
||||||
"selectedFiles": "Ausgewählte Dateien",
|
"selectedFiles": "Ausgewählte Dateien",
|
||||||
"uploading": "Wird hochgeladen...",
|
"uploading": "Wird hochgeladen...",
|
||||||
"transferring": "Übertragung",
|
"transferring": "Übertragung",
|
||||||
@@ -192,7 +192,8 @@
|
|||||||
"processingFailed_one": "{{count}} Foto konnte nicht verarbeitet werden",
|
"processingFailed_one": "{{count}} Foto konnte nicht verarbeitet werden",
|
||||||
"processingFailed_other": "{{count}} Fotos konnten nicht verarbeitet werden",
|
"processingFailed_other": "{{count}} Fotos konnten nicht verarbeitet werden",
|
||||||
"uploadingChunks_one": "{{count}} Teil wird hochgeladen",
|
"uploadingChunks_one": "{{count}} Teil wird hochgeladen",
|
||||||
"uploadingChunks_other": "{{count}} Teile werden hochgeladen"
|
"uploadingChunks_other": "{{count}} Teile werden hochgeladen",
|
||||||
|
"fileTooLarge": "Datei zu groß: {{name}} (max. {{limit}}MB)"
|
||||||
},
|
},
|
||||||
"navigation": {
|
"navigation": {
|
||||||
"dashboard": "Dashboard",
|
"dashboard": "Dashboard",
|
||||||
|
|||||||
@@ -159,7 +159,7 @@
|
|||||||
"noCategory": "No category",
|
"noCategory": "No category",
|
||||||
"eventSpecific": "(Event specific)",
|
"eventSpecific": "(Event specific)",
|
||||||
"clickToUpload": "Click to upload or drag and drop",
|
"clickToUpload": "Click to upload or drag and drop",
|
||||||
"fileRequirements": "JPEG, PNG or WebP (max 50MB per file, {{limit}} files per upload)",
|
"fileRequirements": "JPEG, PNG or WebP (max {{sizeLimit}}MB per file, {{limit}} files per upload)",
|
||||||
"selectedFiles": "Selected files",
|
"selectedFiles": "Selected files",
|
||||||
"uploading": "Uploading...",
|
"uploading": "Uploading...",
|
||||||
"transferring": "Transferring",
|
"transferring": "Transferring",
|
||||||
@@ -192,7 +192,8 @@
|
|||||||
"processingFailed_one": "{{count}} photo failed to process",
|
"processingFailed_one": "{{count}} photo failed to process",
|
||||||
"processingFailed_other": "{{count}} photos failed to process",
|
"processingFailed_other": "{{count}} photos failed to process",
|
||||||
"uploadingChunks_one": "{{count}} chunk uploading",
|
"uploadingChunks_one": "{{count}} chunk uploading",
|
||||||
"uploadingChunks_other": "{{count}} chunks uploading"
|
"uploadingChunks_other": "{{count}} chunks uploading",
|
||||||
|
"fileTooLarge": "File too large: {{name}} (max {{limit}}MB)"
|
||||||
},
|
},
|
||||||
"navigation": {
|
"navigation": {
|
||||||
"dashboard": "Dashboard",
|
"dashboard": "Dashboard",
|
||||||
|
|||||||
@@ -126,7 +126,7 @@
|
|||||||
"noCategory": "Sin categoría",
|
"noCategory": "Sin categoría",
|
||||||
"eventSpecific": "(Específico del evento)",
|
"eventSpecific": "(Específico del evento)",
|
||||||
"clickToUpload": "Haz clic para subir o arrastra y suelta",
|
"clickToUpload": "Haz clic para subir o arrastra y suelta",
|
||||||
"fileRequirements": "JPEG, PNG o WebP (máx. 50MB por archivo, {{limit}} archivos por subida)",
|
"fileRequirements": "JPEG, PNG o WebP (máx. {{sizeLimit}}MB por archivo, {{limit}} archivos por subida)",
|
||||||
"fileRequirementsMedia": "Imágenes JPEG, PNG o WebP y videos MP4/MOV/WEBM (máx. 50MB por archivo, {{limit}} archivos por subida)",
|
"fileRequirementsMedia": "Imágenes JPEG, PNG o WebP y videos MP4/MOV/WEBM (máx. 50MB por archivo, {{limit}} archivos por subida)",
|
||||||
"unsupportedFiles": "Algunos archivos se omitieron porque el formato no es compatible (usa JPEG/PNG/WebP/MP4/MOV/WEBM).",
|
"unsupportedFiles": "Algunos archivos se omitieron porque el formato no es compatible (usa JPEG/PNG/WebP/MP4/MOV/WEBM).",
|
||||||
"selectedFiles": "Archivos seleccionados",
|
"selectedFiles": "Archivos seleccionados",
|
||||||
|
|||||||
@@ -131,7 +131,7 @@
|
|||||||
"noCategory": "Aucune catégorie",
|
"noCategory": "Aucune catégorie",
|
||||||
"eventSpecific": "(Spécifique à l'événement)",
|
"eventSpecific": "(Spécifique à l'événement)",
|
||||||
"clickToUpload": "Cliquez pour téléverser ou glissez-déposez",
|
"clickToUpload": "Cliquez pour téléverser ou glissez-déposez",
|
||||||
"fileRequirements": "JPEG, PNG ou WebP (max 50 Mo par fichier, {{limit}} fichiers par téléversement)",
|
"fileRequirements": "JPEG, PNG ou WebP (max {{sizeLimit}} Mo par fichier, {{limit}} fichiers par téléversement)",
|
||||||
"selectedFiles": "Fichiers sélectionnés",
|
"selectedFiles": "Fichiers sélectionnés",
|
||||||
"uploading": "Téléversement en cours...",
|
"uploading": "Téléversement en cours...",
|
||||||
"transferring": "Transfert en cours",
|
"transferring": "Transfert en cours",
|
||||||
|
|||||||
@@ -131,7 +131,7 @@
|
|||||||
"noCategory": "Geen categorie",
|
"noCategory": "Geen categorie",
|
||||||
"eventSpecific": "(Evenement-specifiek)",
|
"eventSpecific": "(Evenement-specifiek)",
|
||||||
"clickToUpload": "Klik om te uploaden of sleep bestanden hierheen",
|
"clickToUpload": "Klik om te uploaden of sleep bestanden hierheen",
|
||||||
"fileRequirements": "JPEG, PNG of WebP (max. 50 MB per bestand, {{limit}} bestanden per upload)",
|
"fileRequirements": "JPEG, PNG of WebP (max. {{sizeLimit}} MB per bestand, {{limit}} bestanden per upload)",
|
||||||
"selectedFiles": "Geselecteerde bestanden",
|
"selectedFiles": "Geselecteerde bestanden",
|
||||||
"uploading": "Uploaden...",
|
"uploading": "Uploaden...",
|
||||||
"uploadComplete": "Upload voltooid!",
|
"uploadComplete": "Upload voltooid!",
|
||||||
|
|||||||
@@ -131,7 +131,7 @@
|
|||||||
"noCategory": "Sem categoria",
|
"noCategory": "Sem categoria",
|
||||||
"eventSpecific": "(Específico do evento)",
|
"eventSpecific": "(Específico do evento)",
|
||||||
"clickToUpload": "Clique para enviar ou arraste e solte",
|
"clickToUpload": "Clique para enviar ou arraste e solte",
|
||||||
"fileRequirements": "JPEG, PNG ou WebP (máx. 50MB por arquivo, {{limit}} arquivos por envio)",
|
"fileRequirements": "JPEG, PNG ou WebP (máx. {{sizeLimit}}MB por arquivo, {{limit}} arquivos por envio)",
|
||||||
"selectedFiles": "Arquivos selecionados",
|
"selectedFiles": "Arquivos selecionados",
|
||||||
"uploading": "Enviando...",
|
"uploading": "Enviando...",
|
||||||
"uploadComplete": "Envio concluído!",
|
"uploadComplete": "Envio concluído!",
|
||||||
|
|||||||
@@ -131,7 +131,7 @@
|
|||||||
"noCategory": "Без категории",
|
"noCategory": "Без категории",
|
||||||
"eventSpecific": "(Для конкретного события)",
|
"eventSpecific": "(Для конкретного события)",
|
||||||
"clickToUpload": "Нажмите для загрузки или перетащите файлы",
|
"clickToUpload": "Нажмите для загрузки или перетащите файлы",
|
||||||
"fileRequirements": "JPEG, PNG или WebP (макс. 50 МБ на файл, {{limit}} файлов за загрузку)",
|
"fileRequirements": "JPEG, PNG или WebP (макс. {{sizeLimit}} МБ на файл, {{limit}} файлов за загрузку)",
|
||||||
"selectedFiles": "Выбранные файлы",
|
"selectedFiles": "Выбранные файлы",
|
||||||
"uploading": "Загрузка...",
|
"uploading": "Загрузка...",
|
||||||
"uploadComplete": "Загрузка завершена!",
|
"uploadComplete": "Загрузка завершена!",
|
||||||
|
|||||||
@@ -131,7 +131,7 @@
|
|||||||
"noCategory": "Brez kategorije",
|
"noCategory": "Brez kategorije",
|
||||||
"eventSpecific": "(specifično za dogodek)",
|
"eventSpecific": "(specifično za dogodek)",
|
||||||
"clickToUpload": "Kliknite za nalaganje ali povlecite in spustite",
|
"clickToUpload": "Kliknite za nalaganje ali povlecite in spustite",
|
||||||
"fileRequirements": "JPEG, PNG ali WebP (največ 50 MB na datoteko, {{limit}} datotek na nalaganje)",
|
"fileRequirements": "JPEG, PNG ali WebP (največ {{sizeLimit}} MB na datoteko, {{limit}} datotek na nalaganje)",
|
||||||
"selectedFiles": "Izbrane datoteke",
|
"selectedFiles": "Izbrane datoteke",
|
||||||
"uploading": "Nalaganje...",
|
"uploading": "Nalaganje...",
|
||||||
"transferring": "Prenašanje",
|
"transferring": "Prenašanje",
|
||||||
|
|||||||
Reference in New Issue
Block a user