Compare commits

...

21 Commits

Author SHA1 Message Date
sebseb7
9df5642a6e refactor: reimplement category page display with a recursive tree structure and add configurator image. 2025-12-13 02:14:20 +01:00
sebseb7
a50dd086c3 feat: Include 'Kategorien' in Nginx location regex for HTML content. 2025-12-06 21:02:22 +01:00
sebseb7
e88370ff3e feat: add Categories page with refined layout and translation support 2025-12-06 14:29:33 +01:00
sebseb7
5d3e0832fe doc 2025-12-01 13:11:24 +01:00
sebseb7
3347ba2754 Add missing auth translations and update components to use i18n keys
- Added new translation keys to de/auth.js:
  - resetPassword section (title, button, success, invalidToken, error, emailSent, emailError)
  - errors section (fillAllFields, invalidEmail, passwordsNotMatch, passwordsNotMatchShort, enterEmail, loginFailed, registerFailed, googleLoginFailed, emailExists)
  - success section (registerComplete)
  - newPassword, backToHome keys

- Updated ResetPassword.js to use translation keys instead of hardcoded German strings
- Updated LoginComponent.js to use translation keys instead of hardcoded German strings
- translate-i18n.js generated translations for other languages
2025-12-01 13:02:03 +01:00
sebseb7
013a38ca98 fix: update caniuse-lite version and enhance SPA routing for resetPassword
- Updated caniuse-lite to version 1.0.30001757 in package-lock.json for improved compatibility.
- Added functionality to copy index.html to the resetPassword directory for better SPA routing in production environments.
2025-12-01 12:50:41 +01:00
sebseb7
2d6c8ff25f feat(Orders): add tracking shipment link and update translations
- Implemented a tracking shipment link in the OrdersTab component for DHL deliveries, enhancing user experience by allowing direct access to shipment tracking.
- Added 'trackShipment' translation key across multiple languages to support the new feature.
- Updated existing translations for consistency and improved localization in the orders module.
2025-11-29 14:05:59 +01:00
sebseb7
d2ac8d3fc1 feat(Orders): update order status translations and colors
- Refactored order status translations to use English keys for 'new', 'shipped', and 'delivered'.
- Updated corresponding status colors to maintain consistency in the UI.
- Adjusted the display logic to reflect the new status keys in the OrdersTab component.
2025-11-29 13:45:39 +01:00
sebseb7
8928b3f283 feat(Orders): add 'paid' status and update translations across multiple languages
- Introduced 'paid' status to the orders system, enhancing order tracking capabilities.
- Updated translations for 'paid' status in various languages including German, Spanish, French, and more.
- Adjusted related UI components to reflect the new status and ensure consistent user experience across the application.
2025-11-29 13:21:35 +01:00
sebseb7
87db7ba3ea feat(Content, ProductDetail, SearchBar): enhance product handling with translation support
- Updated Content component to process products using translated attributes, improving localization.
- Modified ProductDetailPage to utilize translatedProduct for similar products.
- Adjusted SearchBar to provide suggestions based on translated products, enhancing user experience across components.
2025-11-23 07:53:37 +01:00
sebseb7
766fef2796 feat(ProductDetail): enhance attribute handling with translation support
- Updated ProductDetailPage to utilize translated attributes if available, improving localization.
- Cached both product and attribute data for better performance.
- Adjusted state management to reflect the use of translated attributes in the component.
2025-11-22 12:48:40 +01:00
sebseb7
a08c90a521 fix 2025-11-22 10:12:41 +01:00
sebseb7
10d60d5827 fix 2025-11-22 10:02:59 +01:00
sebseb7
905eee57d5 feat(Translation): add kitConfig.js for improved localization in GrowTentKonfigurator
- Updated the translation model by adding kitConfig.js to the list of translation files.
- Enhanced the GrowTentKonfigurator component to utilize translation functions for various UI texts, improving localization support throughout the configuration process.
2025-11-22 09:59:47 +01:00
sebseb7
3389a9b66c feat(Translation): enhance product dialogs and update translation model
- Added new translation files for product dialogs to support additional languages.
- Refactored various components to utilize translation functions for error messages, labels, and placeholders, enhancing localization support.
2025-11-22 09:43:51 +01:00
sebseb7
d63c385a97 feat(Content): pass categoryName prop to ProductFilters for improved filtering
- Added categoryName prop to the ProductFilters component to have it translated
2025-11-22 08:49:58 +01:00
sebseb7
1b51da69a9 feat(Images): update image URLs to AVIF format in SEO components
- Changed image file extensions from JPG to AVIF in category, feeds, and product SEO components to enhance performance and reduce file sizes.
- Ensured consistent image handling across the application by updating relevant image paths.
2025-11-21 13:21:58 +01:00
sebseb7
da81479d9b refactor(App): update Box component to use 'main' role for improved semantics
- Changed Box component in App.js and PrerenderAppContent.js to use 'main' as the component role, enhancing accessibility and semantic structure of the application.
2025-11-21 11:57:39 +01:00
sebseb7
d8678e261d feat(Images): update image handling to AVIF format across components
- Changed image file extensions from JPG to AVIF in data-fetching, product, category, and image components for improved performance and reduced file sizes.
- Updated image blob creation to reflect the new AVIF format in various components, ensuring consistency in image handling throughout the application.
2025-11-21 11:10:50 +01:00
sebseb7
ef91e50aa5 feat(Images): convert images to AVIF format for improved performance
- Updated image references in various components and configuration files to use AVIF format instead of PNG and JPG.
- Modified the build process to include a script for converting images to AVIF, enhancing loading times and reducing file sizes.
- Ensured consistency across the application by updating image paths in the footer, main layout, and content components.
2025-11-20 11:43:07 +01:00
sebseb7
061bf5ff17 feat(SEO): add price validity date to category JSON-LD
- Introduced a priceValidUntil field in the category JSON-LD to indicate the validity period of product prices, set to three months from the current date.
- This enhancement improves the structured data for SEO, providing clearer information about pricing timelines.
2025-11-20 11:13:48 +01:00
180 changed files with 4624 additions and 920 deletions

154
docs/nginx.conf Normal file
View File

@@ -0,0 +1,154 @@
server {
client_max_body_size 64M;
listen 443 ssl;
http2 on;
server_name example.de;
ssl_certificate /etc/letsencrypt/live/example.de/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.de/privkey.pem;
gzip on;
gzip_comp_level 6;
gzip_min_length 256;
gzip_vary on;
gzip_proxied any;
gzip_types
text/css
text/javascript
application/javascript
application/x-javascript
application/json
application/xml
image/svg+xml;
index index.html;
root /example/dist;
error_log logs/error.log info;
access_log logs/access.log combined;
location /socket.io/ {
proxy_pass http://localhost:9303/socket.io/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_connect_timeout 3600s;
proxy_send_timeout 3600s;
proxy_read_timeout 3600s;
send_timeout 3600s;
proxy_buffering off;
proxy_cache off;
keepalive_timeout 65;
keepalive_requests 100;
}
location /api/ {
proxy_pass http://localhost:9303/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header User-Agent $http_user_agent;
proxy_set_header Content-Type $content_type;
proxy_set_header Content-Length $content_length;
proxy_set_header X-API-Key $http_x_api_key;
proxy_connect_timeout 30s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
proxy_buffering off;
client_max_body_size 10M;
}
location ^~ /Kategorie/ {
types {}
default_type text/html;
}
location ^~ /Artikel/ {
types {}
default_type text/html;
}
location = /sitemap.xml {
types {}
default_type application/xml;
}
location ~ ^/(datenschutz|impressum|batteriegesetzhinweise|widerrufsrecht|sitemap|agb|Kategorien|Konfigurator|404|profile|resetPassword|thc-test|filiale|aktionen|presseverleih|payment/success)(/|$) {
types {}
default_type text/html;
}
location = /404 {
error_page 404 =404 /404-big.html;
return 404;
}
location = /404-big.html {
internal;
alias /home/seb/src/growheads_de/dist/404;
default_type text/html;
}
error_page 404 /404.html;
location = /404.html {
internal;
default_type text/html;
return 404 '<!doctype html><html><body>
<script>
if (!navigator.userAgent.includes("bot")) { location.href="/404"; }
</script>
</body></html>';
}
location ~* \.(js|css)\?.*$ {
expires 1y;
add_header Cache-Control "public, immutable";
add_header Vary Accept-Encoding;
}
location ~* \.(js|css)$ {
if ($uri ~ "\.[a-f0-9]{7,}\.(js|css)$") {
expires 1y;
add_header Cache-Control "public, immutable";
break;
}
expires 1d;
add_header Cache-Control "public";
add_header Vary Accept-Encoding;
}
location ~* \.(ttf|otf|woff|woff2|eot)$ {
expires 1y;
add_header Cache-Control "public";
add_header Access-Control-Allow-Origin "*";
}
location ~* \.(jpg|jpeg|png|gif|ico|svg|webp)$ {
expires 1y;
add_header Cache-Control "public";
add_header Vary Accept-Encoding;
}
location = /prerender.css {
expires 1w;
add_header Cache-Control "public";
add_header Vary Accept-Encoding;
}
location /assets/ {
expires 1y;
add_header Cache-Control "public";
add_header Vary Accept-Encoding;
}
}

6
package-lock.json generated
View File

@@ -4554,9 +4554,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001727",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz",
"integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==",
"version": "1.0.30001757",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz",
"integrity": "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==",
"dev": true,
"funding": [
{

View File

@@ -7,7 +7,7 @@
"start": "cross-env NODE_OPTIONS=\"--no-deprecation\" webpack serve --progress --mode development --no-open",
"start:seedheads": "cross-env PROXY_TARGET=https://seedheads.de NODE_OPTIONS=\"--no-deprecation\" webpack serve --progress --mode development --no-open",
"prod": "webpack serve --progress --mode production --no-client-overlay --no-client --no-web-socket-server --no-open --no-live-reload --no-hot --compress --no-devtool",
"build:client": "cross-env NODE_ENV=production webpack --progress --mode production && shx cp dist/index.html dist/index_template.html",
"build:client": "node scripts/convert-images-to-avif.cjs && cross-env NODE_ENV=production webpack --progress --mode production && shx cp dist/index.html dist/index_template.html",
"build": "npm run build:client",
"analyze": "cross-env ANALYZE=true NODE_ENV=production webpack --progress --mode production",
"lint": "eslint src/**/*.{js,jsx}",

View File

@@ -28,7 +28,7 @@ class CategoryService {
const cacheKey = `${categoryId}_${language}`;
return null;
}
async get(categoryId, language = "de") {
const cacheKey = `${categoryId}_${language}`;
return null;
@@ -159,6 +159,7 @@ const Batteriegesetzhinweise =
const Widerrufsrecht = require("./src/pages/Widerrufsrecht.js").default;
const Sitemap = require("./src/pages/Sitemap.js").default;
const PrerenderSitemap = require("./src/PrerenderSitemap.js").default;
const PrerenderCategoriesPage = require("./src/PrerenderCategoriesPage.js").default;
const AGB = require("./src/pages/AGB.js").default;
const NotFound404 = require("./src/pages/NotFound404.js").default;
@@ -189,7 +190,7 @@ const renderProductWorker = async (productSeoNames, workerId, progressCallback,
try {
const productDetails = await fetchProductDetails(workerSocket, productSeoName);
const actualSeoName = productDetails.product.seoName || productSeoName;
const productComponent = React.createElement(PrerenderProduct, {
productData: productDetails,
@@ -205,7 +206,7 @@ const renderProductWorker = async (productSeoNames, workerId, progressCallback,
}, shopConfig.baseUrl, shopConfig);
// Get category info from categoryMap if available
const categoryInfo = productDetails.product.categoryId ? categoryMap[productDetails.product.categoryId] : null;
const jsonLdScript = generateProductJsonLd({
...productDetails.product,
seoName: actualSeoName,
@@ -234,9 +235,9 @@ const renderProductWorker = async (productSeoNames, workerId, progressCallback,
success,
workerId
};
results.push(result);
// Call progress callback if provided
if (progressCallback) {
progressCallback(result);
@@ -252,14 +253,14 @@ const renderProductWorker = async (productSeoNames, workerId, progressCallback,
error: error.message,
workerId
};
results.push(result);
// Call progress callback if provided
if (progressCallback) {
progressCallback(result);
}
setTimeout(processNextProduct, 25);
}
};
@@ -291,16 +292,16 @@ const renderProductsInParallel = async (allProductsArray, maxWorkers, totalProdu
const barLength = 30;
const filledLength = Math.round((barLength * current) / total);
const bar = '█'.repeat(filledLength) + '░'.repeat(barLength - filledLength);
// @note Single line progress update to prevent flickering
const truncatedName = productName ? ` - ${productName.substring(0, 25)}${productName.length > 25 ? '...' : ''}` : '';
// Build worker stats on one line
let workerStats = '';
for (let i = 0; i < Math.min(maxWorkers, 8); i++) { // Limit to 8 workers to fit on screen
workerStats += `W${i + 1}:${workerCounts[i]}/${workerSuccess[i]} `;
}
// Single line update without complex cursor movements
process.stdout.write(`\r [${bar}] ${percentage}% (${current}/${total})${truncatedName}\n ${workerStats}${current < total ? '\x1b[1A' : '\n'}`);
};
@@ -308,26 +309,26 @@ const renderProductsInParallel = async (allProductsArray, maxWorkers, totalProdu
// Split products among workers
const productsPerWorker = Math.ceil(allProductsArray.length / maxWorkers);
const workerPromises = [];
// Initial progress bar
updateProgressBar(0, totalProducts);
for (let i = 0; i < maxWorkers; i++) {
const start = i * productsPerWorker;
const end = Math.min(start + productsPerWorker, allProductsArray.length);
const productsForWorker = allProductsArray.slice(start, end);
if (productsForWorker.length > 0) {
const promise = renderProductWorker(productsForWorker, i + 1, (result) => {
// Progress callback - called each time a product is completed
completedProducts++;
progressResults.push(result);
lastProductName = result.productName;
// Update per-worker counters
const workerIndex = result.workerId - 1; // Convert to 0-based index
workerCounts[workerIndex]++;
if (result.success) {
totalSuccessCount++;
workerSuccess[workerIndex]++;
@@ -335,11 +336,11 @@ const renderProductsInParallel = async (allProductsArray, maxWorkers, totalProdu
// Don't log errors immediately to avoid interfering with progress bar
// Errors will be shown after completion
}
// Update progress bar with worker stats
updateProgressBar(completedProducts, totalProducts, lastProductName);
}, categoryMap);
workerPromises.push(promise);
}
}
@@ -347,10 +348,10 @@ const renderProductsInParallel = async (allProductsArray, maxWorkers, totalProdu
try {
// Wait for all workers to complete
await Promise.all(workerPromises);
// Ensure final progress update
updateProgressBar(totalProducts, totalProducts, lastProductName);
// Show any errors that occurred
const errorResults = progressResults.filter(r => !r.success && r.error);
if (errorResults.length > 0) {
@@ -359,7 +360,7 @@ const renderProductsInParallel = async (allProductsArray, maxWorkers, totalProdu
console.log(` - ${result.productSeoName}: ${result.error}`);
});
}
return totalSuccessCount;
} catch (error) {
console.error('Error in parallel rendering:', error);
@@ -422,6 +423,14 @@ const renderApp = async (categoryData, socket) => {
process.exit(1);
}
// Copy index.html to resetPassword (no file extension) for SPA routing
if (config.isProduction) {
const indexPath = path.resolve(__dirname, config.outputDir, "index.html");
const resetPasswordPath = path.resolve(__dirname, config.outputDir, "resetPassword");
fs.copyFileSync(indexPath, resetPasswordPath);
console.log(`✅ Copied index.html to ${resetPasswordPath}`);
}
// Render static pages
console.log("\n📄 Rendering static pages...");
@@ -457,6 +466,13 @@ const renderApp = async (categoryData, socket) => {
description: "Sitemap page",
needsCategoryData: true,
},
{
component: PrerenderCategoriesPage,
path: "/Kategorien",
filename: "Kategorien",
description: "Categories page",
needsCategoryData: true,
},
{ component: AGB, path: "/agb", filename: "agb", description: "AGB page" },
{ component: NotFound404, path: "/404", filename: "404", description: "404 Not Found page" },
{
@@ -551,8 +567,7 @@ const renderApp = async (categoryData, socket) => {
try {
productData = await fetchCategoryProducts(socket, category.id);
console.log(
` ✅ Found ${
productData.products ? productData.products.length : 0
` ✅ Found ${productData.products ? productData.products.length : 0
} products`
);
@@ -636,7 +651,7 @@ const renderApp = async (categoryData, socket) => {
const totalProducts = allProducts.size;
const numCPUs = os.cpus().length;
const maxWorkers = Math.min(numCPUs, totalProducts, 8); // Cap at 8 workers to avoid overwhelming the server
// Create category map for breadcrumbs
const categoryMap = {};
allCategories.forEach(category => {
@@ -645,11 +660,11 @@ const renderApp = async (categoryData, socket) => {
seoName: category.seoName
};
});
console.log(
`\n📦 Rendering ${totalProducts} individual product pages using ${maxWorkers} parallel workers...`
);
const productPagesRendered = await renderProductsInParallel(
Array.from(allProducts),
maxWorkers,
@@ -701,21 +716,21 @@ const renderApp = async (categoryData, socket) => {
// Generate products.xml (Google Shopping feed) in parallel to sitemap.xml
if (allProductsData.length > 0) {
console.log("\n🛒 Generating products.xml (Google Shopping feed)...");
try {
const productsXml = generateProductsXml(allProductsData, shopConfig.baseUrl, shopConfig);
const productsXmlPath = path.resolve(__dirname, config.outputDir, "products.xml");
// Write with explicit UTF-8 encoding
fs.writeFileSync(productsXmlPath, productsXml, { encoding: 'utf8' });
console.log(`✅ products.xml generated: ${productsXmlPath}`);
console.log(` - Products included: ${allProductsData.length}`);
console.log(` - Format: Google Shopping RSS 2.0 feed`);
console.log(` - Encoding: UTF-8`);
console.log(` - Includes: title, description, price, availability, images`);
// Verify the file is valid UTF-8
try {
const verification = fs.readFileSync(productsXmlPath, 'utf8');
@@ -723,18 +738,18 @@ const renderApp = async (categoryData, socket) => {
} catch (verifyError) {
console.log(` - File verification: ⚠️ ${verifyError.message}`);
}
// Validate XML against Google Shopping schema
try {
const ProductsXmlValidator = require('./scripts/validate-products-xml.cjs');
const validator = new ProductsXmlValidator(productsXmlPath);
const validationResults = await validator.validate();
if (validationResults.valid) {
console.log(` - Schema validation: ✅ Valid Google Shopping RSS 2.0`);
} else {
console.log(` - Schema validation: ⚠️ ${validationResults.summary.errorCount} errors, ${validationResults.summary.warningCount} warnings`);
// Show first few errors for quick debugging
if (validationResults.errors.length > 0) {
console.log(` - First error: ${validationResults.errors[0].message}`);
@@ -743,7 +758,7 @@ const renderApp = async (categoryData, socket) => {
} catch (validationError) {
console.log(` - Schema validation: ⚠️ Validation failed: ${validationError.message}`);
}
} catch (error) {
console.error(`❌ Error generating products.xml: ${error.message}`);
console.log("\n⚠ Skipping products.xml generation due to errors");
@@ -754,18 +769,18 @@ const renderApp = async (categoryData, socket) => {
// Generate llms.txt (LLM-friendly markdown sitemap) and category-specific files
console.log("\n🤖 Generating LLM sitemap files...");
try {
// Generate main llms.txt overview file
const llmsTxt = generateLlmsTxt(allCategories, allProductsData, shopConfig.baseUrl, shopConfig);
const llmsTxtPath = path.resolve(__dirname, config.outputDir, "llms.txt");
fs.writeFileSync(llmsTxtPath, llmsTxt, { encoding: 'utf8' });
console.log(`✅ Main llms.txt generated: ${llmsTxtPath}`);
console.log(` - Static pages: 8 pages`);
console.log(` - Categories: ${allCategories.length} with links to detailed files`);
console.log(` - File size: ${Math.round(llmsTxt.length / 1024)}KB`);
// Group products by category for category-specific files
const productsByCategory = {};
allProductsData.forEach((product) => {
@@ -775,20 +790,20 @@ const renderApp = async (categoryData, socket) => {
}
productsByCategory[categoryId].push(product);
});
// Generate category-specific LLM files with pagination
let categoryFilesGenerated = 0;
let totalCategoryProducts = 0;
let totalPaginatedFiles = 0;
for (const category of allCategories) {
if (category.seoName) {
const categoryProducts = productsByCategory[category.id] || [];
const categorySlug = category.seoName.toLowerCase().replace(/[^a-z0-9]/g, '-');
// Generate all paginated files for this category
const categoryPages = generateAllCategoryLlmsPages(category, categoryProducts, shopConfig.baseUrl, shopConfig);
// Write each paginated file
for (const page of categoryPages) {
const pagePath = path.resolve(__dirname, config.outputDir, page.fileName);
@@ -806,22 +821,22 @@ const renderApp = async (categoryData, socket) => {
console.log(` ✅ llms-${categorySlug}-page-*.txt - ${categoryProducts.length} products across ${pageCount} pages (${Math.round(totalSize / 1024)}KB total)`);
console.log(` 📋 ${productList.fileName} - ${productList.productCount} products (${Math.round(productList.content.length / 1024)}KB)`);
categoryFilesGenerated++;
totalCategoryProducts += categoryProducts.length;
}
}
console.log(` 📄 Total paginated files generated: ${totalPaginatedFiles}`);
console.log(` 📦 Total products across all categories: ${totalCategoryProducts}`);
try {
const verification = fs.readFileSync(llmsTxtPath, 'utf8');
console.log(` - File verification: ✅ All files valid UTF-8`);
} catch (verifyError) {
console.log(` - File verification: ⚠️ ${verifyError.message}`);
}
} catch (error) {
console.error(`❌ Error generating LLM sitemap files: ${error.message}`);
console.log("\n⚠ Skipping LLM sitemap generation due to errors");
@@ -841,7 +856,7 @@ const fetchCategoryDataAndRender = () => {
const socket = io(socketUrl, {
path: "/socket.io/",
transports: [ "websocket"],
transports: ["websocket"],
reconnection: false,
timeout: 10000,
});

View File

@@ -152,7 +152,7 @@ const saveProductImages = async (socket, products, categoryName, outputDir) => {
"public",
"assets",
"images",
"sh.png"
"sh.avif"
);
// Ensure assets/images directory exists
@@ -185,7 +185,7 @@ const saveProductImages = async (socket, products, categoryName, outputDir) => {
if (imageIds.length > 0) {
// Process first image for each product
const bildId = parseInt(imageIds[0]);
const estimatedFilename = `prod${bildId}.jpg`; // We'll generate a filename based on the ID
const estimatedFilename = `prod${bildId}.avif`; // We'll generate a filename based on the ID
const imagePath = path.join(assetsPath, estimatedFilename);
@@ -231,12 +231,12 @@ const saveProductImages = async (socket, products, categoryName, outputDir) => {
opacity: 0.3,
},
])
.jpeg() // Ensure output is JPEG
.avif() // Ensure output is AVIF
.toBuffer();
fs.writeFileSync(imagePath, processedImageBuffer);
console.log(
` ✅ Applied centered inverted sh.png overlay to ${estimatedFilename}`
` ✅ Applied centered inverted sh.avif overlay to ${estimatedFilename}`
);
} catch (overlayError) {
console.log(
@@ -281,7 +281,7 @@ const saveCategoryImages = async (socket, categories, outputDir) => {
// Debug: Log categories that will be processed
console.log(" 🔍 Categories to process:");
categories.forEach((cat, index) => {
console.log(` ${index + 1}. "${cat.name}" (ID: ${cat.id}) -> cat${cat.id}.jpg`);
console.log(` ${index + 1}. "${cat.name}" (ID: ${cat.id}) -> cat${cat.id}.avif`);
});
const assetsPath = path.resolve(
@@ -308,7 +308,7 @@ const saveCategoryImages = async (socket, categories, outputDir) => {
for (const category of categories) {
categoriesProcessed++;
const estimatedFilename = `cat${category.id}.jpg`; // Use 'cat' prefix with category ID
const estimatedFilename = `cat${category.id}.avif`; // Use 'cat' prefix with category ID
const imagePath = path.join(assetsPath, estimatedFilename);
// Skip if image already exists

View File

@@ -1,6 +1,19 @@
const generateCategoryJsonLd = (category, products = [], baseUrl, config) => {
// Category IDs to skip (seeds, plants, headshop items)
const skipCategoryIds = [689, 706, 709, 711, 714, 748, 749, 896, 710, 924, 923, 922, 921, 916, 278, 259, 258];
// Check if category ID is in skip list
if (category.id && skipCategoryIds.includes(parseInt(category.id))) {
return '';
}
const categoryUrl = `${baseUrl}/Kategorie/${category.seoName}`;
// Calculate price valid date (current date + 3 months)
const priceValidDate = new Date();
priceValidDate.setMonth(priceValidDate.getMonth() + 3);
const priceValidUntil = priceValidDate.toISOString().split("T")[0];
const jsonLd = {
"@context": "https://schema.org/",
"@type": "CollectionPage",
@@ -42,7 +55,7 @@ const generateCategoryJsonLd = (category, products = [], baseUrl, config) => {
product.pictureList && product.pictureList.trim()
? `${baseUrl}/assets/images/prod${product.pictureList
.split(",")[0]
.trim()}.jpg`
.trim()}.avif`
: `${baseUrl}/assets/images/nopicture.jpg`,
description: product.description
? product.description.replace(/<[^>]*>/g, "").substring(0, 200)
@@ -57,6 +70,7 @@ const generateCategoryJsonLd = (category, products = [], baseUrl, config) => {
url: `${baseUrl}/Artikel/${product.seoName}`,
price: product.price && !isNaN(product.price) ? product.price.toString() : "0.00",
priceCurrency: config.currency,
priceValidUntil: priceValidUntil,
availability: product.available
? "https://schema.org/InStock"
: "https://schema.org/OutOfStock",

View File

@@ -535,7 +535,7 @@ const generateProductsXml = (allProductsData = [], baseUrl, config) => {
// Generate image URL
const imageUrl = product.pictureList && product.pictureList.trim()
? `${baseUrl}/assets/images/prod${product.pictureList.split(",")[0].trim()}.jpg`
? `${baseUrl}/assets/images/prod${product.pictureList.split(",")[0].trim()}.avif`
: `${baseUrl}/assets/images/nopicture.jpg`;
// Generate brand (manufacturer)

View File

@@ -5,7 +5,7 @@ const generateProductMetaTags = (product, baseUrl, config) => {
product.pictureList && product.pictureList.trim()
? `${baseUrl}/assets/images/prod${product.pictureList
.split(",")[0]
.trim()}.jpg`
.trim()}.avif`
: `${baseUrl}/assets/images/nopicture.jpg`;
@@ -68,7 +68,7 @@ const generateProductJsonLd = (product, baseUrl, config, categoryInfo = null) =>
product.pictureList && product.pictureList.trim()
? `${baseUrl}/assets/images/prod${product.pictureList
.split(",")[0]
.trim()}.jpg`
.trim()}.avif`
: `${baseUrl}/assets/images/nopicture.jpg`;
// Clean description for JSON-LD (remove HTML tags)

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@@ -0,0 +1,61 @@
const sharp = require('sharp');
const path = require('path');
const fs = require('fs');
const imagesToConvert = [
{ src: 'sh.png', dest: 'sh.avif' },
{ src: 'seeds.jpg', dest: 'seeds.avif' },
{ src: 'cutlings.jpg', dest: 'cutlings.avif' },
{ src: 'gg.png', dest: 'gg.avif' },
{ src: 'konfigurator.png', dest: 'konfigurator.avif' },
{ src: 'maps.png', dest: 'maps.avif' }
];
const run = async () => {
const imagesDir = path.join(__dirname, '../public/assets/images');
let hasError = false;
for (const image of imagesToConvert) {
const inputPath = path.join(imagesDir, image.src);
const outputPath = path.join(imagesDir, image.dest);
console.log('d');
if (!fs.existsSync(inputPath)) {
console.warn(`⚠️ Input file not found: ${inputPath}`);
continue;
}
// Check if output file exists and compare modification times
// Only convert if source is newer or destination doesn't exist
let shouldConvert = true;
if (fs.existsSync(outputPath)) {
const inputStat = fs.statSync(inputPath);
const outputStat = fs.statSync(outputPath);
if (inputStat.mtime <= outputStat.mtime) {
shouldConvert = false;
}
}
if (shouldConvert) {
try {
await sharp(inputPath)
.toFormat('avif')
.toFile(outputPath);
console.log(`✅ Converted ${image.src} to ${image.dest}`);
} catch (error) {
console.error(`❌ Error converting ${image.src}:`, error.message);
hasError = true;
}
} else {
// Silent skip if already up to date to keep logs clean, or use verbose flag
// console.log(`Skipping ${image.src} (already up to date)`);
}
}
if (hasError) {
process.exit(1);
}
};
console.log('dsfs');
run();

View File

@@ -0,0 +1,26 @@
const sharp = require('sharp');
const path = require('path');
const fs = require('fs');
const run = async () => {
const inputPath = path.join(__dirname, '../public/assets/images/sh.png');
const outputPath = path.join(__dirname, '../public/assets/images/sh.avif');
if (!fs.existsSync(inputPath)) {
console.error('Input file not found:', inputPath);
process.exit(1);
}
try {
await sharp(inputPath)
.toFormat('avif')
.toFile(outputPath);
console.log(`Successfully converted ${inputPath} to ${outputPath}`);
} catch (error) {
console.error('Error converting image:', error);
process.exit(1);
}
};
run();

View File

@@ -50,6 +50,7 @@ const Datenschutz = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/D
const AGB = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/AGB.js"));
//const NotFound404 = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/NotFound404.js")); <Route path="/404" element={<NotFound404 />} />
const Sitemap = lazy(() => import(/* webpackChunkName: "sitemap" */ "./pages/Sitemap.js"));
const CategoriesPage = lazy(() => import(/* webpackChunkName: "categories" */ "./pages/CategoriesPage.js"));
const Impressum = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/Impressum.js"));
const Batteriegesetzhinweise = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/Batteriegesetzhinweise.js"));
const Widerrufsrecht = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/Widerrufsrecht.js"));
@@ -228,7 +229,7 @@ const AppContent = ({ currentTheme, dynamicTheme, onThemeChange }) => {
<TitleUpdater />
<ScrollToTop />
<Header active categoryId={categoryId} key={authVersion} />
<Box sx={{ flexGrow: 1 }}>
<Box component="main" sx={{ flexGrow: 1 }}>
<Suspense fallback={
// Use prerender fallback if available, otherwise show loading spinner
typeof window !== "undefined" && window.__PRERENDER_FALLBACK__ ? (
@@ -260,19 +261,19 @@ const AppContent = ({ currentTheme, dynamicTheme, onThemeChange }) => {
{/* Category page - Render Content in parallel */}
<Route
path="/Kategorie/:categoryId"
element={<Content/>}
element={<Content />}
/>
{/* Single product page */}
<Route
path="/Artikel/:seoName"
element={<ProductDetail/>}
element={<ProductDetail />}
/>
{/* Search page - Render Content in parallel */}
<Route path="/search" element={<Content/>} />
<Route path="/search" element={<Content />} />
{/* Profile page */}
<Route path="/profile" element={<ProfilePage/>} />
<Route path="/profile" element={<ProfilePage />} />
{/* Payment success page for Mollie redirects */}
<Route path="/payment/success" element={<PaymentSuccess />} />
@@ -280,22 +281,23 @@ const AppContent = ({ currentTheme, dynamicTheme, onThemeChange }) => {
{/* Reset password page */}
<Route
path="/resetPassword"
element={<ResetPassword/>}
element={<ResetPassword />}
/>
{/* Admin page */}
<Route path="/admin" element={<AdminPage/>} />
<Route path="/admin" element={<AdminPage />} />
{/* Admin Users page */}
<Route path="/admin/users" element={<UsersPage/>} />
<Route path="/admin/users" element={<UsersPage />} />
{/* Admin Server Logs page */}
<Route path="/admin/logs" element={<ServerLogsPage/>} />
<Route path="/admin/logs" element={<ServerLogsPage />} />
{/* Legal pages */}
<Route path="/datenschutz" element={<Datenschutz />} />
<Route path="/agb" element={<AGB />} />
<Route path="/sitemap" element={<Sitemap />} />
<Route path="/Kategorien" element={<CategoriesPage />} />
<Route path="/impressum" element={<Impressum />} />
<Route
path="/batteriegesetzhinweise"
@@ -304,7 +306,7 @@ const AppContent = ({ currentTheme, dynamicTheme, onThemeChange }) => {
<Route path="/widerrufsrecht" element={<Widerrufsrecht />} />
{/* Grow Tent Configurator */}
<Route path="/Konfigurator" element={<GrowTentKonfigurator/>} />
<Route path="/Konfigurator" element={<GrowTentKonfigurator />} />
{/* Separate pages that are truly different */}
<Route path="/presseverleih" element={<PresseverleihPage />} />
@@ -457,11 +459,11 @@ const App = () => {
<ProductContextProvider>
<CategoryContextProvider>
<CssBaseline />
<AppContent
currentTheme={currentTheme}
dynamicTheme={dynamicTheme}
onThemeChange={handleThemeChange}
/>
<AppContent
currentTheme={currentTheme}
dynamicTheme={dynamicTheme}
onThemeChange={handleThemeChange}
/>
</CategoryContextProvider>
</ProductContextProvider>
</ThemeProvider>

View File

@@ -44,7 +44,7 @@ const PrerenderAppContent = (socket) => (
<CategoryList categoryId={209} activeCategoryId={null} socket={socket}/>
</AppBar>
<Box sx={{ flexGrow: 1 }}>
<Box component="main" sx={{ flexGrow: 1 }}>
<CarouselProvider>
<Routes>
<Route path="/" element={<MainPageLayout />} />

View File

@@ -0,0 +1,118 @@
import React from 'react';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import Paper from '@mui/material/Paper';
import LegalPage from './pages/LegalPage.js';
import CategoryBox from './components/CategoryBox.js';
const PrerenderCategoriesPage = ({ categoryData }) => {
// Helper function to recursively collect all categories from the tree
const collectAllCategories = (categoryNode, categories = [], level = 0) => {
if (!categoryNode) return categories;
// Add current category (skip root category 209)
if (categoryNode.id !== 209 && categoryNode.seoName) {
categories.push({
id: categoryNode.id,
name: categoryNode.name,
seoName: categoryNode.seoName,
level: level
});
}
// Recursively add children
if (categoryNode.children) {
for (const child of categoryNode.children) {
collectAllCategories(child, categories, level + 1);
}
}
return categories;
};
// The categoryData passed prop is the root tree (id: 209)
const rootTree = categoryData;
const renderLevel1Section = (l1Node) => {
// Collect all descendants (excluding the L1 node itself, which collectAllCategories would include first)
const descendants = collectAllCategories(l1Node).slice(1);
return (
<Paper
key={l1Node.id}
elevation={1}
sx={{
p: 2,
mb: 3,
display: 'flex',
flexDirection: { xs: 'column', md: 'row' },
alignItems: { xs: 'flex-start', md: 'flex-start' },
gap: 3
}}
>
{/* Level 1 Header/Box */}
<Box sx={{
minWidth: '150px',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: 1
}}>
<CategoryBox
id={l1Node.id}
name={l1Node.name}
seoName={l1Node.seoName}
sx={{
boxShadow: 4,
width: '150px',
height: '150px'
}}
/>
</Box>
{/* Descendants area */}
<Box sx={{ flex: 1 }}>
<Box sx={{
display: 'flex',
flexWrap: 'wrap',
gap: 2
}}>
{descendants.map((cat) => (
<CategoryBox
key={cat.id}
id={cat.id}
name={cat.name}
seoName={cat.seoName}
sx={{
width: '100px',
height: '100px',
minWidth: '100px',
minHeight: '100px',
boxShadow: 1,
fontSize: '0.9rem'
}}
/>
))}
</Box>
</Box>
</Paper>
);
};
const content = (
<Box>
<Box>
{rootTree && rootTree.children && rootTree.children.map((child) => (
renderLevel1Section(child)
))}
{(!rootTree || !rootTree.children || rootTree.children.length === 0) && (
<Typography>Keine Kategorien gefunden.</Typography>
)}
</Box>
</Box>
);
return <LegalPage title="Kategorien" content={content} />;
};
export default PrerenderCategoriesPage;

View File

@@ -111,7 +111,7 @@ const PrerenderCategory = ({ categoryId, categoryName, categorySeoName: _categor
component="img"
height="200"
image={product.pictureList && product.pictureList.trim()
? `/assets/images/prod${product.pictureList.split(',')[0].trim()}.jpg`
? `/assets/images/prod${product.pictureList.split(',')[0].trim()}.avif`
: '/assets/images/nopicture.jpg'
}
alt={product.name}

View File

@@ -79,7 +79,7 @@ class ArticleAvailabilityForm extends Component {
} else {
this.setState({
loading: false,
error: response.error || 'Ein Fehler ist aufgetreten'
error: response.error || this.props.t("productDialogs.errorGeneric")
});
}
@@ -114,20 +114,21 @@ class ArticleAvailabilityForm extends Component {
render() {
const { name, email, telegramId, notificationMethod, message, loading, success, error } = this.state;
const { t } = this.props;
return (
<Paper id="availability-form" sx={{ p: 3, mt: 4, borderRadius: 2, boxShadow: "0 2px 8px rgba(0,0,0,0.08)" }}>
<Typography variant="h5" gutterBottom sx={{ fontWeight: 600, color: '#333' }}>
Verfügbarkeit anfragen
{t("productDialogs.availabilityTitle")}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
Dieser Artikel ist derzeit nicht verfügbar. Gerne informieren wir Sie, sobald er wieder lieferbar ist.
{t("productDialogs.availabilitySubtitle")}
</Typography>
{success && (
<Alert severity="success" sx={{ mb: 3 }}>
Vielen Dank für Ihre Anfrage! Wir werden Sie {notificationMethod === 'email' ? 'per E-Mail' : 'über Telegram'} informieren, sobald der Artikel wieder verfügbar ist.
{notificationMethod === 'email' ? t("productDialogs.availabilitySuccessEmail") : t("productDialogs.availabilitySuccessTelegram")}
</Alert>
)}
@@ -139,18 +140,18 @@ class ArticleAvailabilityForm extends Component {
<Box component="form" onSubmit={this.handleSubmit} sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<TextField
label="Name"
label={t("productDialogs.nameLabel")}
value={name}
onChange={this.handleInputChange('name')}
required
fullWidth
disabled={loading}
placeholder="Ihr Name"
placeholder={t("productDialogs.namePlaceholder")}
/>
<FormControl component="fieldset" disabled={loading}>
<FormLabel component="legend" sx={{ mb: 1 }}>
Wie möchten Sie benachrichtigt werden?
{t("productDialogs.notificationMethodLabel")}
</FormLabel>
<RadioGroup
value={notificationMethod}
@@ -160,51 +161,51 @@ class ArticleAvailabilityForm extends Component {
<FormControlLabel
value="email"
control={<Radio />}
label="E-Mail"
label={t("productDialogs.emailLabel")}
/>
<FormControlLabel
value="telegram"
control={<Radio />}
label="Telegram Bot"
label={t("productDialogs.telegramBotLabel")}
/>
</RadioGroup>
</FormControl>
{notificationMethod === 'email' && (
<TextField
label="E-Mail"
label={t("productDialogs.emailLabel")}
type="email"
value={email}
onChange={this.handleInputChange('email')}
required
fullWidth
disabled={loading}
placeholder="ihre.email@example.com"
placeholder={t("productDialogs.emailPlaceholder")}
/>
)}
{notificationMethod === 'telegram' && (
<TextField
label="Telegram ID"
label={t("productDialogs.telegramIdLabel")}
value={telegramId}
onChange={this.handleInputChange('telegramId')}
required
fullWidth
disabled={loading}
placeholder="@IhrTelegramName oder Telegram ID"
helperText="Geben Sie Ihren Telegram-Benutzernamen (mit @) oder Ihre Telegram-ID ein"
placeholder={t("productDialogs.telegramPlaceholder")}
helperText={t("productDialogs.telegramHelper")}
/>
)}
<TextField
label="Nachricht (optional)"
label={t("productDialogs.messageLabel")}
value={message}
onChange={this.handleInputChange('message')}
fullWidth
multiline
rows={3}
disabled={loading}
placeholder="Zusätzliche Informationen oder Fragen..."
placeholder={t("productDialogs.messagePlaceholder")}
/>
<Button
@@ -225,10 +226,10 @@ class ArticleAvailabilityForm extends Component {
{loading ? (
<>
<CircularProgress size={20} sx={{ mr: 1 }} />
Wird gesendet...
{t("productDialogs.sending")}
</>
) : (
'Verfügbarkeit anfragen'
t("productDialogs.submitAvailability")
)}
</Button>
</Box>

View File

@@ -98,7 +98,7 @@ class ArticleQuestionForm extends Component {
} else {
this.setState({
loading: false,
error: response.error || 'Ein Fehler ist aufgetreten'
error: response.error || this.props.t("productDialogs.errorGeneric")
});
}
@@ -110,7 +110,7 @@ class ArticleQuestionForm extends Component {
} catch {
this.setState({
loading: false,
error: 'Fehler beim Verarbeiten der Fotos'
error: this.props.t("productDialogs.errorPhotos")
});
}
@@ -140,20 +140,21 @@ class ArticleQuestionForm extends Component {
render() {
const { name, email, question, loading, success, error } = this.state;
const { t } = this.props;
return (
<Paper sx={{ p: 3, mt: 4, borderRadius: 2, boxShadow: "0 2px 8px rgba(0,0,0,0.08)" }}>
<Typography variant="h5" gutterBottom sx={{ fontWeight: 600, color: '#333' }}>
Frage zum Artikel
{t("productDialogs.questionTitle")}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
Haben Sie eine Frage zu diesem Artikel? Wir helfen Ihnen gerne weiter.
{t("productDialogs.questionSubtitle")}
</Typography>
{success && (
<Alert severity="success" sx={{ mb: 3 }}>
Vielen Dank für Ihre Frage! Wir werden uns schnellstmöglich bei Ihnen melden.
{t("productDialogs.questionSuccess")}
</Alert>
)}
@@ -165,28 +166,28 @@ class ArticleQuestionForm extends Component {
<Box component="form" onSubmit={this.handleSubmit} sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<TextField
label="Name"
label={t("productDialogs.nameLabel")}
value={name}
onChange={this.handleInputChange('name')}
required
fullWidth
disabled={loading}
placeholder="Ihr Name"
placeholder={t("productDialogs.namePlaceholder")}
/>
<TextField
label="E-Mail"
label={t("productDialogs.emailLabel")}
type="email"
value={email}
onChange={this.handleInputChange('email')}
required
fullWidth
disabled={loading}
placeholder="ihre.email@example.com"
placeholder={t("productDialogs.emailPlaceholder")}
/>
<TextField
label="Ihre Frage"
label={t("productDialogs.questionLabel")}
value={question}
onChange={this.handleInputChange('question')}
required
@@ -194,7 +195,7 @@ class ArticleQuestionForm extends Component {
multiline
rows={4}
disabled={loading}
placeholder="Beschreiben Sie Ihre Frage zu diesem Artikel..."
placeholder={t("productDialogs.questionPlaceholder")}
/>
<PhotoUpload
@@ -202,7 +203,7 @@ class ArticleQuestionForm extends Component {
onChange={this.handlePhotosChange}
disabled={loading}
maxFiles={3}
label="Fotos zur Frage anhängen (optional)"
label={t("productDialogs.photosLabelQuestion")}
/>
<Button
@@ -219,10 +220,10 @@ class ArticleQuestionForm extends Component {
{loading ? (
<>
<CircularProgress size={20} sx={{ mr: 1 }} />
Wird gesendet...
{t("productDialogs.sending")}
</>
) : (
'Frage senden'
t("productDialogs.submitQuestion")
)}
</Button>
</Box>

View File

@@ -106,7 +106,7 @@ class ArticleRatingForm extends Component {
} else {
this.setState({
loading: false,
error: response.error || 'Ein Fehler ist aufgetreten'
error: response.error || this.props.t("productDialogs.errorGeneric")
});
}
@@ -118,7 +118,7 @@ class ArticleRatingForm extends Component {
} catch {
this.setState({
loading: false,
error: 'Fehler beim Verarbeiten der Fotos'
error: this.props.t("productDialogs.errorPhotos")
});
}
@@ -149,20 +149,21 @@ class ArticleRatingForm extends Component {
render() {
const { name, email, rating, review, loading, success, error } = this.state;
const { t } = this.props;
return (
<Paper sx={{ p: 3, mt: 4, borderRadius: 2, boxShadow: "0 2px 8px rgba(0,0,0,0.08)" }}>
<Typography variant="h5" gutterBottom sx={{ fontWeight: 600, color: '#333' }}>
Artikel Bewerten
{t("productDialogs.ratingTitle")}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
Teilen Sie Ihre Erfahrungen mit diesem Artikel und helfen Sie anderen Kunden bei der Entscheidung.
{t("productDialogs.ratingSubtitle")}
</Typography>
{success && (
<Alert severity="success" sx={{ mb: 3 }}>
Vielen Dank für Ihre Bewertung! Sie wird nach Prüfung veröffentlicht.
{t("productDialogs.ratingSuccess")}
</Alert>
)}
@@ -174,30 +175,30 @@ class ArticleRatingForm extends Component {
<Box component="form" onSubmit={this.handleSubmit} sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<TextField
label="Name"
label={t("productDialogs.nameLabel")}
value={name}
onChange={this.handleInputChange('name')}
required
fullWidth
disabled={loading}
placeholder="Ihr Name"
placeholder={t("productDialogs.namePlaceholder")}
/>
<TextField
label="E-Mail"
label={t("productDialogs.emailLabel")}
type="email"
value={email}
onChange={this.handleInputChange('email')}
required
fullWidth
disabled={loading}
placeholder="ihre.email@example.com"
helperText="Ihre E-Mail wird nicht veröffentlicht"
placeholder={t("productDialogs.emailPlaceholder")}
helperText={t("productDialogs.emailHelper")}
/>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography variant="body2" sx={{ fontWeight: 500 }}>
Bewertung *
{t("productDialogs.ratingLabel")}
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Rating
@@ -209,20 +210,20 @@ class ArticleRatingForm extends Component {
emptyIcon={<StarIcon style={{ opacity: 0.55 }} fontSize="inherit" />}
/>
<Typography variant="body2" color="text.secondary">
{rating > 0 ? `${rating} von 5 Sternen` : 'Bitte bewerten'}
{rating > 0 ? t("productDialogs.ratingStars", { rating }) : t("productDialogs.pleaseRate")}
</Typography>
</Box>
</Box>
<TextField
label="Ihre Bewertung (optional)"
label={t("productDialogs.reviewLabel")}
value={review}
onChange={this.handleInputChange('review')}
fullWidth
multiline
rows={4}
disabled={loading}
placeholder="Beschreiben Sie Ihre Erfahrungen mit diesem Artikel..."
placeholder={t("productDialogs.reviewPlaceholder")}
/>
<PhotoUpload
@@ -230,7 +231,7 @@ class ArticleRatingForm extends Component {
onChange={this.handlePhotosChange}
disabled={loading}
maxFiles={5}
label="Fotos zur Bewertung anhängen (optional)"
label={t("productDialogs.photosLabelRating")}
/>
<Button
@@ -247,10 +248,10 @@ class ArticleRatingForm extends Component {
{loading ? (
<>
<CircularProgress size={20} sx={{ mr: 1 }} />
Wird gesendet...
{t("productDialogs.sending")}
</>
) : (
'Bewertung abgeben'
t("productDialogs.submitRating")
)}
</Button>
</Box>

View File

@@ -23,7 +23,7 @@ class CartItem extends Component {
window.socketManager.emit('getPic', { bildId:picid, size:'tiny' }, (res) => {
if(res.success){
window.tinyPicCache[picid] = URL.createObjectURL(new Blob([res.imageBuffer], { type: 'image/jpeg' }));
window.tinyPicCache[picid] = URL.createObjectURL(new Blob([res.imageBuffer], { type: 'image/avif' }));
this.setState({image: window.tinyPicCache[picid], loading: false});
}
})

View File

@@ -47,7 +47,7 @@ const CategoryBox = ({
// Create fresh blob URL from cached binary data
try {
const uint8Array = new Uint8Array(cachedImageData);
const blob = new Blob([uint8Array], { type: 'image/jpeg' });
const blob = new Blob([uint8Array], { type: 'image/avif' });
objectUrl = URL.createObjectURL(blob);
setImageUrl(objectUrl);
setImageError(false);
@@ -73,7 +73,7 @@ const CategoryBox = ({
try {
// Convert binary data to blob URL
const uint8Array = new Uint8Array(imageData);
const blob = new Blob([uint8Array], { type: 'image/jpeg' });
const blob = new Blob([uint8Array], { type: 'image/avif' });
objectUrl = URL.createObjectURL(blob);
setImageUrl(objectUrl);
setImageError(false);
@@ -158,7 +158,7 @@ const CategoryBox = ({
position: 'relative',
backgroundImage: ((typeof window !== 'undefined' && window.__PRERENDER_FALLBACK__) ||
(typeof global !== 'undefined' && global.window && global.window.__PRERENDER_FALLBACK__))
? `url("/assets/images/cat${id}.jpg")`
? `url("/assets/images/cat${id}.avif")`
: (imageUrl && !imageError ? `url("${imageUrl}")` : 'none'),
backgroundSize: 'cover',
backgroundPosition: 'center',

View File

@@ -279,17 +279,25 @@ class Content extends Component {
}
processData(response) {
const unfilteredProducts = response.products;
const rawProducts = response.products;
const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n?.language || 'de';
if (!window.individualProductCache) {
window.individualProductCache = {};
}
//console.log("processData", unfilteredProducts);
if(unfilteredProducts) unfilteredProducts.forEach(product => {
window.individualProductCache[product.id] = {
data: product,
const unfilteredProducts = [];
//console.log("processData", rawProducts);
if(rawProducts) rawProducts.forEach(product => {
const effectiveProduct = product.translatedProduct || product;
const cacheKey = `${effectiveProduct.id}_${currentLanguage}`;
window.individualProductCache[cacheKey] = {
data: effectiveProduct,
timestamp: Date.now()
};
unfilteredProducts.push(effectiveProduct);
});
this.setState({
@@ -450,7 +458,12 @@ class Content extends Component {
{ query, language: currentLanguage, requestTranslation: currentLanguage === 'de' ? false : true },
(response) => {
if (response && response.products) {
this.processData(response);
// Map products to use translatedProduct if available
const enhancedResponse = {
...response,
products: response.products.map(p => p.translatedProduct || p)
};
this.processData(enhancedResponse);
} else {
console.log("fetchSearchData in Content failed", response);
}
@@ -681,6 +694,7 @@ class Content extends Component {
onFilterChange={()=>{this.filterProducts()}}
dataType={this.state.dataType}
dataParam={this.state.dataParam}
categoryName={this.state.categoryName}
/>
</Box>
@@ -722,7 +736,7 @@ class Content extends Component {
justifyContent: 'center'
}}>
<img
src="/assets/images/seeds.jpg"
src="/assets/images/seeds.avif"
alt="Seeds"
fetchPriority="high"
loading="eager"
@@ -783,7 +797,7 @@ class Content extends Component {
justifyContent: 'center'
}}>
<img
src="/assets/images/cutlings.jpg"
src="/assets/images/cutlings.avif"
alt="Stecklinge"
fetchPriority="high"
loading="eager"

View File

@@ -296,7 +296,7 @@ class Footer extends Component {
>
<Box
component="img"
src="/assets/images/gg.png"
src="/assets/images/gg.avif"
alt="Google Reviews"
sx={{
height: { xs: 50, md: 60 },
@@ -326,7 +326,7 @@ class Footer extends Component {
>
<Box
component="img"
src="/assets/images/maps.png"
src="/assets/images/maps.avif"
alt="Google Maps"
sx={{
height: { xs: 40, md: 50 },
@@ -352,6 +352,9 @@ class Footer extends Component {
<Typography variant="body2" sx={{ fontSize: { xs: '11px', md: '14px' }, lineHeight: 1.5 }}>
© {new Date().getFullYear()} <StyledDomainLink href="https://growheads.de" target="_blank" rel="noopener noreferrer">GrowHeads.de</StyledDomainLink>
</Typography>
<Typography variant="body2" sx={{ fontSize: { xs: '9px', md: '9px' }, lineHeight: 1.5, mt: 1 }}>
<StyledDomainLink href="https://telegraf.growheads.de" target="_blank" rel="noreferrer">Telegraf - sicherer Chat mit unseren Mitarbeitern</StyledDomainLink>
</Typography>
</Box>
</Stack>
</Box>

View File

@@ -56,7 +56,7 @@ class Images extends Component {
pics.push(window.tinyPicCache[bildId]);
this.loadPic(this.props.fullscreenOpen ? 'large' : 'medium',bildId,newMainPic);
}else{
pics.push(`/assets/images/prod${bildId}.jpg`);
pics.push(`/assets/images/prod${bildId}.avif`);
this.loadPic(this.props.fullscreenOpen ? 'large' : 'medium',bildId,newMainPic);
}
}else{
@@ -84,7 +84,7 @@ class Images extends Component {
window.socketManager.emit('getPic', { bildId, size }, (res) => {
if(res.success){
const url = URL.createObjectURL(new Blob([res.imageBuffer], { type: 'image/jpeg' }));
const url = URL.createObjectURL(new Blob([res.imageBuffer], { type: 'image/avif' }));
if(size === 'medium') window.mediumPicCache[bildId] = url;
if(size === 'small') window.smallPicCache[bildId] = url;
@@ -118,7 +118,7 @@ class Images extends Component {
if (!this.props.pictureList || !this.props.pictureList.trim()) {
return '/assets/images/nopicture.jpg';
}
return `/assets/images/prod${this.props.pictureList.split(',')[0].trim()}.jpg`;
return `/assets/images/prod${this.props.pictureList.split(',')[0].trim()}.avif`;
};
return (

View File

@@ -175,12 +175,12 @@ export class LoginComponent extends Component {
const { location, navigate } = this.props;
if (!email || !password) {
this.setState({ error: 'Bitte füllen Sie alle Felder aus' });
this.setState({ error: this.props.t ? this.props.t('auth.errors.fillAllFields') : 'Bitte füllen Sie alle Felder aus' });
return;
}
if (!this.validateEmail(email)) {
this.setState({ error: 'Bitte geben Sie eine gültige E-Mail-Adresse ein' });
this.setState({ error: this.props.t ? this.props.t('auth.errors.invalidEmail') : 'Bitte geben Sie eine gültige E-Mail-Adresse ein' });
return;
}
@@ -238,7 +238,7 @@ export class LoginComponent extends Component {
} else {
this.setState({
loading: false,
error: response.message || 'Anmeldung fehlgeschlagen'
error: response.message || (this.props.t ? this.props.t('auth.errors.loginFailed') : 'Anmeldung fehlgeschlagen')
});
}
});
@@ -248,22 +248,22 @@ export class LoginComponent extends Component {
const { email, password, confirmPassword } = this.state;
if (!email || !password || !confirmPassword) {
this.setState({ error: 'Bitte füllen Sie alle Felder aus' });
this.setState({ error: this.props.t ? this.props.t('auth.errors.fillAllFields') : 'Bitte füllen Sie alle Felder aus' });
return;
}
if (!this.validateEmail(email)) {
this.setState({ error: 'Bitte geben Sie eine gültige E-Mail-Adresse ein' });
this.setState({ error: this.props.t ? this.props.t('auth.errors.invalidEmail') : 'Bitte geben Sie eine gültige E-Mail-Adresse ein' });
return;
}
if (password !== confirmPassword) {
this.setState({ error: 'Passwörter stimmen nicht überein' });
this.setState({ error: this.props.t ? this.props.t('auth.errors.passwordsNotMatchShort') : 'Passwörter stimmen nicht überein' });
return;
}
if (password.length < 8) {
this.setState({ error: 'Das Passwort muss mindestens 8 Zeichen lang sein' });
this.setState({ error: this.props.t ? this.props.t('auth.passwordMinLength') : 'Das Passwort muss mindestens 8 Zeichen lang sein' });
return;
}
@@ -274,14 +274,14 @@ export class LoginComponent extends Component {
if (response.success) {
this.setState({
loading: false,
success: 'Registrierung erfolgreich. Sie können sich jetzt anmelden.',
success: this.props.t ? this.props.t('auth.success.registerComplete') : 'Registrierung erfolgreich. Sie können sich jetzt anmelden.',
tabValue: 0 // Switch to login tab
});
} else {
let errorMessage = 'Registrierung fehlgeschlagen';
let errorMessage = this.props.t ? this.props.t('auth.errors.registerFailed') : 'Registrierung fehlgeschlagen';
if (response.cause === 'emailExists') {
errorMessage = 'Ein Benutzer mit dieser E-Mail-Adresse existiert bereits. Bitte verwenden Sie eine andere E-Mail-Adresse oder melden Sie sich an.';
errorMessage = this.props.t ? this.props.t('auth.errors.emailExists') : 'Ein Benutzer mit dieser E-Mail-Adresse existiert bereits. Bitte verwenden Sie eine andere E-Mail-Adresse oder melden Sie sich an.';
} else if (response.message) {
errorMessage = response.message;
}
@@ -322,12 +322,12 @@ export class LoginComponent extends Component {
const { email } = this.state;
if (!email) {
this.setState({ error: 'Bitte geben Sie Ihre E-Mail-Adresse ein' });
this.setState({ error: this.props.t ? this.props.t('auth.errors.enterEmail') : 'Bitte geben Sie Ihre E-Mail-Adresse ein' });
return;
}
if (!this.validateEmail(email)) {
this.setState({ error: 'Bitte geben Sie eine gültige E-Mail-Adresse ein' });
this.setState({ error: this.props.t ? this.props.t('auth.errors.invalidEmail') : 'Bitte geben Sie eine gültige E-Mail-Adresse ein' });
return;
}
@@ -342,12 +342,12 @@ export class LoginComponent extends Component {
if (response.success) {
this.setState({
loading: false,
success: 'Ein Link zum Zurücksetzen des Passworts wurde an Ihre E-Mail-Adresse gesendet.'
success: this.props.t ? this.props.t('auth.resetPassword.emailSent') : 'Ein Link zum Zurücksetzen des Passworts wurde an Ihre E-Mail-Adresse gesendet.'
});
} else {
this.setState({
loading: false,
error: response.message || 'Fehler beim Senden der E-Mail'
error: response.message || (this.props.t ? this.props.t('auth.resetPassword.emailError') : 'Fehler beim Senden der E-Mail')
});
}
});
@@ -408,7 +408,7 @@ export class LoginComponent extends Component {
} else {
this.setState({
loading: false,
error: 'Google-Anmeldung fehlgeschlagen',
error: this.props.t ? this.props.t('auth.errors.googleLoginFailed') : 'Google-Anmeldung fehlgeschlagen',
showGoogleAuth: false // Reset Google auth state on failed login
});
}
@@ -418,7 +418,7 @@ export class LoginComponent extends Component {
handleGoogleLoginError = (error) => {
console.error('Google Login Error:', error);
this.setState({
error: 'Google-Anmeldung fehlgeschlagen',
error: this.props.t ? this.props.t('auth.errors.googleLoginFailed') : 'Google-Anmeldung fehlgeschlagen',
showGoogleAuth: false, // Reset Google auth state on error
loading: false
});

View File

@@ -163,8 +163,8 @@ const MainPageLayout = () => {
const allContentBoxes = {
home: [
{ title: t('sections.seeds'), image: "/assets/images/seeds.jpg", bgcolor: "#e1f0d3", link: "/Kategorie/Seeds" },
{ title: t('sections.stecklinge'), image: "/assets/images/cutlings.jpg", bgcolor: "#e8f5d6", link: "/Kategorie/Stecklinge" }
{ title: t('sections.seeds'), image: "/assets/images/seeds.avif", bgcolor: "#e1f0d3", link: "/Kategorie/Seeds" },
{ title: t('sections.stecklinge'), image: "/assets/images/cutlings.avif", bgcolor: "#e8f5d6", link: "/Kategorie/Stecklinge" }
],
aktionen: [
{ title: t('sections.oilPress'), image: "/assets/images/presse.jpg", bgcolor: "#e1f0d3", link: "/presseverleih" },

View File

@@ -10,6 +10,7 @@ import {
} from '@mui/material';
import Delete from '@mui/icons-material/Delete';
import CloudUpload from '@mui/icons-material/CloudUpload';
import { withI18n } from '../i18n/withTranslation.js';
class PhotoUpload extends Component {
constructor(props) {
@@ -30,7 +31,7 @@ class PhotoUpload extends Component {
// Validate file count
if (this.state.files.length + selectedFiles.length > maxFiles) {
this.setState({
error: `Maximal ${maxFiles} Dateien erlaubt`
error: this.props.t("productDialogs.photoUploadErrorMaxFiles", { max: maxFiles })
});
return;
}
@@ -43,14 +44,14 @@ class PhotoUpload extends Component {
for (const file of selectedFiles) {
if (!validTypes.includes(file.type)) {
this.setState({
error: 'Nur Bilddateien (JPEG, PNG, GIF, WebP) sind erlaubt'
error: this.props.t("productDialogs.photoUploadErrorFileType")
});
continue;
}
if (file.size > maxSize) {
this.setState({
error: `Datei zu groß. Maximum: ${Math.round(maxSize / (1024 * 1024))}MB`
error: this.props.t("productDialogs.photoUploadErrorFileSize", { maxSize: Math.round(maxSize / (1024 * 1024)) })
});
continue;
}
@@ -167,12 +168,12 @@ class PhotoUpload extends Component {
render() {
const { files, previews, error } = this.state;
const { disabled, label } = this.props;
const { disabled, label, t } = this.props;
return (
<Box>
<Typography variant="body2" sx={{ mb: 1, fontWeight: 500 }}>
{label || 'Fotos anhängen (optional)'}
{label || t("productDialogs.photoUploadLabelDefault")}
</Typography>
<input
@@ -192,7 +193,7 @@ class PhotoUpload extends Component {
disabled={disabled}
sx={{ mb: 2 }}
>
Fotos auswählen
{t("productDialogs.photoUploadSelect")}
</Button>
{error && (
@@ -228,7 +229,7 @@ class PhotoUpload extends Component {
size="small"
onClick={() => this.handleRemoveFile(index)}
disabled={disabled}
aria-label="Bild entfernen"
aria-label={t("productDialogs.photoUploadRemove")}
sx={{
position: 'absolute',
top: 4,
@@ -269,10 +270,10 @@ class PhotoUpload extends Component {
{files.length > 0 && (
<Typography variant="caption" color="text.secondary" sx={{ mt: 1, display: 'block' }}>
{files.length} Datei(en) ausgewählt
{t("productDialogs.photoUploadSelectedFiles", { count: files.length })}
{previews.length > 0 && previews.some(p => p.originalSize && p.compressedSize) && (
<span style={{ marginLeft: '8px' }}>
(komprimiert für Upload)
{t("productDialogs.photoUploadCompressed")}
</span>
)}
</Typography>
@@ -282,4 +283,4 @@ class PhotoUpload extends Component {
}
}
export default PhotoUpload;
export default withI18n()(PhotoUpload);

View File

@@ -101,7 +101,7 @@ class Product extends Component {
console.log('loadImagevisSocket', bildId);
window.socketManager.emit('getPic', { bildId, size:'small' }, (res) => {
if(res.success){
window.smallPicCache[bildId] = URL.createObjectURL(new Blob([res.imageBuffer], { type: 'image/jpeg' }));
window.smallPicCache[bildId] = URL.createObjectURL(new Blob([res.imageBuffer], { type: 'image/avif' }));
if (this._isMounted) {
this.setState({image: window.smallPicCache[bildId], loading: false});
} else {

View File

@@ -305,7 +305,7 @@ class ProductDetailPage extends Component {
window.socketManager.emit('getPic', { bildId, size: 'small' }, (res) => {
if (res.success) {
// Cache the image
window.smallPicCache[bildId] = URL.createObjectURL(new Blob([res.imageBuffer], { type: 'image/jpeg' }));
window.smallPicCache[bildId] = URL.createObjectURL(new Blob([res.imageBuffer], { type: 'image/avif' }));
// Update state
this.setState(prevState => ({
@@ -546,7 +546,7 @@ class ProductDetailPage extends Component {
console.log("getAttributePicture", res);
if (res.success && !res.noPicture) {
const blob = new Blob([res.imageBuffer], {
type: "image/jpeg",
type: "image/avif",
});
const url = URL.createObjectURL(blob);
@@ -599,13 +599,16 @@ class ProductDetailPage extends Component {
const productData = res.translatedProduct || res.product;
productData.seoName = this.props.seoName;
// Use translated attributes if available
const attributesData = res.translatedAttributes || res.attributes;
// Initialize cache if it doesn't exist
if (!window.productDetailCache) {
window.productDetailCache = {};
}
// Cache the complete response data (product + attributes) - cache the response with translated product
const cacheData = { ...res, product: productData };
const cacheData = { ...res, product: productData, attributes: attributesData };
window.productDetailCache[cacheKey] = cacheData;
// Clean up prerender fallback since we now have real data
@@ -628,7 +631,7 @@ class ProductDetailPage extends Component {
upgrading: false, // Clear upgrading state since we now have complete data
error: null,
imageDialogOpen: false,
attributes: res.attributes,
attributes: attributesData,
komponenten: komponenten,
komponentenLoaded: komponenten.length === 0, // If no komponenten, mark as loaded
similarProducts: res.similarProducts || []
@@ -653,7 +656,7 @@ class ProductDetailPage extends Component {
console.log("getProductView", res);
// Load attribute images
this.loadAttributeImages(res.attributes);
this.loadAttributeImages(attributesData);
} else {
console.error(
"Error loading product:",
@@ -762,7 +765,7 @@ class ProductDetailPage extends Component {
handleEmbedShare = () => {
const embedCode = `<iframe src="${this.getProductUrl()}" width="100%" height="600" frameborder="0"></iframe>`;
navigator.clipboard.writeText(embedCode).then(() => {
this.showSnackbar("Einbettungscode wurde in die Zwischenablage kopiert!");
this.showSnackbar(this.props.t ? this.props.t("productDialogs.shareSuccessEmbed") : "Einbettungscode wurde in die Zwischenablage kopiert!");
}).catch(() => {
// Fallback for older browsers
try {
@@ -772,9 +775,9 @@ class ProductDetailPage extends Component {
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
this.showSnackbar("Einbettungscode wurde in die Zwischenablage kopiert!");
this.showSnackbar(this.props.t ? this.props.t("productDialogs.shareSuccessEmbed") : "Einbettungscode wurde in die Zwischenablage kopiert!");
} catch {
this.showSnackbar("Fehler beim Kopieren des Einbettungscodes", "error");
this.showSnackbar(this.props.t ? this.props.t("productDialogs.shareErrorEmbed") : "Fehler beim Kopieren des Einbettungscodes", "error");
}
});
this.handleShareClose();
@@ -782,7 +785,10 @@ class ProductDetailPage extends Component {
handleWhatsAppShare = () => {
const url = this.getProductUrl();
const text = `Schau dir dieses Produkt an: ${cleanProductName(this.state.product.name)}`;
const productName = cleanProductName(this.state.product.name);
const text = this.props.t
? this.props.t("productDialogs.shareWhatsAppText", { name: productName })
: `Schau dir dieses Produkt an: ${productName}`;
const whatsappUrl = `https://wa.me/?text=${encodeURIComponent(text + ' ' + url)}`;
window.open(whatsappUrl, '_blank');
this.handleShareClose();
@@ -797,7 +803,10 @@ class ProductDetailPage extends Component {
handleTelegramShare = () => {
const url = this.getProductUrl();
const text = `Schau dir dieses Produkt an: ${cleanProductName(this.state.product.name)}`;
const productName = cleanProductName(this.state.product.name);
const text = this.props.t
? this.props.t("productDialogs.shareTelegramText", { name: productName })
: `Schau dir dieses Produkt an: ${productName}`;
const telegramUrl = `https://t.me/share/url?url=${encodeURIComponent(url)}&text=${encodeURIComponent(text)}`;
window.open(telegramUrl, '_blank');
this.handleShareClose();
@@ -805,8 +814,18 @@ class ProductDetailPage extends Component {
handleEmailShare = () => {
const url = this.getProductUrl();
const subject = `Produktempfehlung: ${cleanProductName(this.state.product.name)}`;
const body = `Hallo,\n\nich möchte dir dieses Produkt empfehlen:\n\n${cleanProductName(this.state.product.name)}\n${url}\n\nViele Grüße`;
const productName = cleanProductName(this.state.product.name);
const subject = this.props.t
? `${this.props.t("productDialogs.shareEmailSubject")}: ${productName}`
: `Produktempfehlung: ${productName}`;
const body = this.props.t
? this.props.t("productDialogs.shareEmailBody", {
name: productName,
url: url
})
: `Hallo,\n\nich möchte dir dieses Produkt empfehlen:\n\n${productName}\n${url}\n\nViele Grüße`;
const emailUrl = `mailto:?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
window.location.href = emailUrl;
this.handleShareClose();
@@ -815,7 +834,7 @@ class ProductDetailPage extends Component {
handleLinkCopy = () => {
const url = this.getProductUrl();
navigator.clipboard.writeText(url).then(() => {
this.showSnackbar("Link wurde in die Zwischenablage kopiert!");
this.showSnackbar(this.props.t ? this.props.t("productDialogs.shareSuccessLink") : "Link wurde in die Zwischenablage kopiert!");
}).catch(() => {
// Fallback for older browsers
try {
@@ -825,7 +844,7 @@ class ProductDetailPage extends Component {
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
this.showSnackbar("Link wurde in die Zwischenablage kopiert!");
this.showSnackbar(this.props.t ? this.props.t("productDialogs.shareSuccessLink") : "Link wurde in die Zwischenablage kopiert!");
} catch {
this.showSnackbar("Fehler beim Kopieren des Links", "error");
}
@@ -968,7 +987,7 @@ class ProductDetailPage extends Component {
}).format(productData.price)}
</Typography>
<Typography variant="caption" color="text.secondary">
inkl. MwSt.
{this.props.t ? this.props.t('product.inclVatSimple') : 'inkl. MwSt.'}
</Typography>
</Box>
</Box>
@@ -1047,7 +1066,7 @@ class ProductDetailPage extends Component {
console.log('loadEmbeddedProductImage response:', articleNr, res.success);
if (res.success) {
const imageUrl = URL.createObjectURL(new Blob([res.imageBuffer], { type: 'image/jpeg' }));
const imageUrl = URL.createObjectURL(new Blob([res.imageBuffer], { type: 'image/avif' }));
this.setState(prevState => {
console.log('Setting embedded product image for', articleNr);
return {
@@ -1331,7 +1350,7 @@ class ProductDetailPage extends Component {
whiteSpace: "nowrap"
}}
>
Frage zum Artikel
{this.props.t ? this.props.t('productDialogs.questionTitle') : "Frage zum Artikel"}
</Button>
<Button
variant="outlined"
@@ -1345,7 +1364,7 @@ class ProductDetailPage extends Component {
whiteSpace: "nowrap"
}}
>
Artikel Bewerten
{this.props.t ? this.props.t('productDialogs.ratingTitle') : "Artikel Bewerten"}
</Button>
{(product.available !== 1 && product.availableSupplier !== 1) && (
<Button
@@ -1366,7 +1385,7 @@ class ProductDetailPage extends Component {
}
}}
>
Verfügbarkeit anfragen
{this.props.t ? this.props.t('productDialogs.availabilityTitle') : "Verfügbarkeit anfragen"}
</Button>
)}
</Stack>
@@ -1595,7 +1614,7 @@ class ProductDetailPage extends Component {
}}
size="small"
>
Teilen
{this.props.t ? this.props.t("productDialogs.shareTitle") : "Teilen"}
</Button>
<Box
sx={{
@@ -1669,7 +1688,7 @@ class ProductDetailPage extends Component {
<ListItemIcon>
<CodeIcon fontSize="small" />
</ListItemIcon>
<ListItemText primary="Einbetten" />
<ListItemText primary={this.props.t ? this.props.t("productDialogs.shareEmbed") : "Einbetten"} />
</MenuItem>
<MenuItem onClick={this.handleWhatsAppShare}>
<ListItemIcon>
@@ -1699,7 +1718,7 @@ class ProductDetailPage extends Component {
<ListItemIcon>
<LinkIcon fontSize="small" />
</ListItemIcon>
<ListItemText primary="Link kopieren" />
<ListItemText primary={this.props.t ? this.props.t("productDialogs.shareCopyLink") : "Link kopieren"} />
</MenuItem>
</MenuList>
</Box>
@@ -1953,7 +1972,7 @@ class ProductDetailPage extends Component {
gap: 2
}}>
{this.state.similarProducts.map((similarProductData, index) => {
const product = similarProductData.product;
const product = similarProductData.translatedProduct || similarProductData.product;
return (
<Box key={product.id} sx={{ display: 'flex', justifyContent: 'center' }}>
<Product

View File

@@ -209,7 +209,7 @@ class ProductFilters extends Component {
color: 'primary.main'
}}
>
{this.props.dataParam}
{this.props.categoryName}
</Typography>
)}

View File

@@ -1,4 +1,5 @@
import React from 'react';
import { Link } from 'react-router-dom';
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import IconButton from "@mui/material/IconButton";
@@ -27,7 +28,7 @@ class SharedCarousel extends React.Component {
constructor(props) {
super(props);
const { i18n } = props;
// Don't load categories in constructor - will be loaded in componentDidMount with correct language
this.state = {
categories: [],
@@ -41,7 +42,7 @@ class SharedCarousel extends React.Component {
componentDidMount() {
this._isMounted = true;
const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n.language;
// ALWAYS reload categories to ensure correct language
console.log("SharedCarousel componentDidMount: ALWAYS RELOADING categories for language", currentLanguage);
window.categoryService.get(209, currentLanguage).then((response) => {
@@ -60,12 +61,12 @@ class SharedCarousel extends React.Component {
componentDidUpdate(prevProps) {
console.log("componentDidUpdate", prevProps.languageContext?.currentLanguage, this.props.languageContext?.currentLanguage);
if(prevProps.languageContext?.currentLanguage !== this.props.languageContext?.currentLanguage) {
this.setState({ categories: [] },() => {
window.categoryService.get(209,this.props.languageContext?.currentLanguage || this.props.i18n.language).then((response) => {
if (prevProps.languageContext?.currentLanguage !== this.props.languageContext?.currentLanguage) {
this.setState({ categories: [] }, () => {
window.categoryService.get(209, this.props.languageContext?.currentLanguage || this.props.i18n.language).then((response) => {
console.log("response", response);
if (response.children && response.children.length > 0) {
this.originalCategories = response.children;
this.originalCategories = response.children;
this.categories = [...response.children, ...response.children];
this.setState({ categories: this.categories });
this.startAutoScroll();
@@ -123,7 +124,7 @@ class SharedCarousel extends React.Component {
showScrollbarFlash = () => {
this.clearScrollbarTimer();
this.setState({ showScrollbar: true });
this.scrollbarTimer = setTimeout(() => {
if (this._isMounted) {
this.setState({ showScrollbar: false });
@@ -133,7 +134,7 @@ class SharedCarousel extends React.Component {
handleAutoScroll = () => {
if (!this.autoScrollActive || this.originalCategories.length === 0) return;
this.translateX -= AUTO_SCROLL_SPEED;
this.updateTrackTransform();
@@ -172,7 +173,7 @@ class SharedCarousel extends React.Component {
scrollBy = (direction) => {
if (this.originalCategories.length === 0) return;
// direction: 1 = left (scroll content right), -1 = right (scroll content left)
const originalItemCount = this.originalCategories.length;
const maxScroll = ITEM_WIDTH * originalItemCount;
@@ -189,7 +190,7 @@ class SharedCarousel extends React.Component {
}
this.updateTrackTransform();
// Force scrollbar to update immediately after wrap-around
if (this.state.showScrollbar) {
this.forceUpdate();
@@ -204,11 +205,11 @@ class SharedCarousel extends React.Component {
const originalItemCount = this.originalCategories.length;
const viewportWidth = 1080; // carousel container max-width
const itemsInView = Math.floor(viewportWidth / ITEM_WIDTH);
// Calculate which item is currently at the left edge (first visible)
// Map translateX directly to item index using the same logic as scrollBy
let currentItemIndex;
if (this.translateX === 0) {
// At the beginning - item 0 is visible
currentItemIndex = 0;
@@ -221,10 +222,10 @@ class SharedCarousel extends React.Component {
// Normal negative scrolling - calculate which item is at left edge
currentItemIndex = Math.floor(Math.abs(this.translateX) / ITEM_WIDTH);
}
// Ensure we stay within bounds
currentItemIndex = Math.max(0, Math.min(currentItemIndex, originalItemCount - 1));
// Calculate scrollbar position: 0% when item 0 is first visible, 100% when last item is first visible
const lastPossibleFirstItem = Math.max(0, originalItemCount - itemsInView);
const thumbPosition = lastPossibleFirstItem > 0 ? Math.min((currentItemIndex / lastPossibleFirstItem) * 100, 100) : 0;
@@ -268,25 +269,41 @@ class SharedCarousel extends React.Component {
const { t } = this.props;
const { categories } = this.state;
if(!categories || categories.length === 0) {
if (!categories || categories.length === 0) {
return null;
}
return (
<Box sx={{ mt: 3 }}>
<Typography
variant="h4"
component="h1"
<Box
component={Link}
to="/Kategorien"
sx={{
mb: 2,
fontFamily: "SwashingtonCP",
display: "flex",
alignItems: "center",
justifyContent: "center",
textDecoration: "none",
color: "primary.main",
textAlign: "center",
textShadow: "3px 3px 10px rgba(0, 0, 0, 0.4)"
mb: 2,
transition: "all 0.3s ease",
"&:hover": {
transform: "translateX(5px)",
color: "primary.dark"
}
}}
>
{t('navigation.categories')}
</Typography>
<Typography
variant="h4"
component="span"
sx={{
fontFamily: "SwashingtonCP",
textShadow: "3px 3px 10px rgba(0, 0, 0, 0.4)"
}}
>
{t('navigation.categories')}
</Typography>
<ChevronRight sx={{ fontSize: "2.5rem", ml: 1 }} />
</Box>
<div
className="carousel-wrapper"
@@ -394,7 +411,7 @@ class SharedCarousel extends React.Component {
</div>
))}
</div>
{/* Virtual Scrollbar */}
{this.renderVirtualScrollbar()}
</div>

View File

@@ -63,7 +63,7 @@ class ExtrasSelector extends Component {
this.loadingImages.add(bildId);
window.socketManager.emit('getPic', { bildId, size:'small' }, (res) => {
if (res.success) {
window.smallPicCache[bildId] = URL.createObjectURL(new Blob([res.imageBuffer], { type: 'image/jpeg' }));
window.smallPicCache[bildId] = URL.createObjectURL(new Blob([res.imageBuffer], { type: 'image/avif' }));
this.forceUpdate();
}
this.loadingImages.delete(bildId);

View File

@@ -5,6 +5,7 @@ import CardContent from '@mui/material/CardContent';
import Typography from '@mui/material/Typography';
import Box from '@mui/material/Box';
import Chip from '@mui/material/Chip';
import { withI18n } from '../../i18n/withTranslation.js';
class TentShapeSelector extends Component {
// Generate plant layout based on tent shape
@@ -180,12 +181,20 @@ class TentShapeSelector extends Component {
</Box>
<Typography variant="body2" color="text.secondary" gutterBottom>
{shape.description}
{this.props.t && shape.descriptionKey ? this.props.t(shape.descriptionKey) : shape.description}
</Typography>
<Box sx={{ mt: 2 }}>
<Chip
label={`${shape.minPlants}-${shape.maxPlants} Pflanzen`}
label={this.props.t
? (
shape.minPlants === 1 && shape.maxPlants === 2 ? this.props.t("kitConfig.plants1to2") :
shape.minPlants === 2 && shape.maxPlants === 4 ? this.props.t("kitConfig.plants2to4") :
shape.minPlants === 4 && shape.maxPlants === 6 ? this.props.t("kitConfig.plants4to6") :
shape.minPlants === 3 && shape.maxPlants === 6 ? this.props.t("kitConfig.plants3to6") :
`${shape.minPlants}-${shape.maxPlants} Pflanzen`
)
: `${shape.minPlants}-${shape.maxPlants} Pflanzen`}
size="small"
sx={{
bgcolor: isSelected ? '#2e7d32' : '#f0f0f0',
@@ -205,7 +214,7 @@ class TentShapeSelector extends Component {
transition: 'opacity 0.3s ease'
}}
>
Ausgewählt
{this.props.t ? this.props.t("kitConfig.selected") : "✓ Ausgewählt"}
</Typography>
</Box>
</CardContent>
@@ -238,4 +247,4 @@ class TentShapeSelector extends Component {
}
}
export default TentShapeSelector;
export default withI18n()(TentShapeSelector);

View File

@@ -32,9 +32,9 @@ class CategoryList extends Component {
console.log(" i18n.language:", this.props.i18n?.language);
console.log(" sessionStorage i18nextLng:", typeof sessionStorage !== 'undefined' ? sessionStorage.getItem('i18nextLng') : 'N/A');
console.log(" localStorage i18nextLng:", typeof localStorage !== 'undefined' ? localStorage.getItem('i18nextLng') : 'N/A');
const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n.language;
// ALWAYS reload categories to ensure correct language
console.log("CategoryList componentDidMount: ALWAYS RELOADING categories for language", currentLanguage);
this.setState({ categories: [] }); // Clear any cached categories
@@ -53,15 +53,15 @@ class CategoryList extends Component {
componentDidUpdate(prevProps) {
console.log("componentDidUpdate", prevProps.languageContext?.currentLanguage, this.props.languageContext?.currentLanguage);
if(prevProps.languageContext?.currentLanguage !== this.props.languageContext?.currentLanguage) {
if (prevProps.languageContext?.currentLanguage !== this.props.languageContext?.currentLanguage) {
this.setState({
categories: [],
activeCategoryId: null
},() => {
window.categoryService.get(209,this.props.languageContext?.currentLanguage || this.props.i18n.language).then((response) => {
}, () => {
window.categoryService.get(209, this.props.languageContext?.currentLanguage || this.props.i18n.language).then((response) => {
console.log("response", response);
if (response.children && response.children.length > 0) {
this.setState({
this.setState({
categories: response.children,
activeCategoryId: this.setLevel1CategoryId(this.props.activeCategoryId)
});
@@ -69,14 +69,14 @@ class CategoryList extends Component {
});
});
}
if (prevProps.activeCategoryId !== this.props.activeCategoryId) {
this.setLevel1CategoryId(this.props.activeCategoryId);
}
if (prevProps.activeCategoryId !== this.props.activeCategoryId) {
this.setLevel1CategoryId(this.props.activeCategoryId);
}
}
setLevel1CategoryId = (input) => {
if(input) {
if (input) {
const language = this.props.languageContext?.currentLanguage || this.props.i18n.language;
const categoryTreeCache = window.categoryService.getSync(209, language);
@@ -136,7 +136,7 @@ class CategoryList extends Component {
this.setState({ activeCategoryId: null });
}
handleMobileMenuToggle = () => {
this.setState(prevState => ({
@@ -173,141 +173,141 @@ class CategoryList extends Component {
py: 0.5, // Add vertical padding to prevent border clipping
}}
>
<Button
component={Link}
to="/"
color="inherit"
size="small"
aria-label="Zur Startseite"
onClick={isMobile ? this.handleMobileCategoryClick : undefined}
sx={{
fontSize: "0.75rem",
textTransform: "none",
whiteSpace: "nowrap",
opacity: 0.9,
mx: isMobile ? 0 : 0.5,
my: 0.25,
minWidth: isMobile ? "100%" : "auto",
borderRadius: 1,
justifyContent: isMobile ? "flex-start" : "center",
transition: "all 0.2s ease",
textShadow: "0 1px 2px rgba(0,0,0,0.3)",
position: "relative",
...(activeCategoryId === null && {
bgcolor: "#fff",
textShadow: "none",
opacity: 1,
}),
"&:hover": {
opacity: 1,
bgcolor: "#fff",
textShadow: "none",
"& .MuiSvgIcon-root": {
color: "#2e7d32 !important",
},
"& .bold-text": {
color: "#2e7d32 !important",
},
"& .thin-text": {
color: "transparent !important",
},
<Button
component={Link}
to="/"
color="inherit"
size="small"
aria-label="Zur Startseite"
onClick={isMobile ? this.handleMobileCategoryClick : undefined}
sx={{
fontSize: "0.75rem",
textTransform: "none",
whiteSpace: "nowrap",
opacity: 0.9,
mx: isMobile ? 0 : 0.5,
my: 0.25,
minWidth: isMobile ? "100%" : "auto",
borderRadius: 1,
justifyContent: isMobile ? "flex-start" : "center",
transition: "all 0.2s ease",
textShadow: "0 1px 2px rgba(0,0,0,0.3)",
position: "relative",
...(activeCategoryId === null && {
bgcolor: "#fff",
textShadow: "none",
opacity: 1,
}),
"&:hover": {
opacity: 1,
bgcolor: "#fff",
textShadow: "none",
"& .MuiSvgIcon-root": {
color: "#2e7d32 !important",
},
}}
>
<HomeIcon sx={{
fontSize: "1rem",
mr: isMobile ? 1 : 0,
color: activeCategoryId === null ? "#2e7d32" : "inherit"
}} />
{isMobile && (
<Box sx={{ position: "relative", display: "inline-block" }}>
{/* Bold text (always rendered to set width) */}
<Box
className="bold-text"
sx={{
fontWeight: "bold",
color: activeCategoryId === null ? "#2e7d32" : "transparent",
position: "relative",
zIndex: 2,
}}
>
{this.props.t ? this.props.t('navigation.home') : 'Startseite'}
</Box>
{/* Thin text (positioned on top) */}
<Box
className="thin-text"
sx={{
fontWeight: "400",
color: activeCategoryId === null ? "transparent" : "inherit",
position: "absolute",
top: 0,
left: 0,
zIndex: 1,
}}
>
{this.props.t ? this.props.t('navigation.home') : 'Startseite'}
</Box>
"& .bold-text": {
color: "#2e7d32 !important",
},
"& .thin-text": {
color: "transparent !important",
},
},
}}
>
<HomeIcon sx={{
fontSize: "1rem",
mr: isMobile ? 1 : 0,
color: activeCategoryId === null ? "#2e7d32" : "inherit"
}} />
{isMobile && (
<Box sx={{ position: "relative", display: "inline-block" }}>
{/* Bold text (always rendered to set width) */}
<Box
className="bold-text"
sx={{
fontWeight: "bold",
color: activeCategoryId === null ? "#2e7d32" : "transparent",
position: "relative",
zIndex: 2,
}}
>
{this.props.t ? this.props.t('navigation.home') : 'Startseite'}
</Box>
)}
</Button>
{/* Thin text (positioned on top) */}
<Box
className="thin-text"
sx={{
fontWeight: "400",
color: activeCategoryId === null ? "transparent" : "inherit",
position: "absolute",
top: 0,
left: 0,
zIndex: 1,
}}
>
{this.props.t ? this.props.t('navigation.home') : 'Startseite'}
</Box>
</Box>
)}
</Button>
<Button
component={Link}
to="/Kategorie/neu"
color="inherit"
size="small"
aria-label="Neuheiten"
onClick={isMobile ? this.handleMobileCategoryClick : undefined}
sx={{
fontSize: "0.75rem",
textTransform: "none",
whiteSpace: "nowrap",
opacity: 0.9,
mx: isMobile ? 0 : 0.5,
my: 0.25,
minWidth: isMobile ? "100%" : "auto",
borderRadius: 1,
justifyContent: isMobile ? "flex-start" : "center",
transition: "all 0.2s ease",
textShadow: "0 1px 2px rgba(0,0,0,0.3)",
position: "relative"
}}
>
<FiberNewIcon sx={{
fontSize: "1rem",
mr: isMobile ? 1 : 0
}} />
{isMobile && (
<Box sx={{ position: "relative", display: "inline-block" }}>
{/* Bold text (always rendered to set width) */}
<Box
className="bold-text"
sx={{
fontWeight: "bold",
color: "transparent",
position: "relative",
zIndex: 2,
}}
>
{this.props.t ? this.props.t('navigation.new') : 'Neuheiten'}
</Box>
{/* Thin text (positioned on top) */}
<Box
className="thin-text"
sx={{
fontWeight: "400",
color: "inherit",
position: "absolute",
top: 0,
left: 0,
zIndex: 1,
}}
>
{this.props.t ? this.props.t('navigation.new') : 'Neuheiten'}
</Box>
<Button
component={Link}
to="/Kategorie/neu"
color="inherit"
size="small"
aria-label="Neuheiten"
onClick={isMobile ? this.handleMobileCategoryClick : undefined}
sx={{
fontSize: "0.75rem",
textTransform: "none",
whiteSpace: "nowrap",
opacity: 0.9,
mx: isMobile ? 0 : 0.5,
my: 0.25,
minWidth: isMobile ? "100%" : "auto",
borderRadius: 1,
justifyContent: isMobile ? "flex-start" : "center",
transition: "all 0.2s ease",
textShadow: "0 1px 2px rgba(0,0,0,0.3)",
position: "relative"
}}
>
<FiberNewIcon sx={{
fontSize: "1rem",
mr: isMobile ? 1 : 0
}} />
{isMobile && (
<Box sx={{ position: "relative", display: "inline-block" }}>
{/* Bold text (always rendered to set width) */}
<Box
className="bold-text"
sx={{
fontWeight: "bold",
color: "transparent",
position: "relative",
zIndex: 2,
}}
>
{this.props.t ? this.props.t('navigation.new') : 'Neuheiten'}
</Box>
)}
</Button>
{/* Thin text (positioned on top) */}
<Box
className="thin-text"
sx={{
fontWeight: "400",
color: "inherit",
position: "absolute",
top: 0,
left: 0,
zIndex: 1,
}}
>
{this.props.t ? this.props.t('navigation.new') : 'Neuheiten'}
</Box>
</Box>
)}
</Button>
{categories.length > 0 ? (
@@ -385,100 +385,100 @@ class CategoryList extends Component {
);
})}
</>
) : ( !isMobile && (
<Typography
variant="caption"
color="inherit"
sx={{
display: "inline-flex",
alignItems: "center",
height: "33px", // Match small button height
px: 1,
fontSize: "0.75rem",
opacity: 0.9,
}}
>
&nbsp;
</Typography>
)
)}
<Button
component={Link}
to="/Konfigurator"
) : (!isMobile && (
<Typography
variant="caption"
color="inherit"
size="small"
aria-label="Zur Startseite"
onClick={isMobile ? this.handleMobileCategoryClick : undefined}
sx={{
display: "inline-flex",
alignItems: "center",
height: "33px", // Match small button height
px: 1,
fontSize: "0.75rem",
textTransform: "none",
whiteSpace: "nowrap",
opacity: 0.9,
mx: isMobile ? 0 : 0.5,
my: 0.25,
minWidth: isMobile ? "100%" : "auto",
borderRadius: 1,
justifyContent: isMobile ? "flex-start" : "center",
transition: "all 0.2s ease",
textShadow: "0 1px 2px rgba(0,0,0,0.3)",
position: "relative",
...(activeCategoryId === null && {
bgcolor: "#fff",
textShadow: "none",
opacity: 1,
}),
"&:hover": {
opacity: 1,
bgcolor: "#fff",
textShadow: "none",
"& .MuiSvgIcon-root": {
color: "#2e7d32 !important",
},
"& .bold-text": {
color: "#2e7d32 !important",
},
"& .thin-text": {
color: "transparent !important",
},
},
}}
>
<SettingsIcon sx={{
fontSize: "1rem",
mr: isMobile ? 1 : 0,
color: activeCategoryId === null ? "#2e7d32" : "inherit"
}} />
{isMobile && (
<Box sx={{ position: "relative", display: "inline-block" }}>
{/* Bold text (always rendered to set width) */}
<Box
className="bold-text"
sx={{
fontWeight: "bold",
color: activeCategoryId === null ? "#2e7d32" : "transparent",
position: "relative",
zIndex: 2,
}}
>
{this.props.t ? this.props.t('navigation.home') : 'Startseite'}
</Box>
{/* Thin text (positioned on top) */}
<Box
className="thin-text"
sx={{
fontWeight: "400",
color: activeCategoryId === null ? "transparent" : "inherit",
position: "absolute",
top: 0,
left: 0,
zIndex: 1,
}}
>
{this.props.t ? this.props.t('navigation.home') : 'Startseite'}
</Box>
&nbsp;
</Typography>
)
)}
<Button
component={Link}
to="/Konfigurator"
color="inherit"
size="small"
aria-label="Zur Startseite"
onClick={isMobile ? this.handleMobileCategoryClick : undefined}
sx={{
fontSize: "0.75rem",
textTransform: "none",
whiteSpace: "nowrap",
opacity: 0.9,
mx: isMobile ? 0 : 0.5,
my: 0.25,
minWidth: isMobile ? "100%" : "auto",
borderRadius: 1,
justifyContent: isMobile ? "flex-start" : "center",
transition: "all 0.2s ease",
textShadow: "0 1px 2px rgba(0,0,0,0.3)",
position: "relative",
...(activeCategoryId === null && {
bgcolor: "#fff",
textShadow: "none",
opacity: 1,
}),
"&:hover": {
opacity: 1,
bgcolor: "#fff",
textShadow: "none",
"& .MuiSvgIcon-root": {
color: "#2e7d32 !important",
},
"& .bold-text": {
color: "#2e7d32 !important",
},
"& .thin-text": {
color: "transparent !important",
},
},
}}
>
<SettingsIcon sx={{
fontSize: "1rem",
mr: isMobile ? 1 : 0,
color: activeCategoryId === null ? "#2e7d32" : "inherit"
}} />
{isMobile && (
<Box sx={{ position: "relative", display: "inline-block" }}>
{/* Bold text (always rendered to set width) */}
<Box
className="bold-text"
sx={{
fontWeight: "bold",
color: activeCategoryId === null ? "#2e7d32" : "transparent",
position: "relative",
zIndex: 2,
}}
>
{this.props.t ? this.props.t('navigation.home') : 'Startseite'}
</Box>
)}
</Button>
{/* Thin text (positioned on top) */}
<Box
className="thin-text"
sx={{
fontWeight: "400",
color: activeCategoryId === null ? "transparent" : "inherit",
position: "absolute",
top: 0,
left: 0,
zIndex: 1,
}}
>
{this.props.t ? this.props.t('navigation.home') : 'Startseite'}
</Box>
</Box>
)}
</Button>
</Box>
);
@@ -516,11 +516,11 @@ class CategoryList extends Component {
>
<Container maxWidth="lg" sx={{ px: 2 }}>
{/* Toggle Button */}
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
py: 1,
cursor: "pointer",
"&:hover": {
@@ -530,7 +530,7 @@ class CategoryList extends Component {
onClick={this.handleMobileMenuToggle}
role="button"
tabIndex={0}
aria-label={this.props.t ?
aria-label={this.props.t ?
(mobileMenuOpen ? this.props.t('navigation.categoriesClose') : this.props.t('navigation.categoriesOpen')) :
(mobileMenuOpen ? "Kategorien schließen" : "Kategorien öffnen")
}
@@ -541,11 +541,11 @@ class CategoryList extends Component {
}
}}
>
<Typography variant="subtitle2" color="inherit" sx={{
<Typography variant="subtitle2" color="inherit" sx={{
fontWeight: "bold",
textShadow: "0 1px 2px rgba(0,0,0,0.3)"
}}>
{this.props.t ? this.props.t('navigation.categories') : 'Kategorien'}
{this.props.t ? this.props.t('navigation.categories') : 'Kategorien'}
</Typography>
<Box sx={{ display: "flex", alignItems: "center" }}>
{mobileMenuOpen ? <CloseIcon /> : <MenuIcon />}

View File

@@ -16,7 +16,7 @@ const Logo = () => {
}}
>
<img
src="/assets/images/sh.png"
src="/assets/images/sh.avif"
alt="SH Logo"
width="108px"
height="45px"

View File

@@ -79,7 +79,7 @@ const SearchBar = () => {
(response) => {
if (response && response.products) {
// getSearchProducts returns response.products array
const suggestions = response.products.slice(0, 8); // Limit to 8 suggestions
const suggestions = response.products.map(p => p.translatedProduct || p).slice(0, 8); // Limit to 8 suggestions
setSuggestions(suggestions);
setShowSuggestions(suggestions.length > 0);
setSelectedIndex(-1); // Reset selection

View File

@@ -31,6 +31,7 @@ const getStatusTranslation = (status, t) => {
new: t ? t('orders.status.new') : "in Bearbeitung",
pending: t ? t('orders.status.pending') : "Neu",
processing: t ? t('orders.status.processing') : "in Bearbeitung",
paid: t ? t('orders.status.paid') : "Bezahlt",
cancelled: t ? t('orders.status.cancelled') : "Storniert",
shipped: t ? t('orders.status.shipped') : "Verschickt",
delivered: t ? t('orders.status.delivered') : "Geliefert",
@@ -39,29 +40,23 @@ const getStatusTranslation = (status, t) => {
};
const statusEmojis = {
"in Bearbeitung": "⚙️",
new: "⚙️",
pending: "⏳",
processing: "🔄",
paid: "🏦",
cancelled: "❌",
Verschickt: "🚚",
Geliefert: "✅",
Storniert: "❌",
Retoure: "↩️",
"Teil Retoure": "↪️",
"Teil geliefert": "⚡",
shipped: "🚚",
delivered: "✅",
};
const statusColors = {
"in Bearbeitung": "#ed6c02", // orange
new: "#ed6c02", // orange
pending: "#ff9800", // orange for pending
processing: "#2196f3", // blue for processing
paid: "#2e7d32", // green
cancelled: "#d32f2f", // red for cancelled
Verschickt: "#2e7d32", // green
Geliefert: "#2e7d32", // green
Storniert: "#d32f2f", // red
Retoure: "#9c27b0", // purple
"Teil Retoure": "#9c27b0", // purple
"Teil geliefert": "#009688", // teal
shipped: "#2e7d32", // green
delivered: "#2e7d32", // green
};
const currencyFormatter = new Intl.NumberFormat("de-DE", {
@@ -229,11 +224,11 @@ const OrdersTab = ({ orderIdFromHash, t }) => {
display: "flex",
alignItems: "center",
gap: "8px",
color: getStatusColor(displayStatus),
color: getStatusColor(order.status),
}}
>
<span style={{ fontSize: "1.2rem" }}>
{getStatusEmoji(displayStatus)}
{getStatusEmoji(order.status)}
</span>
<Typography
variant="body2"
@@ -243,6 +238,18 @@ const OrdersTab = ({ orderIdFromHash, t }) => {
{displayStatus}
</Typography>
</Box>
{order.delivery_method === 'DHL' && order.trackingCode && (
<Box sx={{ mt: 0.5 }}>
<a
href={`https://www.dhl.de/de/privatkunden/dhl-sendungsverfolgung.html?piececode=${order.trackingCode}`}
target="_blank"
rel="noopener noreferrer"
style={{ fontSize: '0.85rem', color: '#d40511' }}
>
📦 {t ? t('orders.trackShipment') : 'Sendung verfolgen'}
</a>
</Box>
)}
</TableCell>
<TableCell>
{order.items

View File

@@ -206,7 +206,7 @@ const config = {
// Images
images: {
logo: "/assets/images/sh.png",
logo: "/assets/images/sh.avif",
placeholder: "/assets/images/nopicture.jpg"
},

View File

@@ -1,9 +1,10 @@
// @note Dummy data for grow tent configurator - no backend calls
// descriptions now keys for translation
export const tentShapes = [
{
id: '60x60',
name: '60x60cm',
description: 'Kompakt - ideal für kleine Räume',
descriptionKey: 'kitConfig.description60x60',
footprint: '60x60',
minPlants: 1,
maxPlants: 2,
@@ -13,7 +14,7 @@ export const tentShapes = [
{
id: '80x80',
name: '80x80cm',
description: 'Mittel - perfekte Balance',
descriptionKey: 'kitConfig.description80x80',
footprint: '80x80',
minPlants: 2,
maxPlants: 4,
@@ -23,7 +24,7 @@ export const tentShapes = [
{
id: '100x100',
name: '100x100cm',
description: 'Groß - für erfahrene Grower',
descriptionKey: 'kitConfig.description100x100',
footprint: '100x100',
minPlants: 4,
maxPlants: 6,
@@ -33,7 +34,7 @@ export const tentShapes = [
{
id: '120x60',
name: '120x60cm',
description: 'Rechteckig - maximale Raumnutzung',
descriptionKey: 'kitConfig.description120x60',
footprint: '120x60',
minPlants: 3,
maxPlants: 6,

View File

@@ -5,6 +5,7 @@ export default {
"profile": "الملف الشخصي",
"email": "البريد الإلكتروني",
"password": "كلمة المرور",
"newPassword": "كلمة المرور الجديدة",
"confirmPassword": "تأكيد كلمة المرور",
"forgotPassword": "هل نسيت كلمة المرور؟",
"loginWithGoogle": "تسجيل الدخول باستخدام جوجل",
@@ -13,6 +14,7 @@ export default {
"privacyPolicy": "سياسة الخصوصية",
"passwordMinLength": "يجب أن تكون كلمة المرور 8 أحرف على الأقل",
"newPasswordMinLength": "يجب أن تكون كلمة المرور الجديدة 8 أحرف على الأقل",
"backToHome": "العودة إلى الصفحة الرئيسية",
"menu": {
"profile": "الملف الشخصي",
"myProfile": "ملفي الشخصي",
@@ -21,5 +23,28 @@ export default {
"settings": "الإعدادات",
"adminDashboard": "لوحة تحكم المسؤول",
"adminUsers": "مستخدمو المسؤول"
},
"resetPassword": {
"title": "إعادة تعيين كلمة المرور",
"button": "إعادة تعيين كلمة المرور",
"success": "تم إعادة تعيين كلمة المرور بنجاح! سيتم توجيهك لتسجيل الدخول قريبًا...",
"invalidToken": "لم يتم العثور على رمز صالح. يرجى استخدام الرابط من بريدك الإلكتروني.",
"error": "حدث خطأ أثناء إعادة تعيين كلمة المرور",
"emailSent": "تم إرسال رابط لإعادة تعيين كلمة المرور إلى بريدك الإلكتروني.",
"emailError": "حدث خطأ أثناء إرسال البريد الإلكتروني"
},
"errors": {
"fillAllFields": "يرجى ملء جميع الحقول",
"invalidEmail": "يرجى إدخال بريد إلكتروني صالح",
"passwordsNotMatch": "كلمات المرور غير متطابقة",
"passwordsNotMatchShort": "كلمات المرور غير متطابقة",
"enterEmail": "يرجى إدخال بريدك الإلكتروني",
"loginFailed": "فشل تسجيل الدخول",
"registerFailed": "فشل التسجيل",
"googleLoginFailed": "فشل تسجيل الدخول عبر جوجل",
"emailExists": "يوجد مستخدم بهذا البريد الإلكتروني بالفعل. يرجى استخدام بريد إلكتروني آخر أو تسجيل الدخول."
},
"success": {
"registerComplete": "تم التسجيل بنجاح. يمكنك الآن تسجيل الدخول."
}
};

View File

@@ -3,6 +3,7 @@ import navigation from './navigation.js';
import auth from './auth.js';
import cart from './cart.js';
import product from './product.js';
import productDialogs from './productDialogs.js';
import search from './search.js';
import sorting from './sorting.js';
import chat from './chat.js';
@@ -18,6 +19,7 @@ import pages from './pages.js';
import orders from './orders.js';
import settings from './settings.js';
import common from './common.js';
import kitConfig from './kitConfig.js';
import legalDatenschutzBasic from './legal-datenschutz-basic.js';
import legalDatenschutzCustomer from './legal-datenschutz-customer.js';
import legalDatenschutzGoogleOrders from './legal-datenschutz-google-orders.js';
@@ -35,6 +37,7 @@ export default {
"auth": auth,
"cart": cart,
"product": product,
"productDialogs": productDialogs,
"search": search,
"sorting": sorting,
"chat": chat,
@@ -50,6 +53,7 @@ export default {
"orders": orders,
"settings": settings,
"common": common,
"kitConfig": kitConfig,
"legalDatenschutzBasic": legalDatenschutzBasic,
"legalDatenschutzCustomer": legalDatenschutzCustomer,
"legalDatenschutzGoogleOrders": legalDatenschutzGoogleOrders,

View File

@@ -0,0 +1,43 @@
export default {
"pageTitle": "🌱 مُكوّن جروبوكس",
"pageSubtitle": "ركّب إعداد النمو الداخلي المثالي بتاعك",
"bundleDiscountTitle": "🎯 احصل على خصم الباقة!",
"loadingProducts": "جارٍ تحميل منتجات الجروبوكس...",
"loadingLighting": "جارٍ تحميل منتجات الإضاءة...",
"loadingVentilation": "جارٍ تحميل منتجات التهوية...",
"loadingExtras": "جارٍ تحميل الإضافات...",
"noProductsAvailable": "لا توجد منتجات متاحة لهذا الحجم",
"noLightingAvailable": "لا توجد أضواء مناسبة لحجم الخيمة {{shape}}.",
"noVentilationAvailable": "لا توجد تهوية مناسبة لحجم الخيمة {{shape}}.",
"noExtrasAvailable": "لا توجد إضافات متاحة",
"selectShapeTitle": "1. اختر شكل الجروبوكس",
"selectShapeSubtitle": "اختار أولاً مساحة قاعدة الجروبوكس بتاعتك",
"selectProductTitle": "2. اختر منتج الجروبوكس",
"selectProductSubtitle": "اختار المنتج المناسب لجروبوكس {{shape}} بتاعك",
"selectLightingTitle": "3. اختر الإضاءة",
"selectLightingTitleShape": "3. اختر الإضاءة - {{shape}}",
"selectLightingSubtitle": "من فضلك اختار حجم الخيمة الأول.",
"selectVentilationTitle": "4. اختر التهوية",
"selectVentilationTitleShape": "4. اختر التهوية - {{shape}}",
"selectVentilationSubtitle": "من فضلك اختار حجم الخيمة الأول.",
"selectExtrasTitle": "5. أضف إضافات (اختياري)",
"yourConfiguration": "🎯 التكوين بتاعك",
"growboxLabel": "جروبوكس: {{name}}",
"lightingLabel": "الإضاءة: {{name}}",
"ventilationLabel": "التهوية: {{name}}",
"extraLabel": "إضافة: {{name}}",
"totalPrice": "السعر الكلي:",
"addToCart": "أضف إلى السلة",
"selected": "✓ تم الاختيار",
"notDeliverable": "غير متوفر للتوصيل",
"noPrice": "لا يوجد سعر",
"setName": "طقم جروبوكس - {{shape}}",
"description60x60": "مُدمج - مثالي للمساحات الصغيرة",
"description80x80": "متوسط - توازن مثالي",
"description100x100": "كبير - للمزارعين المتمرسين",
"description120x60": "مستطيل - استخدام أقصى للمساحة",
"plants1to2": "1-2 نباتات",
"plants2to4": "2-4 نباتات",
"plants4to6": "4-6 نباتات",
"plants3to6": "3-6 نباتات"
};

View File

@@ -3,7 +3,8 @@ export default {
"new": "قيد التنفيذ",
"pending": "جديد",
"processing": "قيد التنفيذ",
"cancelled": لغاة",
"paid": دفوع",
"cancelled": "ملغي",
"shipped": "تم الشحن",
"delivered": "تم التوصيل",
"return": "إرجاع",
@@ -24,6 +25,7 @@ export default {
"cancelOrder": "إلغاء الطلب"
},
"noOrders": "لم تقم بوضع أي طلبات بعد.",
"trackShipment": "تتبع الشحنة",
"details": {
"title": "تفاصيل الطلب: {{orderId}}",
"deliveryAddress": "عنوان التوصيل",
@@ -36,14 +38,13 @@ export default {
"item": "العنصر",
"quantity": "الكمية",
"price": "السعر",
"vat": "ضريبة القيمة المضافة",
"total": "الإجمالي",
"cancelOrder": "إلغاء الطلب"
},
"cancelConfirm": {
"title": "إلغاء الطلب",
"message": "هل أنت متأكد أنك تريد إلغاء هذا الطلب؟",
"confirm": "إلغاء الطلب",
"confirm": "إلغاء",
"cancelling": "جارٍ الإلغاء..."
},
"processing": "يتم إكمال الطلب..."

View File

@@ -5,9 +5,10 @@ export default {
"notFoundDescription": "المنتج الذي تبحث عنه غير موجود أو تم إزالته.",
"backToHome": "العودة إلى الصفحة الرئيسية",
"error": "خطأ",
"articleNumber": "رقم الصنف",
"articleNumber": "رقم المنتج",
"manufacturer": "الشركة المصنعة",
"inclVat": "شامل {{vat}}% ضريبة القيمة المضافة",
"inclVatSimple": "شامل ضريبة القيمة المضافة",
"priceUnit": "{{price}}/{{unit}}",
"new": "جديد",
"weeks": "أسابيع",
@@ -15,7 +16,7 @@ export default {
"inclVatFooter": "شامل {{vat}}% ضريبة القيمة المضافة,*",
"availability": "التوفر",
"inStock": "متوفر في المخزون",
"comingSoon": "قريبًا متوفر",
"comingSoon": "قريبًا",
"deliveryTime": "مدة التوصيل",
"inclShort": "شامل",
"vatShort": "ضريبة القيمة المضافة",
@@ -32,10 +33,10 @@ export default {
"similarProducts": "منتجات مشابهة",
"countDisplay": {
"noProducts": "0 منتجات",
"oneProduct": "منتج واحد",
"oneProduct": "1 منتج",
"multipleProducts": "{{count}} منتجات",
"filteredProducts": "{{filtered}} من {{total}} منتجات",
"filteredOneProduct": "{{filtered}} من منتج واحد",
"filteredOneProduct": "{{filtered}} من 1 منتج",
"xOfYProducts": "{{x}} من {{y}} منتجات"
},
"removeFiltersToSee": "قم بإزالة الفلاتر لرؤية المنتجات",

View File

@@ -0,0 +1,61 @@
export default {
"questionTitle": "سؤال عن المنتج",
"questionSubtitle": "هل لديك سؤال عن هذا المنتج؟ نحن سعداء بمساعدتك.",
"questionSuccess": "شكرًا على سؤالك! سنرد عليك في أقرب وقت ممكن.",
"nameLabel": "الاسم",
"namePlaceholder": "اسمك",
"emailLabel": "البريد الإلكتروني",
"emailPlaceholder": "your.email@example.com",
"questionLabel": "سؤالك",
"questionPlaceholder": "صف سؤالك عن هذا المنتج...",
"photosLabelQuestion": "أرفق صورًا مع سؤالك (اختياري)",
"submitQuestion": "إرسال السؤال",
"sending": "جارٍ الإرسال...",
"ratingTitle": "قيم المنتج",
"ratingSubtitle": "شارك تجربتك مع هذا المنتج وساعد العملاء الآخرين في اتخاذ قرارهم.",
"ratingSuccess": "شكرًا على تقييمك! سيتم نشره بعد المراجعة.",
"emailHelper": "لن يتم نشر بريدك الإلكتروني",
"ratingLabel": "التقييم *",
"pleaseRate": "يرجى التقييم",
"ratingStars": "{{rating}} من 5 نجوم",
"reviewLabel": "تقييمك (اختياري)",
"reviewPlaceholder": "صف تجاربك مع هذا المنتج...",
"photosLabelRating": "أرفق صورًا مع تقييمك (اختياري)",
"submitRating": "إرسال التقييم",
"errorGeneric": "حدث خطأ",
"errorPhotos": "خطأ في معالجة الصور",
"availabilityTitle": "طلب التوفر",
"availabilitySubtitle": "هذا المنتج غير متوفر حاليًا. سنكون سعداء بإبلاغك بمجرد عودته للمخزون.",
"availabilitySuccessEmail": "شكرًا على طلبك! سنخطرك عبر البريد الإلكتروني بمجرد توفر المنتج مرة أخرى.",
"availabilitySuccessTelegram": "شكرًا على طلبك! سنخطرك عبر تيليجرام بمجرد توفر المنتج مرة أخرى.",
"notificationMethodLabel": "كيف تود أن يتم إعلامك؟",
"telegramBotLabel": "بوت تيليجرام",
"telegramIdLabel": "معرف تيليجرام",
"telegramPlaceholder": "@اسمكعلىتيليجرام أو معرف تيليجرام",
"telegramHelper": "أدخل اسم المستخدم الخاص بك على تيليجرام (مع @) أو معرف تيليجرام",
"messageLabel": "رسالة (اختياري)",
"messagePlaceholder": "معلومات إضافية أو أسئلة...",
"submitAvailability": "طلب التوفر",
"photoUploadSelect": "اختر الصور",
"photoUploadErrorMaxFiles": "الحد الأقصى {{max}} ملفات مسموح بها",
"photoUploadErrorFileType": "مسموح فقط بملفات الصور (JPEG, PNG, GIF, WebP)",
"photoUploadErrorFileSize": "الملف كبير جدًا. الحد الأقصى: {{maxSize}} ميجابايت",
"photoUploadSelectedFiles": "{{count}} ملف(ملفات) مختارة",
"photoUploadCompressed": "(تم الضغط للرفع)",
"photoUploadRemove": "إزالة الصورة",
"photoUploadLabelDefault": "أرفق صورًا (اختياري)",
"shareTitle": "مشاركة",
"shareEmbed": "تضمين",
"shareCopyLink": "نسخ الرابط",
"shareSuccessEmbed": "تم نسخ كود التضمين إلى الحافظة!",
"shareErrorEmbed": "حدث خطأ أثناء نسخ كود التضمين",
"shareSuccessLink": "تم نسخ الرابط إلى الحافظة!",
"shareWhatsAppText": "شوف المنتج ده: {{name}}",
"shareTelegramText": "شوف المنتج ده: {{name}}",
"shareEmailSubject": "توصية بمنتج",
"shareEmailBody": "مرحبًا،\n\nحابب أوصي لك بالمنتج ده:\n\n{{name}}\n{{url}}\n\nمع أطيب التحيات"
};

View File

@@ -5,14 +5,16 @@ export default {
"profile": "Профил",
"email": "Имейл",
"password": "Парола",
"newPassword": "Нова парола",
"confirmPassword": "Потвърдете паролата",
"forgotPassword": "Забравена парола?",
"loginWithGoogle": "Вход с Google",
"or": "ИЛИ",
"privacyAccept": "С натискане на \"Вход с Google\" приемам",
"privacyAccept": "С натискането на \"Вход с Google\" приемам",
"privacyPolicy": "Политиката за поверителност",
"passwordMinLength": "Паролата трябва да е поне 8 символа",
"newPasswordMinLength": "Новата парола трябва да е поне 8 символа",
"backToHome": "Обратно към началната страница",
"menu": {
"profile": "Профил",
"myProfile": "Моят профил",
@@ -21,5 +23,28 @@ export default {
"settings": "Настройки",
"adminDashboard": "Админ табло",
"adminUsers": "Админ потребители"
},
"resetPassword": {
"title": "Нулиране на парола",
"button": "Нулиране на парола",
"success": "Вашата парола беше успешно нулирана! Скоро ще бъдете пренасочени към вход...",
"invalidToken": "Няма валиден токен. Моля, използвайте линка от имейла си.",
"error": "Грешка при нулиране на паролата",
"emailSent": "Линк за нулиране на паролата беше изпратен на вашия имейл.",
"emailError": "Грешка при изпращане на имейла"
},
"errors": {
"fillAllFields": "Моля, попълнете всички полета",
"invalidEmail": "Моля, въведете валиден имейл адрес",
"passwordsNotMatch": "Паролите не съвпадат",
"passwordsNotMatchShort": "Паролите не съвпадат",
"enterEmail": "Моля, въведете вашия имейл адрес",
"loginFailed": "Входът не бе успешен",
"registerFailed": "Регистрацията не бе успешна",
"googleLoginFailed": "Вход с Google не бе успешен",
"emailExists": "Потребител с този имейл вече съществува. Моля, използвайте друг имейл или влезте в системата."
},
"success": {
"registerComplete": "Регистрацията беше успешна. Сега можете да влезете."
}
};

View File

@@ -3,6 +3,7 @@ import navigation from './navigation.js';
import auth from './auth.js';
import cart from './cart.js';
import product from './product.js';
import productDialogs from './productDialogs.js';
import search from './search.js';
import sorting from './sorting.js';
import chat from './chat.js';
@@ -18,6 +19,7 @@ import pages from './pages.js';
import orders from './orders.js';
import settings from './settings.js';
import common from './common.js';
import kitConfig from './kitConfig.js';
import legalDatenschutzBasic from './legal-datenschutz-basic.js';
import legalDatenschutzCustomer from './legal-datenschutz-customer.js';
import legalDatenschutzGoogleOrders from './legal-datenschutz-google-orders.js';
@@ -35,6 +37,7 @@ export default {
"auth": auth,
"cart": cart,
"product": product,
"productDialogs": productDialogs,
"search": search,
"sorting": sorting,
"chat": chat,
@@ -50,6 +53,7 @@ export default {
"orders": orders,
"settings": settings,
"common": common,
"kitConfig": kitConfig,
"legalDatenschutzBasic": legalDatenschutzBasic,
"legalDatenschutzCustomer": legalDatenschutzCustomer,
"legalDatenschutzGoogleOrders": legalDatenschutzGoogleOrders,

View File

@@ -0,0 +1,43 @@
export default {
"pageTitle": "🌱 Конфигуратор за Growbox",
"pageSubtitle": "Създайте перфектната си вътрешна система за отглеждане",
"bundleDiscountTitle": "🎯 Вземете отстъпка за комплект!",
"loadingProducts": "Зареждане на продукти за growbox...",
"loadingLighting": "Зареждане на осветителни продукти...",
"loadingVentilation": "Зареждане на вентилационни продукти...",
"loadingExtras": "Зареждане на допълнителни продукти...",
"noProductsAvailable": "Няма налични продукти за този размер",
"noLightingAvailable": "Няма подходящи лампи за размер на палатка {{shape}}.",
"noVentilationAvailable": "Няма подходяща вентилация за размер на палатка {{shape}}.",
"noExtrasAvailable": "Няма налични допълнения",
"selectShapeTitle": "1. Изберете форма на growbox",
"selectShapeSubtitle": "Първо изберете основната площ на вашия growbox",
"selectProductTitle": "2. Изберете продукт за growbox",
"selectProductSubtitle": "Изберете подходящия продукт за вашия {{shape}} growbox",
"selectLightingTitle": "3. Изберете осветление",
"selectLightingTitleShape": "3. Изберете осветление - {{shape}}",
"selectLightingSubtitle": "Моля, първо изберете размер на палатка.",
"selectVentilationTitle": "4. Изберете вентилация",
"selectVentilationTitleShape": "4. Изберете вентилация - {{shape}}",
"selectVentilationSubtitle": "Моля, първо изберете размер на палатка.",
"selectExtrasTitle": "5. Добавете допълнения (по избор)",
"yourConfiguration": "🎯 Вашата конфигурация",
"growboxLabel": "Growbox: {{name}}",
"lightingLabel": "Осветление: {{name}}",
"ventilationLabel": "Вентилация: {{name}}",
"extraLabel": "Допълнение: {{name}}",
"totalPrice": "Обща цена:",
"addToCart": "Добави в количката",
"selected": "✓ Избрано",
"notDeliverable": "Не е налично за доставка",
"noPrice": "Няма цена",
"setName": "Комплект Growbox - {{shape}}",
"description60x60": "Компактен - идеален за малки пространства",
"description80x80": "Среден - перфектен баланс",
"description100x100": "Голям - за опитни отглеждачи",
"description120x60": "Правоъгълен - максимално използване на пространството",
"plants1to2": "1-2 растения",
"plants2to4": "2-4 растения",
"plants4to6": "4-6 растения",
"plants3to6": "3-6 растения"
};

View File

@@ -1,14 +1,15 @@
export default {
"status": {
"new": "В процес",
"pending": "Нова",
"processing": "В процес",
"cancelled": "Отменена",
"shipped": "Изпратена",
"delivered": "Доставена",
"new": "в процес",
"pending": "Ново",
"processing": "в процес",
"paid": "Платено",
"cancelled": "Отменено",
"shipped": "Изпратено",
"delivered": "Доставено",
"return": "Връщане",
"partialReturn": "Частично връщане",
"partialDelivered": "Частично доставена"
"partialDelivered": "Частично доставено"
},
"table": {
"orderNumber": "Номер на поръчка",
@@ -24,6 +25,7 @@ export default {
"cancelOrder": "Отмени поръчката"
},
"noOrders": "Все още не сте направили поръчки.",
"trackShipment": "Проследи пратката",
"details": {
"title": "Подробности за поръчка: {{orderId}}",
"deliveryAddress": "Адрес за доставка",
@@ -36,15 +38,14 @@ export default {
"item": "Артикул",
"quantity": "Количество",
"price": "Цена",
"vat": "ДДС",
"total": "Общо",
"cancelOrder": "Отмени поръчката"
},
"cancelConfirm": {
"title": "Отмяна на поръчка",
"message": "Сигурни ли сте, че искате да отмените тази поръчка?",
"confirm": "Отмени поръчката",
"cancelling": "Отмяна..."
"title": "Отмени поръчката",
"message": "Сигурни ли сте, че искате да отмените тази поръчка?",
"confirm": "Отмени",
"cancelling": "Отмяна..."
},
"processing": "Поръчката се обработва...",
"processing": "Поръчката се обработва..."
};

View File

@@ -8,24 +8,25 @@ export default {
"articleNumber": "Номер на артикул",
"manufacturer": "Производител",
"inclVat": "вкл. {{vat}}% ДДС",
"inclVatSimple": "вкл. ДДС",
"priceUnit": "{{price}}/{{unit}}",
"new": "Нов",
"weeks": "седмици",
"weeks": "Седмици",
"arriving": "Пристигане:",
"inclVatFooter": "вкл. {{vat}}% ДДС,*",
"availability": "Наличност",
"inStock": "налично",
"inStock": "налично на склад",
"comingSoon": "Очаква се скоро",
"deliveryTime": "Срок на доставка",
"inclShort": "вкл.",
"vatShort": "ДДС",
"weight": "Тегло: {{weight}} кг",
"youSave": "Спестявате: {{amount}}",
"cheaperThanIndividual": "По-евтино от индивидуална покупка",
"cheaperThanIndividual": "По-евтино от закупуване поотделно",
"pickupPrice": "Цена за вземане: 19,90 € на резник.",
"consistsOf": "Състои се от:",
"loadingComponentDetails": "{{index}}. Зареждане на детайли за компонента...",
"loadingProduct": "Продуктът се зарежда...",
"loadingProduct": "Зареждане на продукта...",
"individualPriceTotal": "Обща индивидуална цена:",
"setPrice": "Цена на комплекта:",
"yourSavings": "Вашите спестявания:",

View File

@@ -0,0 +1,61 @@
export default {
"questionTitle": "Въпрос за продукта",
"questionSubtitle": "Имате ли въпрос за този продукт? Ще се радваме да ви помогнем.",
"questionSuccess": "Благодарим ви за въпроса! Ще се свържем с вас възможно най-скоро.",
"nameLabel": "Име",
"namePlaceholder": "Вашето име",
"emailLabel": "Имейл",
"emailPlaceholder": "your.email@example.com",
"questionLabel": "Вашият въпрос",
"questionPlaceholder": "Опишете въпроса си за този продукт...",
"photosLabelQuestion": "Прикачете снимки към въпроса си (по избор)",
"submitQuestion": "Изпрати въпроса",
"sending": "Изпращане...",
"ratingTitle": "Оценете продукта",
"ratingSubtitle": "Споделете опита си с този продукт и помогнете на други клиенти да вземат решение.",
"ratingSuccess": "Благодарим ви за вашия отзив! Той ще бъде публикуван след проверка.",
"emailHelper": "Вашият имейл няма да бъде публикуван",
"ratingLabel": "Оценка *",
"pleaseRate": "Моля, оценете",
"ratingStars": "{{rating}} от 5 звезди",
"reviewLabel": "Вашият отзив (по избор)",
"reviewPlaceholder": "Опишете опита си с този продукт...",
"photosLabelRating": "Прикачете снимки към отзива си (по избор)",
"submitRating": "Изпрати отзива",
"errorGeneric": "Възникна грешка",
"errorPhotos": "Грешка при обработка на снимките",
"availabilityTitle": "Запитване за наличност",
"availabilitySubtitle": "Този продукт в момента не е наличен. Ще се радваме да ви уведомим веднага щом бъде наличен отново.",
"availabilitySuccessEmail": "Благодарим ви за запитването! Ще ви уведомим по имейл веднага щом продуктът отново е наличен.",
"availabilitySuccessTelegram": "Благодарим ви за запитването! Ще ви уведомим чрез Telegram веднага щом продуктът отново е наличен.",
"notificationMethodLabel": "Как бихте искали да бъдете уведомени?",
"telegramBotLabel": "Telegram Bot",
"telegramIdLabel": "Telegram ID",
"telegramPlaceholder": "@yourTelegramName or Telegram ID",
"telegramHelper": "Въведете вашето потребителско име в Telegram (с @) или Telegram ID",
"messageLabel": "Съобщение (по избор)",
"messagePlaceholder": "Допълнителна информация или въпроси...",
"submitAvailability": "Запитване за наличност",
"photoUploadSelect": "Изберете снимки",
"photoUploadErrorMaxFiles": "Максимум {{max}} файла са разрешени",
"photoUploadErrorFileType": "Разрешени са само файлове с изображения (JPEG, PNG, GIF, WebP)",
"photoUploadErrorFileSize": "Файлът е твърде голям. Максимум: {{maxSize}}MB",
"photoUploadSelectedFiles": "{{count}} файл(ове) избрани",
"photoUploadCompressed": "(компресиран за качване)",
"photoUploadRemove": "Премахни изображението",
"photoUploadLabelDefault": "Прикачи снимки (по избор)",
"shareTitle": "Сподели",
"shareEmbed": "Вграждане",
"shareCopyLink": "Копирай линка",
"shareSuccessEmbed": "Кодът за вграждане е копиран в клипборда!",
"shareErrorEmbed": "Грешка при копиране на кода за вграждане",
"shareSuccessLink": "Линкът е копиран в клипборда!",
"shareWhatsAppText": "Виж този продукт: {{name}}",
"shareTelegramText": "Виж този продукт: {{name}}",
"shareEmailSubject": "Препоръка за продукт",
"shareEmailBody": "Здравейте,\n\nИскам да ви препоръчам този продукт:\n\n{{name}}\n{{url}}\n\nПоздрави"
};

View File

@@ -5,6 +5,7 @@ export default {
"profile": "Profil",
"email": "Email",
"password": "Heslo",
"newPassword": "Nové heslo",
"confirmPassword": "Potvrdit heslo",
"forgotPassword": "Zapomněli jste heslo?",
"loginWithGoogle": "Přihlásit se přes Google",
@@ -13,6 +14,7 @@ export default {
"privacyPolicy": "Zásadami ochrany osobních údajů",
"passwordMinLength": "Heslo musí mít alespoň 8 znaků",
"newPasswordMinLength": "Nové heslo musí mít alespoň 8 znaků",
"backToHome": "Zpět na domovskou stránku",
"menu": {
"profile": "Profil",
"myProfile": "Můj profil",
@@ -21,5 +23,28 @@ export default {
"settings": "Nastavení",
"adminDashboard": "Admin Dashboard",
"adminUsers": "Admin Users"
},
"resetPassword": {
"title": "Obnovení hesla",
"button": "Obnovit heslo",
"success": "Vaše heslo bylo úspěšně obnoveno! Brzy budete přesměrováni na přihlášení...",
"invalidToken": "Nebyl nalezen platný token. Použijte prosím odkaz z vašeho e-mailu.",
"error": "Chyba při obnově hesla",
"emailSent": "Odkaz pro obnovení hesla byl odeslán na vaši e-mailovou adresu.",
"emailError": "Chyba při odesílání e-mailu"
},
"errors": {
"fillAllFields": "Vyplňte prosím všechna pole",
"invalidEmail": "Zadejte platnou e-mailovou adresu",
"passwordsNotMatch": "Hesla se neshodují",
"passwordsNotMatchShort": "Hesla se neshodují",
"enterEmail": "Zadejte prosím svou e-mailovou adresu",
"loginFailed": "Přihlášení selhalo",
"registerFailed": "Registrace selhala",
"googleLoginFailed": "Přihlášení přes Google selhalo",
"emailExists": "Uživatel s touto e-mailovou adresou již existuje. Použijte prosím jinou e-mailovou adresu nebo se přihlaste."
},
"success": {
"registerComplete": "Registrace byla úspěšná. Nyní se můžete přihlásit."
}
};

View File

@@ -3,6 +3,7 @@ import navigation from './navigation.js';
import auth from './auth.js';
import cart from './cart.js';
import product from './product.js';
import productDialogs from './productDialogs.js';
import search from './search.js';
import sorting from './sorting.js';
import chat from './chat.js';
@@ -18,6 +19,7 @@ import pages from './pages.js';
import orders from './orders.js';
import settings from './settings.js';
import common from './common.js';
import kitConfig from './kitConfig.js';
import legalDatenschutzBasic from './legal-datenschutz-basic.js';
import legalDatenschutzCustomer from './legal-datenschutz-customer.js';
import legalDatenschutzGoogleOrders from './legal-datenschutz-google-orders.js';
@@ -35,6 +37,7 @@ export default {
"auth": auth,
"cart": cart,
"product": product,
"productDialogs": productDialogs,
"search": search,
"sorting": sorting,
"chat": chat,
@@ -50,6 +53,7 @@ export default {
"orders": orders,
"settings": settings,
"common": common,
"kitConfig": kitConfig,
"legalDatenschutzBasic": legalDatenschutzBasic,
"legalDatenschutzCustomer": legalDatenschutzCustomer,
"legalDatenschutzGoogleOrders": legalDatenschutzGoogleOrders,

View File

@@ -0,0 +1,43 @@
export default {
"pageTitle": "🌱 Konfigurátor Growboxu",
"pageSubtitle": "Sestavte si svůj dokonalý indoor grow setup",
"bundleDiscountTitle": "🎯 Zajistěte si slevu na balíček!",
"loadingProducts": "Načítání produktů growboxu...",
"loadingLighting": "Načítání osvětlení...",
"loadingVentilation": "Načítání ventilace...",
"loadingExtras": "Načítání doplňků...",
"noProductsAvailable": "Pro tuto velikost nejsou k dispozici žádné produkty",
"noLightingAvailable": "Pro velikost stanu {{shape}} nejsou k dispozici žádná vhodná světla.",
"noVentilationAvailable": "Pro velikost stanu {{shape}} není k dispozici vhodná ventilace.",
"noExtrasAvailable": "Žádné doplňky nejsou k dispozici",
"selectShapeTitle": "1. Vyberte tvar growboxu",
"selectShapeSubtitle": "Nejprve vyberte základní plochu vašeho growboxu",
"selectProductTitle": "2. Vyberte produkt growboxu",
"selectProductSubtitle": "Vyberte správný produkt pro váš growbox {{shape}}",
"selectLightingTitle": "3. Vyberte osvětlení",
"selectLightingTitleShape": "3. Vyberte osvětlení - {{shape}}",
"selectLightingSubtitle": "Nejprve prosím vyberte velikost stanu.",
"selectVentilationTitle": "4. Vyberte ventilaci",
"selectVentilationTitleShape": "4. Vyberte ventilaci - {{shape}}",
"selectVentilationSubtitle": "Nejprve prosím vyberte velikost stanu.",
"selectExtrasTitle": "5. Přidejte doplňky (volitelné)",
"yourConfiguration": "🎯 Vaše konfigurace",
"growboxLabel": "Growbox: {{name}}",
"lightingLabel": "Osvětlení: {{name}}",
"ventilationLabel": "Ventilace: {{name}}",
"extraLabel": "Doplněk: {{name}}",
"totalPrice": "Celková cena:",
"addToCart": "Přidat do košíku",
"selected": "✓ Vybráno",
"notDeliverable": "Nedodává se",
"noPrice": "Bez ceny",
"setName": "Sada growboxu - {{shape}}",
"description60x60": "Kompaktní - ideální pro malé prostory",
"description80x80": "Střední - perfektní rovnováha",
"description100x100": "Velký - pro zkušené pěstitele",
"description120x60": "Obdélníkový - maximální využití prostoru",
"plants1to2": "1-2 rostliny",
"plants2to4": "2-4 rostliny",
"plants4to6": "4-6 rostlin",
"plants3to6": "3-6 rostlin"
};

View File

@@ -1,50 +1,51 @@
export default {
"status": {
"new": "Probíhá",
"pending": "Nová",
"processing": "Probíhá",
"cancelled": "Zrušeno",
"shipped": "Odesláno",
"delivered": "Doručeno",
"return": "Vrácení",
"partialReturn": "Částečné vrácení",
"partialDelivered": "Částečně doručeno"
"new": "probíhá",
"pending": "Nové",
"processing": "probíhá",
"paid": "Zaplaceno",
"cancelled": "Zrušeno",
"shipped": "Odesláno",
"delivered": "Doručeno",
"return": "Vrácení",
"partialReturn": "Částečné vrácení",
"partialDelivered": "Částečně doručeno"
},
"table": {
"orderNumber": "Číslo objednávky",
"date": "Datum",
"status": "Stav",
"items": "Položky",
"total": "Celkem",
"actions": "Akce",
"viewDetails": "Zobrazit detaily"
"orderNumber": "Číslo objednávky",
"date": "Datum",
"status": "Stav",
"items": "Položky",
"total": "Celkem",
"actions": "Akce",
"viewDetails": "Zobrazit detaily"
},
"tooltips": {
"viewDetails": "Zobrazit detaily",
"cancelOrder": "Zrušit objednávku"
"viewDetails": "Zobrazit detaily",
"cancelOrder": "Zrušit objednávku"
},
"noOrders": "Ještě jste neprovedli žádné objednávky.",
"noOrders": "Ještě jste neprovedli žádné objednávky.",
"trackShipment": "Sledovat zásilku",
"details": {
"title": "Detaily objednávky: {{orderId}}",
"deliveryAddress": "Dodací adresa",
"invoiceAddress": "Fakturační adresa",
"orderDetails": "Detaily objednávky",
"deliveryMethod": "Způsob doručení:",
"paymentMethod": "Způsob platby:",
"notSpecified": "Nespecifikováno",
"orderedItems": "Objednané položky",
"item": "Položka",
"quantity": "Množství",
"price": "Cena",
"vat": "DPH",
"total": "Celkem",
"cancelOrder": "Zrušit objednávku"
"title": "Detaily objednávky: {{orderId}}",
"deliveryAddress": "Dodací adresa",
"invoiceAddress": "Fakturační adresa",
"orderDetails": "Detaily objednávky",
"deliveryMethod": "Způsob doručení:",
"paymentMethod": "Způsob platby:",
"notSpecified": "Nespecifikováno",
"orderedItems": "Objednané položky",
"item": "Položka",
"quantity": "Množství",
"price": "Cena",
"total": "Celkem",
"cancelOrder": "Zrušit objednávku"
},
"cancelConfirm": {
"title": "Zrušit objednávku",
"message": "Opravdu chcete tuto objednávku zrušit?",
"confirm": "Zrušit objednávku",
"confirm": "Zrušit",
"cancelling": "Rušení..."
},
"processing": "Objednávka se dokončuje...",
"processing": "Objednávka se dokončuje..."
};

View File

@@ -8,10 +8,11 @@ export default {
"articleNumber": "Číslo artiklu",
"manufacturer": "Výrobce",
"inclVat": "včetně {{vat}}% DPH",
"inclVatSimple": "včetně DPH",
"priceUnit": "{{price}}/{{unit}}",
"new": "Nové",
"weeks": "týdny",
"arriving": "Příchod:",
"weeks": "Týdny",
"arriving": "Příjezd:",
"inclVatFooter": "včetně {{vat}}% DPH,*",
"availability": "Dostupnost",
"inStock": "skladem",
@@ -22,10 +23,10 @@ export default {
"weight": "Hmotnost: {{weight}} kg",
"youSave": "Ušetříte: {{amount}}",
"cheaperThanIndividual": "Levnější než nákup jednotlivě",
"pickupPrice": "Cena za vyzvednutí: 19,90 € za řízek.",
"pickupPrice": "Cena za odběr: 19,90 € za řízek.",
"consistsOf": "Skládá se z:",
"loadingComponentDetails": "{{index}}. Načítání detailů komponenty...",
"loadingProduct": "Produkt se načítá...",
"loadingProduct": "Načítání produktu...",
"individualPriceTotal": "Celková cena jednotlivě:",
"setPrice": "Cena sady:",
"yourSavings": "Vaše úspory:",

View File

@@ -0,0 +1,61 @@
export default {
"questionTitle": "Otázka ohledně produktu",
"questionSubtitle": "Máte otázku ohledně tohoto produktu? Rádi vám pomůžeme.",
"questionSuccess": "Děkujeme za vaši otázku! Ozveme se vám co nejdříve.",
"nameLabel": "Jméno",
"namePlaceholder": "Vaše jméno",
"emailLabel": "Email",
"emailPlaceholder": "vas.email@priklad.cz",
"questionLabel": "Vaše otázka",
"questionPlaceholder": "Popište svou otázku ohledně tohoto produktu...",
"photosLabelQuestion": "Přiložte fotografie k vaší otázce (volitelné)",
"submitQuestion": "Odeslat otázku",
"sending": "Odesílání...",
"ratingTitle": "Ohodnoťte produkt",
"ratingSubtitle": "Podělte se o své zkušenosti s tímto produktem a pomozte ostatním zákazníkům s rozhodnutím.",
"ratingSuccess": "Děkujeme za vaši recenzi! Bude zveřejněna po ověření.",
"emailHelper": "Váš email nebude zveřejněn",
"ratingLabel": "Hodnocení *",
"pleaseRate": "Prosím ohodnoťte",
"ratingStars": "{{rating}} z 5 hvězdiček",
"reviewLabel": "Vaše recenze (volitelné)",
"reviewPlaceholder": "Popište své zkušenosti s tímto produktem...",
"photosLabelRating": "Přiložte fotografie k vaší recenzi (volitelné)",
"submitRating": "Odeslat recenzi",
"errorGeneric": "Došlo k chybě",
"errorPhotos": "Chyba při zpracování fotografií",
"availabilityTitle": "Požádejte o dostupnost",
"availabilitySubtitle": "Tento produkt momentálně není dostupný. Rádi vás informujeme, jakmile bude opět skladem.",
"availabilitySuccessEmail": "Děkujeme za váš požadavek! Jakmile bude produkt opět dostupný, budeme vás informovat e-mailem.",
"availabilitySuccessTelegram": "Děkujeme za váš požadavek! Jakmile bude produkt opět dostupný, budeme vás informovat přes Telegram.",
"notificationMethodLabel": "Jak chcete být informováni?",
"telegramBotLabel": "Telegram Bot",
"telegramIdLabel": "Telegram ID",
"telegramPlaceholder": "@vaseTelegramJmeno nebo Telegram ID",
"telegramHelper": "Zadejte své uživatelské jméno na Telegramu (s @) nebo Telegram ID",
"messageLabel": "Zpráva (volitelné)",
"messagePlaceholder": "Další informace nebo otázky...",
"submitAvailability": "Požádat o dostupnost",
"photoUploadSelect": "Vybrat fotografie",
"photoUploadErrorMaxFiles": "Maximálně {{max}} souborů povoleno",
"photoUploadErrorFileType": "Jsou povoleny pouze obrazové soubory (JPEG, PNG, GIF, WebP)",
"photoUploadErrorFileSize": "Soubor je příliš velký. Maximum: {{maxSize}}MB",
"photoUploadSelectedFiles": "Vybráno {{count}} souborů",
"photoUploadCompressed": "(komprimováno pro nahrání)",
"photoUploadRemove": "Odstranit obrázek",
"photoUploadLabelDefault": "Přiložit fotografie (volitelné)",
"shareTitle": "Sdílet",
"shareEmbed": "Vložit",
"shareCopyLink": "Kopírovat odkaz",
"shareSuccessEmbed": "Kód pro vložení zkopírován do schránky!",
"shareErrorEmbed": "Chyba při kopírování kódu pro vložení",
"shareSuccessLink": "Odkaz zkopírován do schránky!",
"shareWhatsAppText": "Podívejte se na tento produkt: {{name}}",
"shareTelegramText": "Podívejte se na tento produkt: {{name}}",
"shareEmailSubject": "Doporučení produktu",
"shareEmailBody": "Dobrý den,\n\nrád bych vám doporučil tento produkt:\n\n{{name}}\n{{url}}\n\nS pozdravem"
};

View File

@@ -5,6 +5,7 @@ export default {
"profile": "Profil",
"email": "E-Mail",
"password": "Passwort",
"newPassword": "Neues Passwort",
"confirmPassword": "Passwort bestätigen",
"forgotPassword": "Passwort vergessen?",
"loginWithGoogle": "Mit Google anmelden",
@@ -13,6 +14,7 @@ export default {
"privacyPolicy": "Datenschutzbestimmungen",
"passwordMinLength": "Das Passwort muss mindestens 8 Zeichen lang sein",
"newPasswordMinLength": "Das neue Passwort muss mindestens 8 Zeichen lang sein",
"backToHome": "Zurück zur Startseite",
"menu": {
"profile": "Profil",
"myProfile": "Mein Profil",
@@ -21,5 +23,28 @@ export default {
"settings": "Einstellungen",
"adminDashboard": "Admin Dashboard",
"adminUsers": "Admin Users"
},
"resetPassword": {
"title": "Passwort zurücksetzen",
"button": "Passwort zurücksetzen",
"success": "Ihr Passwort wurde erfolgreich zurückgesetzt! Sie werden in Kürze zur Anmeldung weitergeleitet...",
"invalidToken": "Kein gültiger Token gefunden. Bitte verwenden Sie den Link aus Ihrer E-Mail.",
"error": "Fehler beim Zurücksetzen des Passworts",
"emailSent": "Ein Link zum Zurücksetzen des Passworts wurde an Ihre E-Mail-Adresse gesendet.",
"emailError": "Fehler beim Senden der E-Mail"
},
"errors": {
"fillAllFields": "Bitte füllen Sie alle Felder aus",
"invalidEmail": "Bitte geben Sie eine gültige E-Mail-Adresse ein",
"passwordsNotMatch": "Die Passwörter stimmen nicht überein",
"passwordsNotMatchShort": "Passwörter stimmen nicht überein",
"enterEmail": "Bitte geben Sie Ihre E-Mail-Adresse ein",
"loginFailed": "Anmeldung fehlgeschlagen",
"registerFailed": "Registrierung fehlgeschlagen",
"googleLoginFailed": "Google-Anmeldung fehlgeschlagen",
"emailExists": "Ein Benutzer mit dieser E-Mail-Adresse existiert bereits. Bitte verwenden Sie eine andere E-Mail-Adresse oder melden Sie sich an."
},
"success": {
"registerComplete": "Registrierung erfolgreich. Sie können sich jetzt anmelden."
}
};

View File

@@ -3,6 +3,7 @@ import navigation from './navigation.js';
import auth from './auth.js';
import cart from './cart.js';
import product from './product.js';
import productDialogs from './productDialogs.js';
import search from './search.js';
import sorting from './sorting.js';
import chat from './chat.js';
@@ -18,6 +19,7 @@ import pages from './pages.js';
import orders from './orders.js';
import settings from './settings.js';
import common from './common.js';
import kitConfig from './kitConfig.js';
import legalDatenschutzBasic from './legal-datenschutz-basic.js';
import legalDatenschutzCustomer from './legal-datenschutz-customer.js';
import legalDatenschutzGoogleOrders from './legal-datenschutz-google-orders.js';
@@ -35,6 +37,7 @@ export default {
"auth": auth,
"cart": cart,
"product": product,
"productDialogs": productDialogs,
"search": search,
"sorting": sorting,
"chat": chat,
@@ -50,6 +53,7 @@ export default {
"orders": orders,
"settings": settings,
"common": common,
"kitConfig": kitConfig,
"legalDatenschutzBasic": legalDatenschutzBasic,
"legalDatenschutzCustomer": legalDatenschutzCustomer,
"legalDatenschutzGoogleOrders": legalDatenschutzGoogleOrders,

View File

@@ -0,0 +1,44 @@
export default {
"pageTitle": "🌱 Growbox Konfigurator",
"pageSubtitle": "Stelle dein perfektes Indoor Grow Setup zusammen",
"bundleDiscountTitle": "🎯 Bundle-Rabatt sichern!",
"loadingProducts": "Lade Growbox-Produkte...",
"loadingLighting": "Lade Beleuchtungs-Produkte...",
"loadingVentilation": "Lade Belüftungs-Produkte...",
"loadingExtras": "Lade Extras...",
"noProductsAvailable": "Keine Produkte für diese Größe verfügbar",
"noLightingAvailable": "Keine passenden Lampen für Zeltgröße {{shape}} verfügbar.",
"noVentilationAvailable": "Keine passenden Belüftung für Zeltgröße {{shape}} verfügbar.",
"noExtrasAvailable": "Keine Extras verfügbar",
"selectShapeTitle": "1. Growbox-Form auswählen",
"selectShapeSubtitle": "Wähle zuerst die Grundfläche deiner Growbox aus",
"selectProductTitle": "2. Growbox Produkt auswählen",
"selectProductSubtitle": "Wähle das passende Produkt für deine {{shape}} Growbox",
"selectLightingTitle": "3. Beleuchtung wählen",
"selectLightingTitleShape": "3. Beleuchtung wählen - {{shape}}",
"selectLightingSubtitle": "Bitte wählen Sie zuerst eine Zeltgröße aus.",
"selectVentilationTitle": "4. Belüftung auswählen",
"selectVentilationTitleShape": "4. Belüftung auswählen - {{shape}}",
"selectVentilationSubtitle": "Bitte wählen Sie zuerst eine Zeltgröße aus.",
"selectExtrasTitle": "5. Extras hinzufügen (optional)",
"yourConfiguration": "🎯 Ihre Konfiguration",
"growboxLabel": "Growbox: {{name}}",
"lightingLabel": "Beleuchtung: {{name}}",
"ventilationLabel": "Belüftung: {{name}}",
"extraLabel": "Extra: {{name}}",
"totalPrice": "Gesamtpreis:",
"addToCart": "In den Warenkorb",
"selected": "✓ Ausgewählt",
"notDeliverable": "Nicht lieferbar",
"noPrice": "Kein Preis",
"setName": "Growbox Set - {{shape}}",
"description60x60": "Kompakt - ideal für kleine Räume",
"description80x80": "Mittel - perfekte Balance",
"description100x100": "Groß - für erfahrene Grower",
"description120x60": "Rechteckig - maximale Raumnutzung",
"plants1to2": "1-2 Pflanzen",
"plants2to4": "2-4 Pflanzen",
"plants4to6": "4-6 Pflanzen",
"plants3to6": "3-6 Pflanzen"
};

View File

@@ -3,6 +3,7 @@ export default {
"new": "in Bearbeitung",
"pending": "Neu",
"processing": "in Bearbeitung",
"paid": "Bezahlt",
"cancelled": "Storniert",
"shipped": "Verschickt",
"delivered": "Geliefert",
@@ -24,6 +25,7 @@ export default {
"cancelOrder": "Bestellung stornieren"
},
"noOrders": "Sie haben noch keine Bestellungen aufgegeben.",
"trackShipment": "Sendung verfolgen",
"details": {
"title": "Bestelldetails: {{orderId}}",
"deliveryAddress": "Lieferadresse",

View File

@@ -8,6 +8,7 @@ export default {
"articleNumber": "Artikelnummer",
"manufacturer": "Hersteller",
"inclVat": "inkl. {{vat}}% MwSt.",
"inclVatSimple": "inkl. MwSt.",
"priceUnit": "{{price}}/{{unit}}",
"new": "Neu",
"weeks": "Wochen",

View File

@@ -0,0 +1,62 @@
export default {
"questionTitle": "Frage zum Artikel",
"questionSubtitle": "Haben Sie eine Frage zu diesem Artikel? Wir helfen Ihnen gerne weiter.",
"questionSuccess": "Vielen Dank für Ihre Frage! Wir werden uns schnellstmöglich bei Ihnen melden.",
"nameLabel": "Name",
"namePlaceholder": "Ihr Name",
"emailLabel": "E-Mail",
"emailPlaceholder": "ihre.email@example.com",
"questionLabel": "Ihre Frage",
"questionPlaceholder": "Beschreiben Sie Ihre Frage zu diesem Artikel...",
"photosLabelQuestion": "Fotos zur Frage anhängen (optional)",
"submitQuestion": "Frage senden",
"sending": "Wird gesendet...",
"ratingTitle": "Artikel Bewerten",
"ratingSubtitle": "Teilen Sie Ihre Erfahrungen mit diesem Artikel und helfen Sie anderen Kunden bei der Entscheidung.",
"ratingSuccess": "Vielen Dank für Ihre Bewertung! Sie wird nach Prüfung veröffentlicht.",
"emailHelper": "Ihre E-Mail wird nicht veröffentlicht",
"ratingLabel": "Bewertung *",
"pleaseRate": "Bitte bewerten",
"ratingStars": "{{rating}} von 5 Sternen",
"reviewLabel": "Ihre Bewertung (optional)",
"reviewPlaceholder": "Beschreiben Sie Ihre Erfahrungen mit diesem Artikel...",
"photosLabelRating": "Fotos zur Bewertung anhängen (optional)",
"submitRating": "Bewertung abgeben",
"errorGeneric": "Ein Fehler ist aufgetreten",
"errorPhotos": "Fehler beim Verarbeiten der Fotos",
"availabilityTitle": "Verfügbarkeit anfragen",
"availabilitySubtitle": "Dieser Artikel ist derzeit nicht verfügbar. Gerne informieren wir Sie, sobald er wieder lieferbar ist.",
"availabilitySuccessEmail": "Vielen Dank für Ihre Anfrage! Wir werden Sie per E-Mail informieren, sobald der Artikel wieder verfügbar ist.",
"availabilitySuccessTelegram": "Vielen Dank für Ihre Anfrage! Wir werden Sie über Telegram informieren, sobald der Artikel wieder verfügbar ist.",
"notificationMethodLabel": "Wie möchten Sie benachrichtigt werden?",
"telegramBotLabel": "Telegram Bot",
"telegramIdLabel": "Telegram ID",
"telegramPlaceholder": "@IhrTelegramName oder Telegram ID",
"telegramHelper": "Geben Sie Ihren Telegram-Benutzernamen (mit @) oder Ihre Telegram-ID ein",
"messageLabel": "Nachricht (optional)",
"messagePlaceholder": "Zusätzliche Informationen oder Fragen...",
"submitAvailability": "Verfügbarkeit anfragen",
"photoUploadSelect": "Fotos auswählen",
"photoUploadErrorMaxFiles": "Maximal {{max}} Dateien erlaubt",
"photoUploadErrorFileType": "Nur Bilddateien (JPEG, PNG, GIF, WebP) sind erlaubt",
"photoUploadErrorFileSize": "Datei zu groß. Maximum: {{maxSize}}MB",
"photoUploadSelectedFiles": "{{count}} Datei(en) ausgewählt",
"photoUploadCompressed": "(komprimiert für Upload)",
"photoUploadRemove": "Bild entfernen",
"photoUploadLabelDefault": "Fotos anhängen (optional)",
"shareTitle": "Teilen",
"shareEmbed": "Einbetten",
"shareCopyLink": "Link kopieren",
"shareSuccessEmbed": "Einbettungscode wurde in die Zwischenablage kopiert!",
"shareErrorEmbed": "Fehler beim Kopieren des Einbettungscodes",
"shareSuccessLink": "Link wurde in die Zwischenablage kopiert!",
"shareWhatsAppText": "Schau dir dieses Produkt an: {{name}}",
"shareTelegramText": "Schau dir dieses Produkt an: {{name}}",
"shareEmailSubject": "Produktempfehlung",
"shareEmailBody": "Hallo,\n\nich möchte dir dieses Produkt empfehlen:\n\n{{name}}\n{{url}}\n\nViele Grüße"
};

View File

@@ -5,14 +5,16 @@ export default {
"profile": "Προφίλ",
"email": "Email",
"password": "Κωδικός",
"newPassword": "Νέος κωδικός",
"confirmPassword": "Επιβεβαίωση κωδικού",
"forgotPassword": "Ξεχάσατε τον κωδικό;",
"loginWithGoogle": "Σύνδεση με Google",
"or": "Ή",
"privacyAccept": "Κάνοντας κλικ στο \"Σύνδεση με Google\" αποδέχομαι την",
"privacyPolicy": "Πολιτική Απορρήτου",
"privacyPolicy": "Πολιτική απορρήτου",
"passwordMinLength": "Ο κωδικός πρέπει να έχει τουλάχιστον 8 χαρακτήρες",
"newPasswordMinLength": "Ο νέος κωδικός πρέπει να έχει τουλάχιστον 8 χαρακτήρες",
"backToHome": "Επιστροφή στην αρχική σελίδα",
"menu": {
"profile": "Προφίλ",
"myProfile": "Το προφίλ μου",
@@ -21,5 +23,28 @@ export default {
"settings": "Ρυθμίσεις",
"adminDashboard": "Πίνακας διαχείρισης",
"adminUsers": "Διαχειριστές"
},
"resetPassword": {
"title": "Επαναφορά κωδικού",
"button": "Επαναφορά κωδικού",
"success": "Ο κωδικός σας επαναφέρθηκε με επιτυχία! Θα ανακατευθυνθείτε στη σύνδεση σύντομα...",
"invalidToken": "Δεν βρέθηκε έγκυρο διακριτικό. Παρακαλώ χρησιμοποιήστε τον σύνδεσμο από το email σας.",
"error": "Σφάλμα κατά την επαναφορά του κωδικού",
"emailSent": "Ένας σύνδεσμος για επαναφορά του κωδικού σας έχει σταλεί στη διεύθυνση email σας.",
"emailError": "Σφάλμα κατά την αποστολή του email"
},
"errors": {
"fillAllFields": "Παρακαλώ συμπληρώστε όλα τα πεδία",
"invalidEmail": "Παρακαλώ εισάγετε μια έγκυρη διεύθυνση email",
"passwordsNotMatch": "Οι κωδικοί δεν ταιριάζουν",
"passwordsNotMatchShort": "Οι κωδικοί δεν ταιριάζουν",
"enterEmail": "Παρακαλώ εισάγετε τη διεύθυνση email σας",
"loginFailed": "Η σύνδεση απέτυχε",
"registerFailed": "Η εγγραφή απέτυχε",
"googleLoginFailed": "Η σύνδεση με Google απέτυχε",
"emailExists": "Υπάρχει ήδη χρήστης με αυτή τη διεύθυνση email. Παρακαλώ χρησιμοποιήστε άλλη διεύθυνση ή συνδεθείτε."
},
"success": {
"registerComplete": "Η εγγραφή ολοκληρώθηκε με επιτυχία. Μπορείτε τώρα να συνδεθείτε."
}
};

View File

@@ -3,6 +3,7 @@ import navigation from './navigation.js';
import auth from './auth.js';
import cart from './cart.js';
import product from './product.js';
import productDialogs from './productDialogs.js';
import search from './search.js';
import sorting from './sorting.js';
import chat from './chat.js';
@@ -18,6 +19,7 @@ import pages from './pages.js';
import orders from './orders.js';
import settings from './settings.js';
import common from './common.js';
import kitConfig from './kitConfig.js';
import legalDatenschutzBasic from './legal-datenschutz-basic.js';
import legalDatenschutzCustomer from './legal-datenschutz-customer.js';
import legalDatenschutzGoogleOrders from './legal-datenschutz-google-orders.js';
@@ -35,6 +37,7 @@ export default {
"auth": auth,
"cart": cart,
"product": product,
"productDialogs": productDialogs,
"search": search,
"sorting": sorting,
"chat": chat,
@@ -50,6 +53,7 @@ export default {
"orders": orders,
"settings": settings,
"common": common,
"kitConfig": kitConfig,
"legalDatenschutzBasic": legalDatenschutzBasic,
"legalDatenschutzCustomer": legalDatenschutzCustomer,
"legalDatenschutzGoogleOrders": legalDatenschutzGoogleOrders,

View File

@@ -0,0 +1,43 @@
export default {
"pageTitle": "🌱 Διαμορφωτής Growbox",
"pageSubtitle": "Συνθέστε το τέλειο εσωτερικό σύστημα καλλιέργειας",
"bundleDiscountTitle": "🎯 Εξασφαλίστε έκπτωση πακέτου!",
"loadingProducts": "Φόρτωση προϊόντων growbox...",
"loadingLighting": "Φόρτωση προϊόντων φωτισμού...",
"loadingVentilation": "Φόρτωση προϊόντων αερισμού...",
"loadingExtras": "Φόρτωση επιπλέον...",
"noProductsAvailable": "Δεν υπάρχουν διαθέσιμα προϊόντα για αυτό το μέγεθος",
"noLightingAvailable": "Δεν υπάρχουν κατάλληλα φώτα για το μέγεθος σκηνής {{shape}}.",
"noVentilationAvailable": "Δεν υπάρχει κατάλληλος αερισμός για το μέγεθος σκηνής {{shape}}.",
"noExtrasAvailable": "Δεν υπάρχουν επιπλέον διαθέσιμα",
"selectShapeTitle": "1. Επιλέξτε το σχήμα του growbox",
"selectShapeSubtitle": "Επιλέξτε πρώτα την επιφάνεια βάσης του growbox σας",
"selectProductTitle": "2. Επιλέξτε προϊόν growbox",
"selectProductSubtitle": "Επιλέξτε το κατάλληλο προϊόν για το growbox {{shape}} σας",
"selectLightingTitle": "3. Επιλέξτε φωτισμό",
"selectLightingTitleShape": "3. Επιλέξτε φωτισμό - {{shape}}",
"selectLightingSubtitle": "Παρακαλώ επιλέξτε πρώτα το μέγεθος της σκηνής.",
"selectVentilationTitle": "4. Επιλέξτε αερισμό",
"selectVentilationTitleShape": "4. Επιλέξτε αερισμό - {{shape}}",
"selectVentilationSubtitle": "Παρακαλώ επιλέξτε πρώτα το μέγεθος της σκηνής.",
"selectExtrasTitle": "5. Προσθέστε επιπλέον (προαιρετικά)",
"yourConfiguration": "🎯 Η διαμόρφωσή σας",
"growboxLabel": "Growbox: {{name}}",
"lightingLabel": "Φωτισμός: {{name}}",
"ventilationLabel": "Αερισμός: {{name}}",
"extraLabel": "Επιπλέον: {{name}}",
"totalPrice": "Συνολική τιμή:",
"addToCart": "Προσθήκη στο καλάθι",
"selected": "✓ Επιλεγμένο",
"notDeliverable": "Μη διαθέσιμο για παράδοση",
"noPrice": "Χωρίς τιμή",
"setName": "Σετ Growbox - {{shape}}",
"description60x60": "Συμπαγές - ιδανικό για μικρούς χώρους",
"description80x80": "Μεσαίο - τέλεια ισορροπία",
"description100x100": "Μεγάλο - για έμπειρους καλλιεργητές",
"description120x60": "Ορθογώνιο - μέγιστη χρήση χώρου",
"plants1to2": "1-2 φυτά",
"plants2to4": "2-4 φυτά",
"plants4to6": "4-6 φυτά",
"plants3to6": "3-6 φυτά"
};

View File

@@ -1,10 +1,11 @@
export default {
"status": {
"new": "Σε εξέλιξη",
"new": "σε εξέλιξη",
"pending": "Νέο",
"processing": "Σε εξέλιξη",
"processing": "σε εξέλιξη",
"paid": "Πληρωμένο",
"cancelled": "Ακυρώθηκε",
"shipped": "Απεστάλη",
"shipped": "Απεσταλμένο",
"delivered": "Παραδόθηκε",
"return": "Επιστροφή",
"partialReturn": "Μερική επιστροφή",
@@ -24,10 +25,11 @@ export default {
"cancelOrder": "Ακύρωση παραγγελίας"
},
"noOrders": "Δεν έχετε κάνει ακόμα καμία παραγγελία.",
"trackShipment": "Παρακολούθηση αποστολής",
"details": {
"title": "Λεπτομέρειες παραγγελίας: {{orderId}}",
"deliveryAddress": "Διεύθυνση παράδοσης",
"invoiceAddress": "Διεύθυνση τιμολόγησης",
"invoiceAddress": "Διεύθυνση τιμολογίου",
"orderDetails": "Λεπτομέρειες παραγγελίας",
"deliveryMethod": "Τρόπος παράδοσης:",
"paymentMethod": "Τρόπος πληρωμής:",
@@ -36,15 +38,14 @@ export default {
"item": "Είδος",
"quantity": "Ποσότητα",
"price": "Τιμή",
"vat": "ΦΠΑ",
"total": "Σύνολο",
"cancelOrder": "Ακύρωση παραγγελίας"
},
"cancelConfirm": {
"title": "Ακύρωση παραγγελίας",
"message": "Είστε σίγουροι ότι θέλετε να ακυρώσετε αυτήν την παραγγελία;",
"confirm": "Ακύρωση παραγγελίας",
"cancelling": "Ακύρωση..."
"message": "Είστε σίγουροι ότι θέλετε να ακυρώσετε αυτή την παραγγελία;",
"confirm": "Ακύρωση",
"cancelling": "Ακύρωση σε εξέλιξη..."
},
"processing": "Η παραγγελία ολοκληρώνεται..."
};

View File

@@ -8,9 +8,10 @@ export default {
"articleNumber": "Αριθμός άρθρου",
"manufacturer": "Κατασκευαστής",
"inclVat": "συμπ. {{vat}}% ΦΠΑ",
"inclVatSimple": "συμπ. ΦΠΑ",
"priceUnit": "{{price}}/{{unit}}",
"new": "Νέο",
"weeks": "εβδομάδες",
"weeks": "Εβδομάδες",
"arriving": "Άφιξη:",
"inclVatFooter": "συμπ. {{vat}}% ΦΠΑ,*",
"availability": "Διαθεσιμότητα",
@@ -28,7 +29,7 @@ export default {
"loadingProduct": "Φόρτωση προϊόντος...",
"individualPriceTotal": "Συνολική τιμή μεμονωμένων:",
"setPrice": "Τιμή σετ:",
"yourSavings": "Η εξοικονόμησή σας:",
"yourSavings": "Οι εξοικονομήσεις σας:",
"similarProducts": "Παρόμοια προϊόντα",
"countDisplay": {
"noProducts": "0 προϊόντα",

View File

@@ -0,0 +1,61 @@
export default {
"questionTitle": "Ερώτηση σχετικά με το προϊόν",
"questionSubtitle": "Έχετε κάποια ερώτηση για αυτό το προϊόν; Είμαστε εδώ για να σας βοηθήσουμε.",
"questionSuccess": "Ευχαριστούμε για την ερώτησή σας! Θα επικοινωνήσουμε μαζί σας το συντομότερο δυνατό.",
"nameLabel": "Όνομα",
"namePlaceholder": "Το όνομά σας",
"emailLabel": "Email",
"emailPlaceholder": "your.email@example.com",
"questionLabel": "Η ερώτησή σας",
"questionPlaceholder": "Περιγράψτε την ερώτησή σας σχετικά με αυτό το προϊόν...",
"photosLabelQuestion": "Επισυνάψτε φωτογραφίες στην ερώτησή σας (προαιρετικό)",
"submitQuestion": "Αποστολή ερώτησης",
"sending": "Αποστολή...",
"ratingTitle": "Αξιολογήστε το προϊόν",
"ratingSubtitle": "Μοιραστείτε την εμπειρία σας με αυτό το προϊόν και βοηθήστε άλλους πελάτες να πάρουν την απόφασή τους.",
"ratingSuccess": "Ευχαριστούμε για την αξιολόγησή σας! Θα δημοσιευτεί μετά από έλεγχο.",
"emailHelper": "Το email σας δεν θα δημοσιευτεί",
"ratingLabel": "Αξιολόγηση *",
"pleaseRate": "Παρακαλώ αξιολογήστε",
"ratingStars": "{{rating}} από 5 αστέρια",
"reviewLabel": "Η κριτική σας (προαιρετικό)",
"reviewPlaceholder": "Περιγράψτε τις εμπειρίες σας με αυτό το προϊόν...",
"photosLabelRating": "Επισυνάψτε φωτογραφίες στην κριτική σας (προαιρετικό)",
"submitRating": "Υποβολή κριτικής",
"errorGeneric": "Παρουσιάστηκε σφάλμα",
"errorPhotos": "Σφάλμα κατά την επεξεργασία των φωτογραφιών",
"availabilityTitle": "Ζητήστε διαθεσιμότητα",
"availabilitySubtitle": "Αυτό το προϊόν δεν είναι διαθέσιμο αυτή τη στιγμή. Θα χαρούμε να σας ενημερώσουμε μόλις είναι ξανά διαθέσιμο.",
"availabilitySuccessEmail": "Ευχαριστούμε για το αίτημά σας! Θα σας ενημερώσουμε μέσω email μόλις το προϊόν είναι ξανά διαθέσιμο.",
"availabilitySuccessTelegram": "Ευχαριστούμε για το αίτημά σας! Θα σας ενημερώσουμε μέσω Telegram μόλις το προϊόν είναι ξανά διαθέσιμο.",
"notificationMethodLabel": "Πώς θέλετε να ειδοποιηθείτε;",
"telegramBotLabel": "Telegram Bot",
"telegramIdLabel": "Telegram ID",
"telegramPlaceholder": "@yourTelegramName or Telegram ID",
"telegramHelper": "Εισάγετε το όνομα χρήστη Telegram (με @) ή το Telegram ID σας",
"messageLabel": "Μήνυμα (προαιρετικό)",
"messagePlaceholder": "Επιπλέον πληροφορίες ή ερωτήσεις...",
"submitAvailability": "Ζητήστε διαθεσιμότητα",
"photoUploadSelect": "Επιλέξτε φωτογραφίες",
"photoUploadErrorMaxFiles": "Επιτρέπονται έως {{max}} αρχεία",
"photoUploadErrorFileType": "Επιτρέπονται μόνο αρχεία εικόνας (JPEG, PNG, GIF, WebP)",
"photoUploadErrorFileSize": "Το αρχείο είναι πολύ μεγάλο. Μέγιστο: {{maxSize}}MB",
"photoUploadSelectedFiles": "Επιλέχθηκαν {{count}} αρχεία",
"photoUploadCompressed": "(συμπιεσμένο για αποστολή)",
"photoUploadRemove": "Αφαίρεση εικόνας",
"photoUploadLabelDefault": "Επισύναψη φωτογραφιών (προαιρετικό)",
"shareTitle": "Κοινοποίηση",
"shareEmbed": "Ενσωμάτωση",
"shareCopyLink": "Αντιγραφή συνδέσμου",
"shareSuccessEmbed": "Ο κώδικας ενσωμάτωσης αντιγράφηκε στο πρόχειρο!",
"shareErrorEmbed": "Σφάλμα κατά την αντιγραφή του κώδικα ενσωμάτωσης",
"shareSuccessLink": "Ο σύνδεσμος αντιγράφηκε στο πρόχειρο!",
"shareWhatsAppText": "Δείτε αυτό το προϊόν: {{name}}",
"shareTelegramText": "Δείτε αυτό το προϊόν: {{name}}",
"shareEmailSubject": "Σύσταση προϊόντος",
"shareEmailBody": "Γεια σας,\n\nΘα ήθελα να σας προτείνω αυτό το προϊόν:\n\n{{name}}\n{{url}}\n\nΜε εκτίμηση"
};

View File

@@ -5,14 +5,16 @@ export default {
"profile": "Profile", // Profil
"email": "Email", // E-Mail
"password": "Password", // Passwort
"newPassword": "New password", // Neues Passwort
"confirmPassword": "Confirm password", // Passwort bestätigen
"forgotPassword": "Forgot password?", // Passwort vergessen?
"loginWithGoogle": "Sign in with Google", // Mit Google anmelden
"or": "OR", // ODER
"privacyAccept": "By clicking \"Sign in with Google\" I accept the", // Mit dem Click auf "Mit Google anmelden" akzeptiere ich die
"privacyPolicy": "Privacy Policy", // Datenschutzbestimmungen
"privacyAccept": "By clicking on \"Sign in with Google\" I accept the", // Mit dem Click auf "Mit Google anmelden" akzeptiere ich die
"privacyPolicy": "Privacy policy", // Datenschutzbestimmungen
"passwordMinLength": "The password must be at least 8 characters long", // Das Passwort muss mindestens 8 Zeichen lang sein
"newPasswordMinLength": "The new password must be at least 8 characters long", // Das neue Passwort muss mindestens 8 Zeichen lang sein
"backToHome": "Back to homepage", // Zurück zur Startseite
"menu": {
"profile": "Profile", // Profil
"myProfile": "My profile", // Mein Profil
@@ -21,5 +23,28 @@ export default {
"settings": "Settings", // Einstellungen
"adminDashboard": "Admin Dashboard", // Admin Dashboard
"adminUsers": "Admin Users" // Admin Users
},
"resetPassword": {
"title": "Reset password", // Passwort zurücksetzen
"button": "Reset password", // Passwort zurücksetzen
"success": "Your password has been reset successfully! You will be redirected to login shortly...", // Ihr Passwort wurde erfolgreich zurückgesetzt! Sie werden in Kürze zur Anmeldung weitergeleitet...
"invalidToken": "No valid token found. Please use the link from your email.", // Kein gültiger Token gefunden. Bitte verwenden Sie den Link aus Ihrer E-Mail.
"error": "Error resetting password", // Fehler beim Zurücksetzen des Passworts
"emailSent": "A link to reset your password has been sent to your email address.", // Ein Link zum Zurücksetzen des Passworts wurde an Ihre E-Mail-Adresse gesendet.
"emailError": "Error sending email" // Fehler beim Senden der E-Mail
},
"errors": {
"fillAllFields": "Please fill in all fields", // Bitte füllen Sie alle Felder aus
"invalidEmail": "Please enter a valid email address", // Bitte geben Sie eine gültige E-Mail-Adresse ein
"passwordsNotMatch": "The passwords do not match", // Die Passwörter stimmen nicht überein
"passwordsNotMatchShort": "Passwords do not match", // Passwörter stimmen nicht überein
"enterEmail": "Please enter your email address", // Bitte geben Sie Ihre E-Mail-Adresse ein
"loginFailed": "Login failed", // Anmeldung fehlgeschlagen
"registerFailed": "Registration failed", // Registrierung fehlgeschlagen
"googleLoginFailed": "Google login failed", // Google-Anmeldung fehlgeschlagen
"emailExists": "A user with this email address already exists. Please use another email address or log in." // Ein Benutzer mit dieser E-Mail-Adresse existiert bereits. Bitte verwenden Sie eine andere E-Mail-Adresse oder melden Sie sich an.
},
"success": {
"registerComplete": "Registration successful. You can now log in." // Registrierung erfolgreich. Sie können sich jetzt anmelden.
}
};

View File

@@ -3,6 +3,7 @@ import navigation from './navigation.js';
import auth from './auth.js';
import cart from './cart.js';
import product from './product.js';
import productDialogs from './productDialogs.js';
import search from './search.js';
import sorting from './sorting.js';
import chat from './chat.js';
@@ -18,6 +19,7 @@ import pages from './pages.js';
import orders from './orders.js';
import settings from './settings.js';
import common from './common.js';
import kitConfig from './kitConfig.js';
import legalDatenschutzBasic from './legal-datenschutz-basic.js';
import legalDatenschutzCustomer from './legal-datenschutz-customer.js';
import legalDatenschutzGoogleOrders from './legal-datenschutz-google-orders.js';
@@ -35,6 +37,7 @@ export default {
"auth": auth,
"cart": cart,
"product": product,
"productDialogs": productDialogs,
"search": search,
"sorting": sorting,
"chat": chat,
@@ -50,6 +53,7 @@ export default {
"orders": orders,
"settings": settings,
"common": common,
"kitConfig": kitConfig,
"legalDatenschutzBasic": legalDatenschutzBasic,
"legalDatenschutzCustomer": legalDatenschutzCustomer,
"legalDatenschutzGoogleOrders": legalDatenschutzGoogleOrders,

View File

@@ -0,0 +1,43 @@
export default {
"pageTitle": "🌱 Growbox Configurator", // 🌱 Growbox Konfigurator
"pageSubtitle": "Put together your perfect indoor grow setup", // Stelle dein perfektes Indoor Grow Setup zusammen
"bundleDiscountTitle": "🎯 Secure bundle discount!", // 🎯 Bundle-Rabatt sichern!
"loadingProducts": "Loading growbox products...", // Lade Growbox-Produkte...
"loadingLighting": "Loading lighting products...", // Lade Beleuchtungs-Produkte...
"loadingVentilation": "Loading ventilation products...", // Lade Belüftungs-Produkte...
"loadingExtras": "Loading extras...", // Lade Extras...
"noProductsAvailable": "No products available for this size", // Keine Produkte für diese Größe verfügbar
"noLightingAvailable": "No suitable lights available for tent size {{shape}}.", // Keine passenden Lampen für Zeltgröße {{shape}} verfügbar.
"noVentilationAvailable": "No suitable ventilation available for tent size {{shape}}.", // Keine passenden Belüftung für Zeltgröße {{shape}} verfügbar.
"noExtrasAvailable": "No extras available", // Keine Extras verfügbar
"selectShapeTitle": "1. Select growbox shape", // 1. Growbox-Form auswählen
"selectShapeSubtitle": "First select the base area of your growbox", // Wähle zuerst die Grundfläche deiner Growbox aus
"selectProductTitle": "2. Select growbox product", // 2. Growbox Produkt auswählen
"selectProductSubtitle": "Choose the right product for your {{shape}} growbox", // Wähle das passende Produkt für deine {{shape}} Growbox
"selectLightingTitle": "3. Choose lighting", // 3. Beleuchtung wählen
"selectLightingTitleShape": "3. Choose lighting - {{shape}}", // 3. Beleuchtung wählen - {{shape}}
"selectLightingSubtitle": "Please select a tent size first.", // Bitte wählen Sie zuerst eine Zeltgröße aus.
"selectVentilationTitle": "4. Select ventilation", // 4. Belüftung auswählen
"selectVentilationTitleShape": "4. Select ventilation - {{shape}}", // 4. Belüftung auswählen - {{shape}}
"selectVentilationSubtitle": "Please select a tent size first.", // Bitte wählen Sie zuerst eine Zeltgröße aus.
"selectExtrasTitle": "5. Add extras (optional)", // 5. Extras hinzufügen (optional)
"yourConfiguration": "🎯 Your configuration", // 🎯 Ihre Konfiguration
"growboxLabel": "Growbox: {{name}}", // Growbox: {{name}}
"lightingLabel": "Lighting: {{name}}", // Beleuchtung: {{name}}
"ventilationLabel": "Ventilation: {{name}}", // Belüftung: {{name}}
"extraLabel": "Extra: {{name}}", // Extra: {{name}}
"totalPrice": "Total price:", // Gesamtpreis:
"addToCart": "Add to cart", // In den Warenkorb
"selected": "✓ Selected", // ✓ Ausgewählt
"notDeliverable": "Not deliverable", // Nicht lieferbar
"noPrice": "No price", // Kein Preis
"setName": "Growbox set - {{shape}}", // Growbox Set - {{shape}}
"description60x60": "Compact - ideal for small spaces", // Kompakt - ideal für kleine Räume
"description80x80": "Medium - perfect balance", // Mittel - perfekte Balance
"description100x100": "Large - for experienced growers", // Groß - für erfahrene Grower
"description120x60": "Rectangular - maximum space usage", // Rechteckig - maximale Raumnutzung
"plants1to2": "1-2 plants", // 1-2 Pflanzen
"plants2to4": "2-4 plants", // 2-4 Pflanzen
"plants4to6": "4-6 plants", // 4-6 Pflanzen
"plants3to6": "3-6 plants" // 3-6 Pflanzen
};

View File

@@ -1,8 +1,9 @@
export default {
"status": {
"new": "In progress", // in Bearbeitung
"new": "in progress", // in Bearbeitung
"pending": "New", // Neu
"processing": "In progress", // in Bearbeitung
"processing": "in progress", // in Bearbeitung
"paid": "Paid", // Bezahlt
"cancelled": "Cancelled", // Storniert
"shipped": "Shipped", // Verschickt
"delivered": "Delivered", // Geliefert
@@ -24,6 +25,7 @@ export default {
"cancelOrder": "Cancel order" // Bestellung stornieren
},
"noOrders": "You have not placed any orders yet.", // Sie haben noch keine Bestellungen aufgegeben.
"trackShipment": "Track shipment", // Sendung verfolgen
"details": {
"title": "Order details: {{orderId}}", // Bestelldetails: {{orderId}}
"deliveryAddress": "Delivery address", // Lieferadresse
@@ -36,15 +38,14 @@ export default {
"item": "Item", // Artikel
"quantity": "Quantity", // Menge
"price": "Price", // Preis
"vat": "VAT", // MwSt.
"total": "Total", // Gesamt
"cancelOrder": "Cancel order" // Bestellung stornieren
},
"cancelConfirm": {
"title": "Cancel Order",
"message": "Are you sure you want to cancel this order?",
"confirm": "Cancel Order",
"cancelling": "Cancelling..."
"title": "Cancel order", // Bestellung stornieren
"message": "Are you sure you want to cancel this order?", // Sind Sie sicher, dass Sie diese Bestellung stornieren möchten?
"confirm": "Cancel", // Stornieren
"cancelling": "Cancelling..." // Wird storniert...
},
"processing": "Order is being completed...", // Bestellung wird abgeschlossen...
"processing": "Order is being completed..." // Bestellung wird abgeschlossen...
};

View File

@@ -8,9 +8,10 @@ export default {
"articleNumber": "Article number", // Artikelnummer
"manufacturer": "Manufacturer", // Hersteller
"inclVat": "incl. {{vat}}% VAT", // inkl. {{vat}}% MwSt.
"inclVatSimple": "incl. VAT", // inkl. MwSt.
"priceUnit": "{{price}}/{{unit}}", // {{price}}/{{unit}}
"new": "New", // Neu
"weeks": "weeks", // Wochen
"weeks": "Weeks", // Wochen
"arriving": "Arrival:", // Ankunft:
"inclVatFooter": "incl. {{vat}}% VAT,*", // inkl. {{vat}}% MwSt.,*
"availability": "Availability", // Verfügbarkeit
@@ -23,13 +24,13 @@ export default {
"youSave": "You save: {{amount}}", // Sie sparen: {{amount}}
"cheaperThanIndividual": "Cheaper than buying individually", // Günstiger als Einzelkauf
"pickupPrice": "Pickup price: €19.90 per cutting.", // Abholpreis: 19,90 € pro Steckling.
"consistsOf": "Consists of:", // Bestehend aus:
"consistsOf": "Consisting of:", // Bestehend aus:
"loadingComponentDetails": "{{index}}. Loading component details...", // {{index}}. Lädt Komponent-Details...
"loadingProduct": "Product is loading...", // Produkt wird geladen...
"loadingProduct": "Loading product...", // Produkt wird geladen...
"individualPriceTotal": "Total individual price:", // Einzelpreis gesamt:
"setPrice": "Set price:", // Set-Preis:
"yourSavings": "Your savings:", // Ihre Ersparnis:
"similarProducts": "Similar Products", // Ähnliche Produkte
"similarProducts": "Similar products", // Ähnliche Produkte
"countDisplay": {
"noProducts": "0 products", // 0 Produkte
"oneProduct": "1 product", // 1 Produkt

View File

@@ -0,0 +1,61 @@
export default {
"questionTitle": "Question about the product", // Frage zum Artikel
"questionSubtitle": "Do you have a question about this product? We are happy to help you.", // Haben Sie eine Frage zu diesem Artikel? Wir helfen Ihnen gerne weiter.
"questionSuccess": "Thank you for your question! We will get back to you as soon as possible.", // Vielen Dank für Ihre Frage! Wir werden uns schnellstmöglich bei Ihnen melden.
"nameLabel": "Name", // Name
"namePlaceholder": "Your name", // Ihr Name
"emailLabel": "Email", // E-Mail
"emailPlaceholder": "your.email@example.com", // ihre.email@example.com
"questionLabel": "Your question", // Ihre Frage
"questionPlaceholder": "Describe your question about this product...", // Beschreiben Sie Ihre Frage zu diesem Artikel...
"photosLabelQuestion": "Attach photos to your question (optional)", // Fotos zur Frage anhängen (optional)
"submitQuestion": "Send question", // Frage senden
"sending": "Sending...", // Wird gesendet...
"ratingTitle": "Rate product", // Artikel Bewerten
"ratingSubtitle": "Share your experience with this product and help other customers make their decision.", // Teilen Sie Ihre Erfahrungen mit diesem Artikel und helfen Sie anderen Kunden bei der Entscheidung.
"ratingSuccess": "Thank you for your review! It will be published after verification.", // Vielen Dank für Ihre Bewertung! Sie wird nach Prüfung veröffentlicht.
"emailHelper": "Your email will not be published", // Ihre E-Mail wird nicht veröffentlicht
"ratingLabel": "Rating *", // Bewertung *
"pleaseRate": "Please rate", // Bitte bewerten
"ratingStars": "{{rating}} out of 5 stars", // {{rating}} von 5 Sternen
"reviewLabel": "Your review (optional)", // Ihre Bewertung (optional)
"reviewPlaceholder": "Describe your experiences with this product...", // Beschreiben Sie Ihre Erfahrungen mit diesem Artikel...
"photosLabelRating": "Attach photos to your review (optional)", // Fotos zur Bewertung anhängen (optional)
"submitRating": "Submit review", // Bewertung abgeben
"errorGeneric": "An error occurred", // Ein Fehler ist aufgetreten
"errorPhotos": "Error processing photos", // Fehler beim Verarbeiten der Fotos
"availabilityTitle": "Request availability", // Verfügbarkeit anfragen
"availabilitySubtitle": "This product is currently unavailable. We will be happy to inform you as soon as it is back in stock.", // Dieser Artikel ist derzeit nicht verfügbar. Gerne informieren wir Sie, sobald er wieder lieferbar ist.
"availabilitySuccessEmail": "Thank you for your request! We will notify you by email as soon as the product is available again.", // Vielen Dank für Ihre Anfrage! Wir werden Sie per E-Mail informieren, sobald der Artikel wieder verfügbar ist.
"availabilitySuccessTelegram": "Thank you for your request! We will notify you via Telegram as soon as the product is available again.", // Vielen Dank für Ihre Anfrage! Wir werden Sie über Telegram informieren, sobald der Artikel wieder verfügbar ist.
"notificationMethodLabel": "How would you like to be notified?", // Wie möchten Sie benachrichtigt werden?
"telegramBotLabel": "Telegram Bot", // Telegram Bot
"telegramIdLabel": "Telegram ID", // Telegram ID
"telegramPlaceholder": "@yourTelegramName or Telegram ID", // @IhrTelegramName oder Telegram ID
"telegramHelper": "Enter your Telegram username (with @) or Telegram ID", // Geben Sie Ihren Telegram-Benutzernamen (mit @) oder Ihre Telegram-ID ein
"messageLabel": "Message (optional)", // Nachricht (optional)
"messagePlaceholder": "Additional information or questions...", // Zusätzliche Informationen oder Fragen...
"submitAvailability": "Request availability", // Verfügbarkeit anfragen
"photoUploadSelect": "Select photos", // Fotos auswählen
"photoUploadErrorMaxFiles": "Maximum {{max}} files allowed", // Maximal {{max}} Dateien erlaubt
"photoUploadErrorFileType": "Only image files (JPEG, PNG, GIF, WebP) are allowed", // Nur Bilddateien (JPEG, PNG, GIF, WebP) sind erlaubt
"photoUploadErrorFileSize": "File too large. Maximum: {{maxSize}}MB", // Datei zu groß. Maximum: {{maxSize}}MB
"photoUploadSelectedFiles": "{{count}} file(s) selected", // {{count}} Datei(en) ausgewählt
"photoUploadCompressed": "(compressed for upload)", // (komprimiert für Upload)
"photoUploadRemove": "Remove image", // Bild entfernen
"photoUploadLabelDefault": "Attach photos (optional)", // Fotos anhängen (optional)
"shareTitle": "Share", // Teilen
"shareEmbed": "Embed", // Einbetten
"shareCopyLink": "Copy link", // Link kopieren
"shareSuccessEmbed": "Embed code copied to clipboard!", // Einbettungscode wurde in die Zwischenablage kopiert!
"shareErrorEmbed": "Error copying the embed code", // Fehler beim Kopieren des Einbettungscodes
"shareSuccessLink": "Link copied to clipboard!", // Link wurde in die Zwischenablage kopiert!
"shareWhatsAppText": "Check out this product: {{name}}", // Schau dir dieses Produkt an: {{name}}
"shareTelegramText": "Check out this product: {{name}}", // Schau dir dieses Produkt an: {{name}}
"shareEmailSubject": "Product recommendation", // Produktempfehlung
"shareEmailBody": "Hello,\n\nI'd like to recommend this product to you:\n\n{{name}}\n{{url}}\n\nBest regards", // Hallo,\n\nich möchte dir dieses Produkt empfehlen:\n\n{{name}}\n{{url}}\n\nViele Grüße
};

View File

@@ -5,6 +5,7 @@ export default {
"profile": "Perfil",
"email": "Correo electrónico",
"password": "Contraseña",
"newPassword": "Nueva contraseña",
"confirmPassword": "Confirmar contraseña",
"forgotPassword": "¿Olvidaste tu contraseña?",
"loginWithGoogle": "Iniciar sesión con Google",
@@ -13,6 +14,7 @@ export default {
"privacyPolicy": "Política de privacidad",
"passwordMinLength": "La contraseña debe tener al menos 8 caracteres",
"newPasswordMinLength": "La nueva contraseña debe tener al menos 8 caracteres",
"backToHome": "Volver a la página principal",
"menu": {
"profile": "Perfil",
"myProfile": "Mi perfil",
@@ -21,5 +23,28 @@ export default {
"settings": "Configuración",
"adminDashboard": "Panel de administración",
"adminUsers": "Usuarios administradores"
},
"resetPassword": {
"title": "Restablecer contraseña",
"button": "Restablecer contraseña",
"success": "¡Tu contraseña ha sido restablecida con éxito! Serás redirigido para iniciar sesión en breve...",
"invalidToken": "No se encontró un token válido. Por favor, usa el enlace de tu correo electrónico.",
"error": "Error al restablecer la contraseña",
"emailSent": "Se ha enviado un enlace para restablecer tu contraseña a tu dirección de correo electrónico.",
"emailError": "Error al enviar el correo electrónico"
},
"errors": {
"fillAllFields": "Por favor, completa todos los campos",
"invalidEmail": "Por favor, introduce una dirección de correo electrónico válida",
"passwordsNotMatch": "Las contraseñas no coinciden",
"passwordsNotMatchShort": "Las contraseñas no coinciden",
"enterEmail": "Por favor, introduce tu dirección de correo electrónico",
"loginFailed": "Error al iniciar sesión",
"registerFailed": "Error al registrarse",
"googleLoginFailed": "Error al iniciar sesión con Google",
"emailExists": "Ya existe un usuario con esta dirección de correo electrónico. Por favor, usa otra dirección de correo electrónico o inicia sesión."
},
"success": {
"registerComplete": "Registro exitoso. Ahora puedes iniciar sesión."
}
};

View File

@@ -3,6 +3,7 @@ import navigation from './navigation.js';
import auth from './auth.js';
import cart from './cart.js';
import product from './product.js';
import productDialogs from './productDialogs.js';
import search from './search.js';
import sorting from './sorting.js';
import chat from './chat.js';
@@ -18,6 +19,7 @@ import pages from './pages.js';
import orders from './orders.js';
import settings from './settings.js';
import common from './common.js';
import kitConfig from './kitConfig.js';
import legalDatenschutzBasic from './legal-datenschutz-basic.js';
import legalDatenschutzCustomer from './legal-datenschutz-customer.js';
import legalDatenschutzGoogleOrders from './legal-datenschutz-google-orders.js';
@@ -35,6 +37,7 @@ export default {
"auth": auth,
"cart": cart,
"product": product,
"productDialogs": productDialogs,
"search": search,
"sorting": sorting,
"chat": chat,
@@ -50,6 +53,7 @@ export default {
"orders": orders,
"settings": settings,
"common": common,
"kitConfig": kitConfig,
"legalDatenschutzBasic": legalDatenschutzBasic,
"legalDatenschutzCustomer": legalDatenschutzCustomer,
"legalDatenschutzGoogleOrders": legalDatenschutzGoogleOrders,

View File

@@ -0,0 +1,43 @@
export default {
"pageTitle": "🌱 Configurador de Growbox",
"pageSubtitle": "Arma tu configuración perfecta para cultivo en interior",
"bundleDiscountTitle": "🎯 ¡Asegura el descuento por paquete!",
"loadingProducts": "Cargando productos de growbox...",
"loadingLighting": "Cargando productos de iluminación...",
"loadingVentilation": "Cargando productos de ventilación...",
"loadingExtras": "Cargando extras...",
"noProductsAvailable": "No hay productos disponibles para este tamaño",
"noLightingAvailable": "No hay luces adecuadas disponibles para el tamaño de tienda {{shape}}.",
"noVentilationAvailable": "No hay ventilación adecuada disponible para el tamaño de tienda {{shape}}.",
"noExtrasAvailable": "No hay extras disponibles",
"selectShapeTitle": "1. Selecciona la forma de la growbox",
"selectShapeSubtitle": "Primero selecciona el área base de tu growbox",
"selectProductTitle": "2. Selecciona el producto de growbox",
"selectProductSubtitle": "Elige el producto adecuado para tu growbox {{shape}}",
"selectLightingTitle": "3. Elige la iluminación",
"selectLightingTitleShape": "3. Elige la iluminación - {{shape}}",
"selectLightingSubtitle": "Por favor selecciona primero un tamaño de tienda.",
"selectVentilationTitle": "4. Selecciona la ventilación",
"selectVentilationTitleShape": "4. Selecciona la ventilación - {{shape}}",
"selectVentilationSubtitle": "Por favor selecciona primero un tamaño de tienda.",
"selectExtrasTitle": "5. Añade extras (opcional)",
"yourConfiguration": "🎯 Tu configuración",
"growboxLabel": "Growbox: {{name}}",
"lightingLabel": "Iluminación: {{name}}",
"ventilationLabel": "Ventilación: {{name}}",
"extraLabel": "Extra: {{name}}",
"totalPrice": "Precio total:",
"addToCart": "Añadir al carrito",
"selected": "✓ Seleccionado",
"notDeliverable": "No entregable",
"noPrice": "Sin precio",
"setName": "Set de growbox - {{shape}}",
"description60x60": "Compacto - ideal para espacios pequeños",
"description80x80": "Mediano - equilibrio perfecto",
"description100x100": "Grande - para cultivadores experimentados",
"description120x60": "Rectangular - uso máximo del espacio",
"plants1to2": "1-2 plantas",
"plants2to4": "2-4 plantas",
"plants4to6": "4-6 plantas",
"plants3to6": "3-6 plantas"
};

View File

@@ -1,8 +1,9 @@
export default {
"status": {
"new": "En progreso",
"new": "en progreso",
"pending": "Nuevo",
"processing": "En progreso",
"processing": "en progreso",
"paid": "Pagado",
"cancelled": "Cancelado",
"shipped": "Enviado",
"delivered": "Entregado",
@@ -24,6 +25,7 @@ export default {
"cancelOrder": "Cancelar pedido"
},
"noOrders": "Aún no has realizado ningún pedido.",
"trackShipment": "Rastrear envío",
"details": {
"title": "Detalles del pedido: {{orderId}}",
"deliveryAddress": "Dirección de entrega",
@@ -36,15 +38,14 @@ export default {
"item": "Artículo",
"quantity": "Cantidad",
"price": "Precio",
"vat": "IVA",
"total": "Total",
"cancelOrder": "Cancelar pedido"
},
"cancelConfirm": {
"title": "Cancelar pedido",
"message": "¿Estás seguro de que deseas cancelar este pedido?",
"confirm": "Cancelar pedido",
"message": "¿Está seguro de que desea cancelar este pedido?",
"confirm": "Cancelar",
"cancelling": "Cancelando..."
},
"processing": "El pedido se está completando...",
"processing": "El pedido se está completando..."
};

View File

@@ -8,9 +8,10 @@ export default {
"articleNumber": "Número de artículo",
"manufacturer": "Fabricante",
"inclVat": "incl. {{vat}}% IVA",
"inclVatSimple": "incl. IVA",
"priceUnit": "{{price}}/{{unit}}",
"new": "Nuevo",
"weeks": "semanas",
"weeks": "Semanas",
"arriving": "Llegada:",
"inclVatFooter": "incl. {{vat}}% IVA,*",
"availability": "Disponibilidad",
@@ -25,7 +26,7 @@ export default {
"pickupPrice": "Precio de recogida: 19,90 € por esqueje.",
"consistsOf": "Consiste en:",
"loadingComponentDetails": "{{index}}. Cargando detalles del componente...",
"loadingProduct": "Producto cargando...",
"loadingProduct": "Cargando producto...",
"individualPriceTotal": "Precio individual total:",
"setPrice": "Precio del set:",
"yourSavings": "Tus ahorros:",

View File

@@ -0,0 +1,61 @@
export default {
"questionTitle": "Pregunta sobre el producto",
"questionSubtitle": "¿Tiene alguna pregunta sobre este producto? Estamos encantados de ayudarle.",
"questionSuccess": "¡Gracias por su pregunta! Nos pondremos en contacto con usted lo antes posible.",
"nameLabel": "Nombre",
"namePlaceholder": "Su nombre",
"emailLabel": "Correo electrónico",
"emailPlaceholder": "su.email@ejemplo.com",
"questionLabel": "Su pregunta",
"questionPlaceholder": "Describa su pregunta sobre este producto...",
"photosLabelQuestion": "Adjunte fotos a su pregunta (opcional)",
"submitQuestion": "Enviar pregunta",
"sending": "Enviando...",
"ratingTitle": "Calificar producto",
"ratingSubtitle": "Comparta su experiencia con este producto y ayude a otros clientes a tomar su decisión.",
"ratingSuccess": "¡Gracias por su reseña! Se publicará después de la verificación.",
"emailHelper": "Su correo electrónico no será publicado",
"ratingLabel": "Calificación *",
"pleaseRate": "Por favor califique",
"ratingStars": "{{rating}} de 5 estrellas",
"reviewLabel": "Su reseña (opcional)",
"reviewPlaceholder": "Describa sus experiencias con este producto...",
"photosLabelRating": "Adjunte fotos a su reseña (opcional)",
"submitRating": "Enviar reseña",
"errorGeneric": "Ocurrió un error",
"errorPhotos": "Error al procesar las fotos",
"availabilityTitle": "Solicitar disponibilidad",
"availabilitySubtitle": "Este producto no está disponible actualmente. Le informaremos tan pronto como vuelva a estar en stock.",
"availabilitySuccessEmail": "¡Gracias por su solicitud! Le notificaremos por correo electrónico tan pronto como el producto esté disponible nuevamente.",
"availabilitySuccessTelegram": "¡Gracias por su solicitud! Le notificaremos vía Telegram tan pronto como el producto esté disponible nuevamente.",
"notificationMethodLabel": "¿Cómo desea ser notificado?",
"telegramBotLabel": "Bot de Telegram",
"telegramIdLabel": "ID de Telegram",
"telegramPlaceholder": "@suNombreTelegram o ID de Telegram",
"telegramHelper": "Ingrese su nombre de usuario de Telegram (con @) o ID de Telegram",
"messageLabel": "Mensaje (opcional)",
"messagePlaceholder": "Información adicional o preguntas...",
"submitAvailability": "Solicitar disponibilidad",
"photoUploadSelect": "Seleccionar fotos",
"photoUploadErrorMaxFiles": "Máximo {{max}} archivos permitidos",
"photoUploadErrorFileType": "Solo se permiten archivos de imagen (JPEG, PNG, GIF, WebP)",
"photoUploadErrorFileSize": "Archivo demasiado grande. Máximo: {{maxSize}}MB",
"photoUploadSelectedFiles": "{{count}} archivo(s) seleccionado(s)",
"photoUploadCompressed": "(comprimido para subir)",
"photoUploadRemove": "Eliminar imagen",
"photoUploadLabelDefault": "Adjuntar fotos (opcional)",
"shareTitle": "Compartir",
"shareEmbed": "Insertar",
"shareCopyLink": "Copiar enlace",
"shareSuccessEmbed": "¡Código de inserción copiado al portapapeles!",
"shareErrorEmbed": "Error al copiar el código de inserción",
"shareSuccessLink": "¡Enlace copiado al portapapeles!",
"shareWhatsAppText": "Mira este producto: {{name}}",
"shareTelegramText": "Mira este producto: {{name}}",
"shareEmailSubject": "Recomendación de producto",
"shareEmailBody": "Hola,\n\nQuisiera recomendarte este producto:\n\n{{name}}\n{{url}}\n\nSaludos cordiales"
};

View File

@@ -1,10 +1,11 @@
export default {
"login": "Connexion",
"register": "S'inscrire",
"register": "Inscription",
"logout": "Déconnexion",
"profile": "Profil",
"email": "Email",
"password": "Mot de passe",
"newPassword": "Nouveau mot de passe",
"confirmPassword": "Confirmer le mot de passe",
"forgotPassword": "Mot de passe oublié ?",
"loginWithGoogle": "Se connecter avec Google",
@@ -13,6 +14,7 @@ export default {
"privacyPolicy": "Politique de confidentialité",
"passwordMinLength": "Le mot de passe doit contenir au moins 8 caractères",
"newPasswordMinLength": "Le nouveau mot de passe doit contenir au moins 8 caractères",
"backToHome": "Retour à la page d'accueil",
"menu": {
"profile": "Profil",
"myProfile": "Mon profil",
@@ -21,5 +23,28 @@ export default {
"settings": "Paramètres",
"adminDashboard": "Tableau de bord Admin",
"adminUsers": "Utilisateurs Admin"
},
"resetPassword": {
"title": "Réinitialiser le mot de passe",
"button": "Réinitialiser le mot de passe",
"success": "Votre mot de passe a été réinitialisé avec succès ! Vous serez redirigé vers la connexion sous peu...",
"invalidToken": "Aucun jeton valide trouvé. Veuillez utiliser le lien de votre email.",
"error": "Erreur lors de la réinitialisation du mot de passe",
"emailSent": "Un lien pour réinitialiser votre mot de passe a été envoyé à votre adresse email.",
"emailError": "Erreur lors de l'envoi de l'email"
},
"errors": {
"fillAllFields": "Veuillez remplir tous les champs",
"invalidEmail": "Veuillez entrer une adresse email valide",
"passwordsNotMatch": "Les mots de passe ne correspondent pas",
"passwordsNotMatchShort": "Les mots de passe ne correspondent pas",
"enterEmail": "Veuillez entrer votre adresse email",
"loginFailed": "Échec de la connexion",
"registerFailed": "Échec de l'inscription",
"googleLoginFailed": "Échec de la connexion Google",
"emailExists": "Un utilisateur avec cette adresse email existe déjà. Veuillez utiliser une autre adresse email ou vous connecter."
},
"success": {
"registerComplete": "Inscription réussie. Vous pouvez maintenant vous connecter."
}
};

View File

@@ -3,6 +3,7 @@ import navigation from './navigation.js';
import auth from './auth.js';
import cart from './cart.js';
import product from './product.js';
import productDialogs from './productDialogs.js';
import search from './search.js';
import sorting from './sorting.js';
import chat from './chat.js';
@@ -18,6 +19,7 @@ import pages from './pages.js';
import orders from './orders.js';
import settings from './settings.js';
import common from './common.js';
import kitConfig from './kitConfig.js';
import legalDatenschutzBasic from './legal-datenschutz-basic.js';
import legalDatenschutzCustomer from './legal-datenschutz-customer.js';
import legalDatenschutzGoogleOrders from './legal-datenschutz-google-orders.js';
@@ -35,6 +37,7 @@ export default {
"auth": auth,
"cart": cart,
"product": product,
"productDialogs": productDialogs,
"search": search,
"sorting": sorting,
"chat": chat,
@@ -50,6 +53,7 @@ export default {
"orders": orders,
"settings": settings,
"common": common,
"kitConfig": kitConfig,
"legalDatenschutzBasic": legalDatenschutzBasic,
"legalDatenschutzCustomer": legalDatenschutzCustomer,
"legalDatenschutzGoogleOrders": legalDatenschutzGoogleOrders,

View File

@@ -0,0 +1,43 @@
export default {
"pageTitle": "🌱 Configurateur Growbox",
"pageSubtitle": "Assemblez votre installation de culture d'intérieur parfaite",
"bundleDiscountTitle": "🎯 Profitez d'une remise sur le pack !",
"loadingProducts": "Chargement des produits growbox...",
"loadingLighting": "Chargement des produits d'éclairage...",
"loadingVentilation": "Chargement des produits de ventilation...",
"loadingExtras": "Chargement des extras...",
"noProductsAvailable": "Aucun produit disponible pour cette taille",
"noLightingAvailable": "Aucun éclairage adapté disponible pour la taille de tente {{shape}}.",
"noVentilationAvailable": "Aucune ventilation adaptée disponible pour la taille de tente {{shape}}.",
"noExtrasAvailable": "Aucun extra disponible",
"selectShapeTitle": "1. Sélectionnez la forme de la growbox",
"selectShapeSubtitle": "Sélectionnez d'abord la surface de base de votre growbox",
"selectProductTitle": "2. Sélectionnez le produit growbox",
"selectProductSubtitle": "Choisissez le produit adapté pour votre growbox {{shape}}",
"selectLightingTitle": "3. Choisissez l'éclairage",
"selectLightingTitleShape": "3. Choisissez l'éclairage - {{shape}}",
"selectLightingSubtitle": "Veuillez d'abord sélectionner une taille de tente.",
"selectVentilationTitle": "4. Sélectionnez la ventilation",
"selectVentilationTitleShape": "4. Sélectionnez la ventilation - {{shape}}",
"selectVentilationSubtitle": "Veuillez d'abord sélectionner une taille de tente.",
"selectExtrasTitle": "5. Ajoutez des extras (optionnel)",
"yourConfiguration": "🎯 Votre configuration",
"growboxLabel": "Growbox : {{name}}",
"lightingLabel": "Éclairage : {{name}}",
"ventilationLabel": "Ventilation : {{name}}",
"extraLabel": "Extra : {{name}}",
"totalPrice": "Prix total :",
"addToCart": "Ajouter au panier",
"selected": "✓ Sélectionné",
"notDeliverable": "Non livrable",
"noPrice": "Pas de prix",
"setName": "Set Growbox - {{shape}}",
"description60x60": "Compact - idéal pour les petits espaces",
"description80x80": "Moyen - équilibre parfait",
"description100x100": "Grand - pour cultivateurs expérimentés",
"description120x60": "Rectangulaire - utilisation maximale de l'espace",
"plants1to2": "1-2 plantes",
"plants2to4": "2-4 plantes",
"plants4to6": "4-6 plantes",
"plants3to6": "3-6 plantes"
};

View File

@@ -1,8 +1,9 @@
export default {
"status": {
"new": "En cours",
"new": "en cours",
"pending": "Nouveau",
"processing": "En cours",
"processing": "en cours",
"paid": "Payé",
"cancelled": "Annulé",
"shipped": "Expédié",
"delivered": "Livré",
@@ -24,6 +25,7 @@ export default {
"cancelOrder": "Annuler la commande"
},
"noOrders": "Vous n'avez pas encore passé de commandes.",
"trackShipment": "Suivre l'envoi",
"details": {
"title": "Détails de la commande : {{orderId}}",
"deliveryAddress": "Adresse de livraison",
@@ -36,14 +38,13 @@ export default {
"item": "Article",
"quantity": "Quantité",
"price": "Prix",
"vat": "TVA",
"total": "Total",
"cancelOrder": "Annuler la commande"
},
"cancelConfirm": {
"title": "Annuler la commande",
"message": "Êtes-vous sûr de vouloir annuler cette commande ?",
"confirm": "Annuler la commande",
"confirm": "Annuler",
"cancelling": "Annulation en cours..."
},
"processing": "La commande est en cours de traitement..."

View File

@@ -8,9 +8,10 @@ export default {
"articleNumber": "Numéro d'article",
"manufacturer": "Fabricant",
"inclVat": "TTC {{vat}}%",
"inclVatSimple": "TTC",
"priceUnit": "{{price}}/{{unit}}",
"new": "Nouveau",
"weeks": "semaines",
"weeks": "Semaines",
"arriving": "Arrivée :",
"inclVatFooter": "TTC {{vat}}%,*",
"availability": "Disponibilité",
@@ -25,7 +26,7 @@ export default {
"pickupPrice": "Prix de retrait : 19,90 € par bouture.",
"consistsOf": "Composé de :",
"loadingComponentDetails": "{{index}}. Chargement des détails du composant...",
"loadingProduct": "Le produit est en cours de chargement...",
"loadingProduct": "Chargement du produit...",
"individualPriceTotal": "Prix individuel total :",
"setPrice": "Prix du lot :",
"yourSavings": "Vos économies :",

View File

@@ -0,0 +1,61 @@
export default {
"questionTitle": "Question sur le produit",
"questionSubtitle": "Vous avez une question sur ce produit ? Nous sommes heureux de vous aider.",
"questionSuccess": "Merci pour votre question ! Nous vous répondrons dès que possible.",
"nameLabel": "Nom",
"namePlaceholder": "Votre nom",
"emailLabel": "Email",
"emailPlaceholder": "votre.email@exemple.com",
"questionLabel": "Votre question",
"questionPlaceholder": "Décrivez votre question à propos de ce produit...",
"photosLabelQuestion": "Joindre des photos à votre question (optionnel)",
"submitQuestion": "Envoyer la question",
"sending": "Envoi en cours...",
"ratingTitle": "Noter le produit",
"ratingSubtitle": "Partagez votre expérience avec ce produit et aidez les autres clients à prendre leur décision.",
"ratingSuccess": "Merci pour votre avis ! Il sera publié après vérification.",
"emailHelper": "Votre email ne sera pas publié",
"ratingLabel": "Note *",
"pleaseRate": "Veuillez noter",
"ratingStars": "{{rating}} sur 5 étoiles",
"reviewLabel": "Votre avis (optionnel)",
"reviewPlaceholder": "Décrivez vos expériences avec ce produit...",
"photosLabelRating": "Joindre des photos à votre avis (optionnel)",
"submitRating": "Soumettre l'avis",
"errorGeneric": "Une erreur est survenue",
"errorPhotos": "Erreur lors du traitement des photos",
"availabilityTitle": "Demander la disponibilité",
"availabilitySubtitle": "Ce produit est actuellement indisponible. Nous serons heureux de vous informer dès qu'il sera de nouveau en stock.",
"availabilitySuccessEmail": "Merci pour votre demande ! Nous vous informerons par email dès que le produit sera à nouveau disponible.",
"availabilitySuccessTelegram": "Merci pour votre demande ! Nous vous informerons via Telegram dès que le produit sera à nouveau disponible.",
"notificationMethodLabel": "Comment souhaitez-vous être informé ?",
"telegramBotLabel": "Bot Telegram",
"telegramIdLabel": "ID Telegram",
"telegramPlaceholder": "@votreNomTelegram ou ID Telegram",
"telegramHelper": "Entrez votre nom d'utilisateur Telegram (avec @) ou votre ID Telegram",
"messageLabel": "Message (optionnel)",
"messagePlaceholder": "Informations supplémentaires ou questions...",
"submitAvailability": "Demander la disponibilité",
"photoUploadSelect": "Sélectionner des photos",
"photoUploadErrorMaxFiles": "Maximum {{max}} fichiers autorisés",
"photoUploadErrorFileType": "Seuls les fichiers image (JPEG, PNG, GIF, WebP) sont autorisés",
"photoUploadErrorFileSize": "Fichier trop volumineux. Maximum : {{maxSize}}Mo",
"photoUploadSelectedFiles": "{{count}} fichier(s) sélectionné(s)",
"photoUploadCompressed": "(compressé pour l'envoi)",
"photoUploadRemove": "Supprimer l'image",
"photoUploadLabelDefault": "Joindre des photos (optionnel)",
"shareTitle": "Partager",
"shareEmbed": "Intégrer",
"shareCopyLink": "Copier le lien",
"shareSuccessEmbed": "Code d'intégration copié dans le presse-papiers !",
"shareErrorEmbed": "Erreur lors de la copie du code d'intégration",
"shareSuccessLink": "Lien copié dans le presse-papiers !",
"shareWhatsAppText": "Découvrez ce produit : {{name}}",
"shareTelegramText": "Découvrez ce produit : {{name}}",
"shareEmailSubject": "Recommandation de produit",
"shareEmailBody": "Bonjour,\n\nJe souhaite vous recommander ce produit :\n\n{{name}}\n{{url}}\n\nCordialement"
};

View File

@@ -5,6 +5,7 @@ export default {
"profile": "Profil",
"email": "Email",
"password": "Lozinka",
"newPassword": "Nova lozinka",
"confirmPassword": "Potvrdi lozinku",
"forgotPassword": "Zaboravili ste lozinku?",
"loginWithGoogle": "Prijavite se putem Googlea",
@@ -13,6 +14,7 @@ export default {
"privacyPolicy": "Pravila privatnosti",
"passwordMinLength": "Lozinka mora imati najmanje 8 znakova",
"newPasswordMinLength": "Nova lozinka mora imati najmanje 8 znakova",
"backToHome": "Natrag na početnu stranicu",
"menu": {
"profile": "Profil",
"myProfile": "Moj profil",
@@ -21,5 +23,28 @@ export default {
"settings": "Postavke",
"adminDashboard": "Admin nadzorna ploča",
"adminUsers": "Admin korisnici"
},
"resetPassword": {
"title": "Resetiraj lozinku",
"button": "Resetiraj lozinku",
"success": "Vaša lozinka je uspješno resetirana! Uskoro ćete biti preusmjereni na prijavu...",
"invalidToken": "Nije pronađen valjani token. Molimo koristite link iz vaše e-pošte.",
"error": "Pogreška pri resetiranju lozinke",
"emailSent": "Link za resetiranje lozinke poslan je na vašu e-mail adresu.",
"emailError": "Pogreška pri slanju e-pošte"
},
"errors": {
"fillAllFields": "Molimo ispunite sva polja",
"invalidEmail": "Molimo unesite valjanu e-mail adresu",
"passwordsNotMatch": "Lozinke se ne podudaraju",
"passwordsNotMatchShort": "Lozinke se ne podudaraju",
"enterEmail": "Molimo unesite vašu e-mail adresu",
"loginFailed": "Prijava nije uspjela",
"registerFailed": "Registracija nije uspjela",
"googleLoginFailed": "Prijava putem Googlea nije uspjela",
"emailExists": "Korisnik s ovom e-mail adresom već postoji. Molimo koristite drugu e-mail adresu ili se prijavite."
},
"success": {
"registerComplete": "Registracija uspješna. Sada se možete prijaviti."
}
};

View File

@@ -3,6 +3,7 @@ import navigation from './navigation.js';
import auth from './auth.js';
import cart from './cart.js';
import product from './product.js';
import productDialogs from './productDialogs.js';
import search from './search.js';
import sorting from './sorting.js';
import chat from './chat.js';
@@ -18,6 +19,7 @@ import pages from './pages.js';
import orders from './orders.js';
import settings from './settings.js';
import common from './common.js';
import kitConfig from './kitConfig.js';
import legalDatenschutzBasic from './legal-datenschutz-basic.js';
import legalDatenschutzCustomer from './legal-datenschutz-customer.js';
import legalDatenschutzGoogleOrders from './legal-datenschutz-google-orders.js';
@@ -35,6 +37,7 @@ export default {
"auth": auth,
"cart": cart,
"product": product,
"productDialogs": productDialogs,
"search": search,
"sorting": sorting,
"chat": chat,
@@ -50,6 +53,7 @@ export default {
"orders": orders,
"settings": settings,
"common": common,
"kitConfig": kitConfig,
"legalDatenschutzBasic": legalDatenschutzBasic,
"legalDatenschutzCustomer": legalDatenschutzCustomer,
"legalDatenschutzGoogleOrders": legalDatenschutzGoogleOrders,

View File

@@ -0,0 +1,43 @@
export default {
"pageTitle": "🌱 Konfigurator Growboxa",
"pageSubtitle": "Sastavite svoju savršenu unutarnju grow postavu",
"bundleDiscountTitle": "🎯 Osigurajte popust na paket!",
"loadingProducts": "Učitavanje growbox proizvoda...",
"loadingLighting": "Učitavanje proizvoda za osvjetljenje...",
"loadingVentilation": "Učitavanje proizvoda za ventilaciju...",
"loadingExtras": "Učitavanje dodataka...",
"noProductsAvailable": "Nema dostupnih proizvoda za ovu veličinu",
"noLightingAvailable": "Nema prikladnih svjetala za veličinu šatora {{shape}}.",
"noVentilationAvailable": "Nema prikladne ventilacije za veličinu šatora {{shape}}.",
"noExtrasAvailable": "Nema dodataka",
"selectShapeTitle": "1. Odaberite oblik growboxa",
"selectShapeSubtitle": "Prvo odaberite osnovnu površinu vašeg growboxa",
"selectProductTitle": "2. Odaberite growbox proizvod",
"selectProductSubtitle": "Odaberite pravi proizvod za vaš {{shape}} growbox",
"selectLightingTitle": "3. Odaberite osvjetljenje",
"selectLightingTitleShape": "3. Odaberite osvjetljenje - {{shape}}",
"selectLightingSubtitle": "Molimo prvo odaberite veličinu šatora.",
"selectVentilationTitle": "4. Odaberite ventilaciju",
"selectVentilationTitleShape": "4. Odaberite ventilaciju - {{shape}}",
"selectVentilationSubtitle": "Molimo prvo odaberite veličinu šatora.",
"selectExtrasTitle": "5. Dodajte dodatke (opcionalno)",
"yourConfiguration": "🎯 Vaša konfiguracija",
"growboxLabel": "Growbox: {{name}}",
"lightingLabel": "Osvjetljenje: {{name}}",
"ventilationLabel": "Ventilacija: {{name}}",
"extraLabel": "Dodatak: {{name}}",
"totalPrice": "Ukupna cijena:",
"addToCart": "Dodaj u košaricu",
"selected": "✓ Odabrano",
"notDeliverable": "Nije dostupno za dostavu",
"noPrice": "Nema cijene",
"setName": "Growbox set - {{shape}}",
"description60x60": "Kompaktan - idealan za male prostore",
"description80x80": "Srednji - savršen balans",
"description100x100": "Veliki - za iskusne uzgajivače",
"description120x60": "Pravokutni - maksimalno iskorištavanje prostora",
"plants1to2": "1-2 biljke",
"plants2to4": "2-4 biljke",
"plants4to6": "4-6 biljaka",
"plants3to6": "3-6 biljaka"
};

View File

@@ -1,8 +1,9 @@
export default {
"status": {
"new": "U tijeku",
"new": "u tijeku",
"pending": "Novo",
"processing": "U tijeku",
"processing": "u tijeku",
"paid": "Plaćeno",
"cancelled": "Otkazano",
"shipped": "Poslano",
"delivered": "Isporučeno",
@@ -23,7 +24,8 @@ export default {
"viewDetails": "Pogledaj detalje",
"cancelOrder": "Otkaži narudžbu"
},
"noOrders": "Još niste napravili nijednu narudžbu.",
"noOrders": "Još niste izvršili nijednu narudžbu.",
"trackShipment": "Prati pošiljku",
"details": {
"title": "Detalji narudžbe: {{orderId}}",
"deliveryAddress": "Adresa dostave",
@@ -36,15 +38,14 @@ export default {
"item": "Artikl",
"quantity": "Količina",
"price": "Cijena",
"vat": "PDV",
"total": "Ukupno",
"cancelOrder": "Otkaži narudžbu"
},
"cancelConfirm": {
"title": "Otkaži narudžbu",
"message": "Jeste li sigurni da želite otkazati ovu narudžbu?",
"confirm": "Otkaži narudžbu",
"confirm": "Otkaži",
"cancelling": "Otkazivanje..."
},
"processing": "Narudžba se dovršava...",
"processing": "Narudžba se obrađuje..."
};

View File

@@ -3,14 +3,15 @@ export default {
"loadingDescription": "Učitavanje opisa proizvoda...",
"notFound": "Proizvod nije pronađen",
"notFoundDescription": "Proizvod koji tražite ne postoji ili je uklonjen.",
"backToHome": "Povratak na početnu stranicu",
"backToHome": "Natrag na početnu stranicu",
"error": "Greška",
"articleNumber": "Broj artikla",
"manufacturer": "Proizvođač",
"inclVat": "uključujući {{vat}}% PDV-a",
"inclVatSimple": "uključujući PDV",
"priceUnit": "{{price}}/{{unit}}",
"new": "Novo",
"weeks": "tjedana",
"weeks": "Tjedni",
"arriving": "Dolazak:",
"inclVatFooter": "uključujući {{vat}}% PDV-a,*",
"availability": "Dostupnost",
@@ -25,7 +26,7 @@ export default {
"pickupPrice": "Cijena preuzimanja: 19,90 € po reznici.",
"consistsOf": "Sastoji se od:",
"loadingComponentDetails": "{{index}}. Učitavanje detalja komponente...",
"loadingProduct": "Proizvod se učitava...",
"loadingProduct": "Učitavanje proizvoda...",
"individualPriceTotal": "Ukupna pojedinačna cijena:",
"setPrice": "Cijena seta:",
"yourSavings": "Vaša ušteda:",
@@ -38,7 +39,7 @@ export default {
"filteredOneProduct": "{{filtered}} od 1 proizvoda",
"xOfYProducts": "{{x}} od {{y}} proizvoda"
},
"removeFiltersToSee": "Uklonite filtere da vidite proizvode",
"removeFiltersToSee": "Uklonite filtre da vidite proizvode",
"outOfStock": "Nema na skladištu",
"fromXProducts": "od {{count}} proizvoda",
"discount": {

View File

@@ -0,0 +1,61 @@
export default {
"questionTitle": "Pitanje o proizvodu",
"questionSubtitle": "Imate li pitanje o ovom proizvodu? Rado ćemo vam pomoći.",
"questionSuccess": "Hvala na vašem pitanju! Javili smo vam se što je prije moguće.",
"nameLabel": "Ime",
"namePlaceholder": "Vaše ime",
"emailLabel": "Email",
"emailPlaceholder": "your.email@example.com",
"questionLabel": "Vaše pitanje",
"questionPlaceholder": "Opišite svoje pitanje o ovom proizvodu...",
"photosLabelQuestion": "Priložite fotografije uz svoje pitanje (opcionalno)",
"submitQuestion": "Pošalji pitanje",
"sending": "Šalje se...",
"ratingTitle": "Ocijenite proizvod",
"ratingSubtitle": "Podijelite svoje iskustvo s ovim proizvodom i pomozite drugim kupcima u odluci.",
"ratingSuccess": "Hvala na vašoj recenziji! Bit će objavljena nakon provjere.",
"emailHelper": "Vaš email neće biti objavljen",
"ratingLabel": "Ocjena *",
"pleaseRate": "Molimo ocijenite",
"ratingStars": "{{rating}} od 5 zvjezdica",
"reviewLabel": "Vaša recenzija (opcionalno)",
"reviewPlaceholder": "Opišite svoja iskustva s ovim proizvodom...",
"photosLabelRating": "Priložite fotografije uz svoju recenziju (opcionalno)",
"submitRating": "Pošalji recenziju",
"errorGeneric": "Došlo je do pogreške",
"errorPhotos": "Pogreška pri obradi fotografija",
"availabilityTitle": "Zatražite dostupnost",
"availabilitySubtitle": "Ovaj proizvod trenutno nije dostupan. Obavijestit ćemo vas čim ponovno bude na skladištu.",
"availabilitySuccessEmail": "Hvala na vašem zahtjevu! Obavijestit ćemo vas putem emaila čim proizvod ponovno bude dostupan.",
"availabilitySuccessTelegram": "Hvala na vašem zahtjevu! Obavijestit ćemo vas putem Telegrama čim proizvod ponovno bude dostupan.",
"notificationMethodLabel": "Kako želite biti obaviješteni?",
"telegramBotLabel": "Telegram Bot",
"telegramIdLabel": "Telegram ID",
"telegramPlaceholder": "@yourTelegramName or Telegram ID",
"telegramHelper": "Unesite svoje Telegram korisničko ime (s @) ili Telegram ID",
"messageLabel": "Poruka (opcionalno)",
"messagePlaceholder": "Dodatne informacije ili pitanja...",
"submitAvailability": "Zatražite dostupnost",
"photoUploadSelect": "Odaberite fotografije",
"photoUploadErrorMaxFiles": "Dozvoljeno maksimalno {{max}} datoteka",
"photoUploadErrorFileType": "Dozvoljene su samo slikovne datoteke (JPEG, PNG, GIF, WebP)",
"photoUploadErrorFileSize": "Datoteka je prevelika. Maksimum: {{maxSize}}MB",
"photoUploadSelectedFiles": "{{count}} datoteka je odabrano",
"photoUploadCompressed": "(komprimirano za prijenos)",
"photoUploadRemove": "Ukloni sliku",
"photoUploadLabelDefault": "Priložite fotografije (opcionalno)",
"shareTitle": "Podijeli",
"shareEmbed": "Ugradi",
"shareCopyLink": "Kopiraj link",
"shareSuccessEmbed": "Kod za ugradnju kopiran u međuspremnik!",
"shareErrorEmbed": "Pogreška pri kopiranju koda za ugradnju",
"shareSuccessLink": "Link kopiran u međuspremnik!",
"shareWhatsAppText": "Pogledajte ovaj proizvod: {{name}}",
"shareTelegramText": "Pogledajte ovaj proizvod: {{name}}",
"shareEmailSubject": "Preporuka proizvoda",
"shareEmailBody": "Pozdrav,\n\nŽelio/la bih vam preporučiti ovaj proizvod:\n\n{{name}}\n{{url}}\n\nSrdačan pozdrav"
};

View File

@@ -5,6 +5,7 @@ export default {
"profile": "Profil",
"email": "Email",
"password": "Jelszó",
"newPassword": "Új jelszó",
"confirmPassword": "Jelszó megerősítése",
"forgotPassword": "Elfelejtett jelszó?",
"loginWithGoogle": "Bejelentkezés Google-lal",
@@ -13,13 +14,37 @@ export default {
"privacyPolicy": "Adatvédelmi szabályzatot",
"passwordMinLength": "A jelszónak legalább 8 karakter hosszúnak kell lennie",
"newPasswordMinLength": "Az új jelszónak legalább 8 karakter hosszúnak kell lennie",
"backToHome": "Vissza a kezdőlapra",
"menu": {
"profile": "Profil",
"myProfile": "Saját profilom",
"myProfile": "Saját profil",
"checkout": "Pénztár",
"orders": "Rendelések",
"settings": "Beállítások",
"adminDashboard": "Admin Vezérlőpult",
"adminUsers": "Admin Felhasználók"
},
"resetPassword": {
"title": "Jelszó visszaállítása",
"button": "Jelszó visszaállítása",
"success": "A jelszavad sikeresen visszaállítva! Hamarosan átirányítunk a bejelentkezéshez...",
"invalidToken": "Érvényes token nem található. Kérjük, használd az emailedben található linket.",
"error": "Hiba történt a jelszó visszaállítása során",
"emailSent": "Egy link a jelszó visszaállításához elküldésre került az email címedre.",
"emailError": "Hiba történt az email küldése során"
},
"errors": {
"fillAllFields": "Kérjük, tölts ki minden mezőt",
"invalidEmail": "Kérjük, adj meg egy érvényes email címet",
"passwordsNotMatch": "A jelszavak nem egyeznek",
"passwordsNotMatchShort": "A jelszavak nem egyeznek",
"enterEmail": "Kérjük, add meg az email címed",
"loginFailed": "Bejelentkezés sikertelen",
"registerFailed": "Regisztráció sikertelen",
"googleLoginFailed": "Google bejelentkezés sikertelen",
"emailExists": "Már létezik felhasználó ezzel az email címmel. Kérjük, használj másik email címet vagy jelentkezz be."
},
"success": {
"registerComplete": "Sikeres regisztráció. Most már bejelentkezhetsz."
}
};

View File

@@ -3,6 +3,7 @@ import navigation from './navigation.js';
import auth from './auth.js';
import cart from './cart.js';
import product from './product.js';
import productDialogs from './productDialogs.js';
import search from './search.js';
import sorting from './sorting.js';
import chat from './chat.js';
@@ -18,6 +19,7 @@ import pages from './pages.js';
import orders from './orders.js';
import settings from './settings.js';
import common from './common.js';
import kitConfig from './kitConfig.js';
import legalDatenschutzBasic from './legal-datenschutz-basic.js';
import legalDatenschutzCustomer from './legal-datenschutz-customer.js';
import legalDatenschutzGoogleOrders from './legal-datenschutz-google-orders.js';
@@ -35,6 +37,7 @@ export default {
"auth": auth,
"cart": cart,
"product": product,
"productDialogs": productDialogs,
"search": search,
"sorting": sorting,
"chat": chat,
@@ -50,6 +53,7 @@ export default {
"orders": orders,
"settings": settings,
"common": common,
"kitConfig": kitConfig,
"legalDatenschutzBasic": legalDatenschutzBasic,
"legalDatenschutzCustomer": legalDatenschutzCustomer,
"legalDatenschutzGoogleOrders": legalDatenschutzGoogleOrders,

Some files were not shown because too many files have changed in this diff Show More