i18n: translate image alt/aria + generated export documents (last English literals)
This commit is contained in:
parent
ac1b56ce76
commit
a3b7f311ce
15 changed files with 608 additions and 132 deletions
|
|
@ -333,7 +333,7 @@ export function CameraCapture({
|
|||
|
||||
{/* Captured preview */}
|
||||
{capturedImage && (
|
||||
<img src={capturedImage} alt="Captured" className="w-full h-full object-cover" />
|
||||
<img src={capturedImage} alt={t('legacyMiscUi.altCaptured')} className="w-full h-full object-cover" />
|
||||
)}
|
||||
|
||||
{/* Switch camera button */}
|
||||
|
|
|
|||
|
|
@ -482,7 +482,7 @@ export function CraftProfileForm({ config }: { config: CraftFormConfig }) {
|
|||
<div className={cn('relative w-20 h-20 rounded-2xl border-2 flex items-center justify-center overflow-hidden flex-shrink-0', image ? 'border-white/20' : 'border-dashed border-white/20')}>
|
||||
{image ? (
|
||||
<>
|
||||
<img src={image} alt="Profile" className="w-full h-full object-cover" />
|
||||
<img src={image} alt={t('legacyMiscUi.altProfile')} className="w-full h-full object-cover" />
|
||||
<button type="button" onClick={() => setImage(null)} className="absolute top-0.5 right-0.5 bg-black/70 rounded-full p-0.5">
|
||||
<X className="h-3 w-3 text-white" />
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -733,7 +733,7 @@ export function DeckCostCalculator() {
|
|||
|
||||
// ── Header ──
|
||||
doc.setFillColor(255, 0, 64); doc.rect(M, y - 12, 34, 3, 'F');
|
||||
setText(17, true, 15); doc.text('SkateSpots — Deck cost calculator', M, y + 8); y += 26;
|
||||
setText(17, true, 15); doc.text(t('legacyMiscUi.exportTitle'), M, y + 8); y += 26;
|
||||
setText(9, false, 120);
|
||||
doc.text(
|
||||
`${new Date().toLocaleDateString(dateLocaleCode(), { year: 'numeric', month: 'long', day: 'numeric' })} · ${cur === 'sats' ? 'sats (Bitcoin)' : cur === 'BTC' ? 'BTC (Bitcoin)' : cur}`,
|
||||
|
|
@ -741,52 +741,56 @@ export function DeckCostCalculator() {
|
|||
);
|
||||
y += 10;
|
||||
|
||||
section('Labor');
|
||||
kv('Hourly rate', fmt(s.hourlyRate));
|
||||
kv('Labour burden', `${s.overheadFactor}\u00d7`);
|
||||
kv('Loaded rate', `${fmt(s.hourlyRate * s.overheadFactor)}/hr`);
|
||||
section(t('legacyMiscUi.exportSectionLabor'));
|
||||
kv(t('legacyMiscUi.exportKvHourlyRate'), fmt(s.hourlyRate));
|
||||
kv(t('legacyMiscUi.exportKvLabourBurden'), `${s.overheadFactor}\u00d7`);
|
||||
kv(t('legacyMiscUi.exportKvLoadedRate'), `${fmt(s.hourlyRate * s.overheadFactor)}/hr`);
|
||||
|
||||
section('Equipment');
|
||||
section(t('legacyMiscUi.exportSectionEquipment'));
|
||||
s.equipment.forEach((e) => {
|
||||
const per = e.boards > 0 ? (e.cost + e.setupHrs * r.laborPerHr) / e.boards : 0;
|
||||
item(e.name, fmt(per), `${fmt(e.cost)}${e.setupHrs ? ` + ${e.setupHrs}h setup` : ''} \u00b7 ${e.boards.toLocaleString()} boards`);
|
||||
const setup = e.setupHrs ? ` ${t('legacyMiscUi.exportDetailSetup', { hours: e.setupHrs })}` : '';
|
||||
item(e.name, fmt(per), `${fmt(e.cost)}${setup} \u00b7 ${t('legacyMiscUi.exportDetailBoards', { boards: e.boards.toLocaleString() })}`);
|
||||
});
|
||||
kv('Equipment per board', fmt(r.equipment), true);
|
||||
kv(t('legacyMiscUi.exportKvEquipmentPerBoard'), fmt(r.equipment), true);
|
||||
|
||||
section('Operation expenses');
|
||||
kv('Boards / month', String(s.boardsPerMonth));
|
||||
kv('Rent / month', fmt(s.rent));
|
||||
kv('Utilities / month', fmt(s.utilities));
|
||||
kv('Subscriptions / month', fmt(s.subscriptions));
|
||||
kv('Per board', fmt(r.operations), true);
|
||||
section(t('legacyMiscUi.exportSectionOperations'));
|
||||
kv(t('legacyMiscUi.exportKvBoardsPerMonth'), String(s.boardsPerMonth));
|
||||
kv(t('legacyMiscUi.exportKvRentPerMonth'), fmt(s.rent));
|
||||
kv(t('legacyMiscUi.exportKvUtilitiesPerMonth'), fmt(s.utilities));
|
||||
kv(t('legacyMiscUi.exportKvSubscriptionsPerMonth'), fmt(s.subscriptions));
|
||||
kv(t('legacyMiscUi.exportKvPerBoard'), fmt(r.operations), true);
|
||||
|
||||
section('Materials');
|
||||
section(t('legacyMiscUi.exportSectionMaterials'));
|
||||
s.materials.forEach((m) => {
|
||||
const per = m.boards > 0 ? (m.orderCost + m.shipping + (m.orderMin + m.unloadMin) * r.laborPerMin) / m.boards : 0;
|
||||
item(m.name, fmt(per), `${fmt(m.orderCost)}${m.shipping ? ` + ${fmt(m.shipping)} ship` : ''} \u00b7 ${m.boards.toLocaleString()} boards`);
|
||||
const ship = m.shipping ? ` ${t('legacyMiscUi.exportDetailShip', { amount: fmt(m.shipping) })}` : '';
|
||||
item(m.name, fmt(per), `${fmt(m.orderCost)}${ship} \u00b7 ${t('legacyMiscUi.exportDetailBoards', { boards: m.boards.toLocaleString() })}`);
|
||||
});
|
||||
kv('Materials per board', fmt(r.materials), true);
|
||||
kv(t('legacyMiscUi.exportKvMaterialsPerBoard'), fmt(r.materials), true);
|
||||
|
||||
section('Process costs');
|
||||
section(t('legacyMiscUi.exportSectionProcesses'));
|
||||
s.processes.forEach((p) => {
|
||||
const rate = p.rate > 0 ? p.rate : r.laborPerHr;
|
||||
item(p.name, fmt(p.boards > 0 ? (p.minutes * (rate / 60)) / p.boards : 0), `${p.minutes} min${p.boards > 1 ? ` / ${p.boards} boards` : ''}${p.scrapPct ? ` \u00b7 ${p.scrapPct}% scrap` : ''}`);
|
||||
const perBoards = p.boards > 1 ? ` ${t('legacyMiscUi.exportDetailPerBoards', { boards: p.boards })}` : '';
|
||||
const scrap = p.scrapPct ? ` \u00b7 ${t('legacyMiscUi.exportDetailScrap', { pct: p.scrapPct })}` : '';
|
||||
item(p.name, fmt(p.boards > 0 ? (p.minutes * (rate / 60)) / p.boards : 0), `${t('legacyMiscUi.exportDetailMinutes', { minutes: p.minutes })}${perBoards}${scrap}`);
|
||||
});
|
||||
kv('Processes per board', fmt(r.processes), true);
|
||||
kv(t('legacyMiscUi.exportKvProcessesPerBoard'), fmt(r.processes), true);
|
||||
|
||||
section('Packaging');
|
||||
s.packaging.forEach((p) => item(p.name, fmt(p.materialsPerBoard + p.minutesPerBoard * r.laborPerMin), `${p.minutesPerBoard} min \u00b7 ${fmt(p.materialsPerBoard)} materials`));
|
||||
kv('Packaging per board', fmt(r.packaging), true);
|
||||
section(t('legacyMiscUi.exportSectionPackaging'));
|
||||
s.packaging.forEach((p) => item(p.name, fmt(p.materialsPerBoard + p.minutesPerBoard * r.laborPerMin), `${t('legacyMiscUi.exportDetailMinutes', { minutes: p.minutesPerBoard })} \u00b7 ${t('legacyMiscUi.exportDetailMaterials', { materials: fmt(p.materialsPerBoard) })}`));
|
||||
kv(t('legacyMiscUi.exportKvPackagingPerBoard'), fmt(r.packaging), true);
|
||||
|
||||
// ── Result (kept together) ──
|
||||
ensure(180);
|
||||
section('Result');
|
||||
kv('Markup', `${s.profitMargin}\u00d7 (\u2248 ${marginPct}% gross margin)`);
|
||||
kv('Waste (effective)', `${r.effectiveWastePct.toFixed(1)}%`);
|
||||
kv('Cost / board (no packaging)', fmt(r.costNoPackUnit));
|
||||
kv('Cost / board (with packaging)', fmt(r.costWithPackUnit));
|
||||
if (r.batchSetup > 0) kv('Batch setup / board', fmt(r.setupPerUnit));
|
||||
if (s.taxPct > 0) kv(`Tax (${s.taxPct}%${s.taxInclusive ? ', included' : ''})`, fmt(r.orderTax));
|
||||
section(t('legacyMiscUi.exportSectionResult'));
|
||||
kv(t('legacyMiscUi.exportKvMarkup'), t('legacyMiscUi.exportMarkupValue', { markup: s.profitMargin, pct: marginPct }));
|
||||
kv(t('legacyMiscUi.exportKvWasteEffective'), `${r.effectiveWastePct.toFixed(1)}%`);
|
||||
kv(t('legacyMiscUi.exportKvCostNoPack'), fmt(r.costNoPackUnit));
|
||||
kv(t('legacyMiscUi.exportKvCostWithPack'), fmt(r.costWithPackUnit));
|
||||
if (r.batchSetup > 0) kv(t('legacyMiscUi.exportKvBatchSetupPerBoard'), fmt(r.setupPerUnit));
|
||||
if (s.taxPct > 0) kv(s.taxInclusive ? t('legacyMiscUi.exportKvTaxIncluded', { pct: s.taxPct }) : t('legacyMiscUi.exportKvTax', { pct: s.taxPct }), fmt(r.orderTax));
|
||||
y += 6;
|
||||
|
||||
const priceBox = (label: string, value: string) => {
|
||||
|
|
@ -796,13 +800,13 @@ export function DeckCostCalculator() {
|
|||
setText(14, true, 30); doc.text(value, rightX - 10, y + 17, { align: 'right' });
|
||||
y += 36;
|
||||
};
|
||||
priceBox('Selling price (no packaging)', fmt(r.priceNoPack));
|
||||
priceBox('Selling price (with packaging)', fmt(r.priceWithPack));
|
||||
priceBox(t('legacyMiscUi.exportKvSellingNoPack'), fmt(r.priceNoPack));
|
||||
priceBox(t('legacyMiscUi.exportKvSellingWithPack'), fmt(r.priceWithPack));
|
||||
|
||||
ensure(30);
|
||||
y += 4;
|
||||
setText(8, false, 135); doc.text('Does not include shipping to the buyer.', M, y); y += 16;
|
||||
setText(8, false, 155); doc.text('Generated by SkateSpots \u00b7 skate-spots.org', M, y);
|
||||
setText(8, false, 135); doc.text(t('legacyMiscUi.exportShippingNote'), M, y); y += 16;
|
||||
setText(8, false, 155); doc.text(t('legacyMiscUi.exportGeneratedBy'), M, y);
|
||||
|
||||
// ── Page numbers ──
|
||||
const pages = doc.getNumberOfPages();
|
||||
|
|
@ -849,35 +853,35 @@ export function DeckCostCalculator() {
|
|||
doc.setDrawColor(220, 220, 220); doc.setLineWidth(0.7); doc.line(M, y, rightX, y); y += 30;
|
||||
|
||||
doc.setFont('helvetica', 'bold'); doc.setFontSize(26); doc.setTextColor(15, 15, 15);
|
||||
doc.text('QUOTE', M, y);
|
||||
doc.text(t('legacyMiscUi.exportQuoteTitle'), M, y);
|
||||
doc.setFont('helvetica', 'normal'); doc.setFontSize(9); doc.setTextColor(120, 120, 120);
|
||||
const now = new Date();
|
||||
doc.text(now.toLocaleDateString(dateLocaleCode(), { year: 'numeric', month: 'long', day: 'numeric' }), rightX, y, { align: 'right' });
|
||||
doc.text(`Ref Q-${String(q.number).padStart(4, '0')}`, rightX, y + 12, { align: 'right' });
|
||||
doc.text(t('legacyMiscUi.exportQuoteRef', { ref: `Q-${String(q.number).padStart(4, '0')}` }), rightX, y + 12, { align: 'right' });
|
||||
y += 34;
|
||||
|
||||
const qty = r.qty;
|
||||
doc.setDrawColor(230, 230, 230); doc.line(M, y, rightX, y); y += 20;
|
||||
doc.setFont('helvetica', 'bold'); doc.setFontSize(12); doc.setTextColor(20, 20, 20);
|
||||
doc.text(`Custom skateboard deck${qty > 1 ? ` \u00d7 ${qty}` : ''}`, M, y);
|
||||
doc.text(qty > 1 ? t('legacyMiscUi.exportQuoteLineItemQty', { qty }) : t('legacyMiscUi.exportQuoteLineItem'), M, y);
|
||||
doc.text(fmt(r.priceWithPack), rightX, y, { align: 'right' }); y += 16;
|
||||
doc.setFont('helvetica', 'normal'); doc.setFontSize(9); doc.setTextColor(120, 120, 120);
|
||||
doc.text(qty > 1 ? `Hand-built, finished and packed. ${fmt(r.priceWithPack)} each.` : 'Hand-built, finished and packed. Quantity 1.', M, y); y += 16;
|
||||
doc.text(qty > 1 ? t('legacyMiscUi.exportQuoteLineDescQty', { price: fmt(r.priceWithPack) }) : t('legacyMiscUi.exportQuoteLineDesc'), M, y); y += 16;
|
||||
doc.setDrawColor(230, 230, 230); doc.line(M, y, rightX, y); y += 24;
|
||||
|
||||
if (s.taxPct > 0 && !s.taxInclusive) {
|
||||
doc.setFont('helvetica', 'normal'); doc.setFontSize(9); doc.setTextColor(120, 120, 120);
|
||||
doc.text(qty > 1 ? `Subtotal (${qty} decks)` : 'Subtotal', M, y);
|
||||
doc.text(qty > 1 ? t('legacyMiscUi.exportQuoteSubtotalQty', { qty }) : t('legacyMiscUi.exportQuoteSubtotal'), M, y);
|
||||
doc.text(fmt(r.orderSubtotal), rightX, y, { align: 'right' }); y += 13;
|
||||
doc.text(`Tax / VAT (${s.taxPct}%)`, M, y);
|
||||
doc.text(t('legacyMiscUi.exportQuoteTax', { pct: s.taxPct }), M, y);
|
||||
doc.text(fmt(r.orderTax), rightX, y, { align: 'right' }); y += 16;
|
||||
}
|
||||
doc.setFont('helvetica', 'bold'); doc.setFontSize(14); doc.setTextColor(20, 20, 20);
|
||||
doc.text(qty > 1 ? `Total (${qty} decks)` : 'Total', M, y);
|
||||
doc.text(qty > 1 ? t('legacyMiscUi.exportQuoteTotalQty', { qty }) : t('legacyMiscUi.exportQuoteTotal'), M, y);
|
||||
doc.text(fmt(r.orderTotal), rightX, y, { align: 'right' }); y += 6;
|
||||
if (s.taxPct > 0 && s.taxInclusive) {
|
||||
doc.setFont('helvetica', 'normal'); doc.setFontSize(8); doc.setTextColor(120, 120, 120);
|
||||
y += 6; doc.text(`Includes ${s.taxPct}% tax (${fmt(r.orderTax)})`, M, y);
|
||||
y += 6; doc.text(t('legacyMiscUi.exportQuoteIncludesTax', { pct: s.taxPct, amount: fmt(r.orderTax) }), M, y);
|
||||
}
|
||||
y += 20;
|
||||
|
||||
|
|
@ -886,19 +890,19 @@ export function DeckCostCalculator() {
|
|||
const deposit = q.depositPct > 0 ? (r.orderTotal * q.depositPct) / 100 : 0;
|
||||
if (deposit > 0) {
|
||||
doc.setFont('helvetica', 'bold'); doc.setFontSize(10); doc.setTextColor(20, 20, 20);
|
||||
doc.text(`Deposit due now (${q.depositPct}%)`, M, y);
|
||||
doc.text(t('legacyMiscUi.exportQuoteDeposit', { pct: q.depositPct }), M, y);
|
||||
doc.text(fmt(deposit), rightX, y, { align: 'right' }); y += 13;
|
||||
doc.setFont('helvetica', 'normal'); doc.setTextColor(120, 120, 120);
|
||||
doc.text('Balance on completion', M, y);
|
||||
doc.text(t('legacyMiscUi.exportQuoteBalance'), M, y);
|
||||
doc.text(fmt(r.orderTotal - deposit), rightX, y, { align: 'right' }); y += 18;
|
||||
}
|
||||
|
||||
const payLines = [
|
||||
q.payMethods.length ? `Payment: ${q.payMethods.join(' \u00b7 ')}` : '',
|
||||
q.lightning.trim() ? `Lightning: ${q.lightning.trim()}` : '',
|
||||
q.paymentDetails.trim() ? `Details: ${q.paymentDetails.trim()}` : '',
|
||||
q.paymentTerms.trim() ? `Terms: ${q.paymentTerms.trim()}` : '',
|
||||
q.incoterm ? `Delivery: ${q.incoterm}` : '',
|
||||
q.payMethods.length ? t('legacyMiscUi.exportQuotePayment', { value: q.payMethods.join(' \u00b7 ') }) : '',
|
||||
q.lightning.trim() ? t('legacyMiscUi.exportQuoteLightning', { value: q.lightning.trim() }) : '',
|
||||
q.paymentDetails.trim() ? t('legacyMiscUi.exportQuoteDetails', { value: q.paymentDetails.trim() }) : '',
|
||||
q.paymentTerms.trim() ? t('legacyMiscUi.exportQuoteTerms', { value: q.paymentTerms.trim() }) : '',
|
||||
q.incoterm ? t('legacyMiscUi.exportQuoteDelivery', { value: q.incoterm }) : '',
|
||||
].filter(Boolean);
|
||||
if (payLines.length) {
|
||||
doc.setFont('helvetica', 'normal'); doc.setFontSize(9); doc.setTextColor(90, 90, 90);
|
||||
|
|
@ -915,8 +919,8 @@ export function DeckCostCalculator() {
|
|||
ensureRoom(30);
|
||||
doc.setFont('helvetica', 'normal'); doc.setFontSize(9); doc.setTextColor(120, 120, 120);
|
||||
const until = new Date(now.getTime() + Math.max(1, q.validDays) * 86_400_000);
|
||||
doc.text(`Does not include shipping. Quote valid until ${until.toLocaleDateString(dateLocaleCode(), { year: 'numeric', month: 'long', day: 'numeric' })}.`, M, y); y += 14;
|
||||
doc.text('Generated with SkateSpots \u00b7 skate-spots.org', M, y);
|
||||
doc.text(t('legacyMiscUi.exportQuoteValidUntil', { date: until.toLocaleDateString(dateLocaleCode(), { year: 'numeric', month: 'long', day: 'numeric' }) }), M, y); y += 14;
|
||||
doc.text(t('legacyMiscUi.exportGeneratedWith'), M, y);
|
||||
|
||||
doc.save(`deck-quote-${new Date().toISOString().slice(0, 10)}.pdf`);
|
||||
};
|
||||
|
|
@ -942,7 +946,7 @@ export function DeckCostCalculator() {
|
|||
|
||||
ctx.fillStyle = '#ff0040'; ctx.fillRect(48, 46, 64, 6);
|
||||
ctx.fillStyle = '#ffffff'; ctx.font = 'bold 34px system-ui, sans-serif';
|
||||
ctx.fillText('Deck cost', 48, 110);
|
||||
ctx.fillText(t('legacyMiscUi.exportPngTitle'), 48, 110);
|
||||
ctx.fillStyle = '#9ca3af'; ctx.font = '16px system-ui, sans-serif';
|
||||
ctx.fillText(`${new Date().toLocaleDateString(dateLocaleCode(), { year: 'numeric', month: 'long', day: 'numeric' })} · ${currencyLabel(cur)}`, 48, 140);
|
||||
if (quote.businessName.trim()) {
|
||||
|
|
@ -956,20 +960,20 @@ export function DeckCostCalculator() {
|
|||
}
|
||||
|
||||
ctx.fillStyle = '#6b7280'; ctx.font = '13px system-ui, sans-serif';
|
||||
ctx.fillText('SELLING PRICE · WITH PACKAGING', 48, 206);
|
||||
ctx.fillText(t('legacyMiscUi.exportPngSellingWith'), 48, 206);
|
||||
ctx.fillStyle = '#ccff00'; ctx.font = 'bold 46px system-ui, sans-serif';
|
||||
ctx.fillText(fmt(r.priceWithPack), 48, 256);
|
||||
ctx.fillStyle = '#6b7280'; ctx.font = '13px system-ui, sans-serif';
|
||||
ctx.fillText('SELLING PRICE · NO PACKAGING', 540, 206);
|
||||
ctx.fillText(t('legacyMiscUi.exportPngSellingNo'), 540, 206);
|
||||
ctx.fillStyle = '#e5e7eb'; ctx.font = 'bold 32px system-ui, sans-serif';
|
||||
ctx.fillText(fmt(r.priceNoPack), 540, 250);
|
||||
|
||||
const rows: [string, number][] = [
|
||||
['Equipment', r.equipment],
|
||||
['Operations', r.operations],
|
||||
['Materials', r.materials],
|
||||
['Processes', r.processes],
|
||||
['Packaging', r.packaging],
|
||||
[t('legacyMiscUi.exportSectionEquipment'), r.equipment],
|
||||
[t('legacyMiscUi.exportLabelOperations'), r.operations],
|
||||
[t('legacyMiscUi.exportSectionMaterials'), r.materials],
|
||||
[t('legacyMiscUi.exportLabelProcesses'), r.processes],
|
||||
[t('legacyMiscUi.exportSectionPackaging'), r.packaging],
|
||||
];
|
||||
let y = 330;
|
||||
ctx.font = '15px system-ui, sans-serif';
|
||||
|
|
@ -980,17 +984,17 @@ export function DeckCostCalculator() {
|
|||
}
|
||||
y += 12;
|
||||
ctx.fillStyle = '#6b7280'; ctx.font = '13px system-ui, sans-serif';
|
||||
ctx.fillText('COST / BOARD (WITH PACKAGING)', 48, y);
|
||||
ctx.fillText(t('legacyMiscUi.exportPngCostWith'), 48, y);
|
||||
ctx.fillStyle = '#ffffff'; ctx.font = 'bold 24px system-ui, sans-serif';
|
||||
ctx.fillText(fmt(r.costWithPack), 48, y + 32);
|
||||
if (quote.depositPct > 0) {
|
||||
ctx.textAlign = 'right';
|
||||
ctx.fillStyle = '#9ca3af'; ctx.font = '15px system-ui, sans-serif';
|
||||
ctx.fillText(`Deposit ${quote.depositPct}%: ${fmt((r.priceWithPack * quote.depositPct) / 100)}`, W - 48, y + 32);
|
||||
ctx.fillText(t('legacyMiscUi.exportPngDeposit', { pct: quote.depositPct, amount: fmt((r.priceWithPack * quote.depositPct) / 100) }), W - 48, y + 32);
|
||||
ctx.textAlign = 'left';
|
||||
}
|
||||
ctx.fillStyle = '#6b7280'; ctx.font = '12px system-ui, sans-serif';
|
||||
ctx.fillText('Generated with SkateSpots · skate-spots.org', 48, H - 40);
|
||||
ctx.fillText(t('legacyMiscUi.exportGeneratedWith'), 48, H - 40);
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = c.toDataURL('image/png');
|
||||
|
|
@ -1004,50 +1008,50 @@ export function DeckCostCalculator() {
|
|||
const wb = XLSX.utils.book_new();
|
||||
const num = (v: number) => Number(v.toFixed(4));
|
||||
const summary: (string | number)[][] = [
|
||||
['SkateSpots — Deck cost calculator'],
|
||||
[t('legacyMiscUi.exportTitle')],
|
||||
[],
|
||||
['Currency', cur],
|
||||
['Date', new Date().toISOString().slice(0, 10)],
|
||||
[t('legacyMiscUi.exportXlsxCurrency'), cur],
|
||||
[t('legacyMiscUi.exportXlsxDate'), new Date().toISOString().slice(0, 10)],
|
||||
[],
|
||||
['Cost / board (no packaging)', num(r.costNoPack)],
|
||||
['Cost / board (with packaging)', num(r.costWithPack)],
|
||||
['Selling price (no packaging)', num(r.priceNoPack)],
|
||||
['Selling price (with packaging)', num(r.priceWithPack)],
|
||||
[t('legacyMiscUi.exportKvCostNoPack'), num(r.costNoPack)],
|
||||
[t('legacyMiscUi.exportKvCostWithPack'), num(r.costWithPack)],
|
||||
[t('legacyMiscUi.exportKvSellingNoPack'), num(r.priceNoPack)],
|
||||
[t('legacyMiscUi.exportKvSellingWithPack'), num(r.priceWithPack)],
|
||||
[],
|
||||
['Equipment per board', num(r.equipment)],
|
||||
['Operations per board', num(r.operations)],
|
||||
['Materials per board', num(r.materials)],
|
||||
['Processes per board', num(r.processes)],
|
||||
['Packaging per board', num(r.packaging)],
|
||||
[t('legacyMiscUi.exportKvEquipmentPerBoard'), num(r.equipment)],
|
||||
[t('legacyMiscUi.exportXlsxOperationsPerBoard'), num(r.operations)],
|
||||
[t('legacyMiscUi.exportKvMaterialsPerBoard'), num(r.materials)],
|
||||
[t('legacyMiscUi.exportKvProcessesPerBoard'), num(r.processes)],
|
||||
[t('legacyMiscUi.exportKvPackagingPerBoard'), num(r.packaging)],
|
||||
];
|
||||
XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(summary), 'Summary');
|
||||
XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(summary), t('legacyMiscUi.exportXlsxSummary'));
|
||||
XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet([
|
||||
['Equipment', 'Cost', 'Setup (h)', 'Boards', 'Per board'],
|
||||
[t('legacyMiscUi.exportSectionEquipment'), t('legacyMiscUi.exportColCost'), t('legacyMiscUi.exportColSetupH'), t('legacyMiscUi.exportColBoards'), t('legacyMiscUi.exportColPerBoard')],
|
||||
...s.equipment.map((e) => [e.name, e.cost, e.setupHrs, e.boards, num(e.boards > 0 ? (e.cost + e.setupHrs * r.laborPerHr) / e.boards : 0)]),
|
||||
]), 'Equipment');
|
||||
]), t('legacyMiscUi.exportSectionEquipment'));
|
||||
XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet([
|
||||
['Material', 'Order cost', 'Shipping', 'Boards', 'Order (min)', 'Unload (min)', 'Per board'],
|
||||
[t('legacyMiscUi.exportColMaterial'), t('legacyMiscUi.exportColOrderCost'), t('legacyMiscUi.exportColShipping'), t('legacyMiscUi.exportColBoards'), t('legacyMiscUi.exportColOrderMin'), t('legacyMiscUi.exportColUnloadMin'), t('legacyMiscUi.exportColPerBoard')],
|
||||
...s.materials.map((m) => [m.name, m.orderCost, m.shipping, m.boards, m.orderMin, m.unloadMin, num(m.boards > 0 ? (m.orderCost + m.shipping + (m.orderMin + m.unloadMin) * r.laborPerMin) / m.boards : 0)]),
|
||||
]), 'Materials');
|
||||
]), t('legacyMiscUi.exportSectionMaterials'));
|
||||
XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet([
|
||||
['Process', 'Time (min)', 'Boards', 'Per board'],
|
||||
[t('legacyMiscUi.exportColProcess'), t('legacyMiscUi.exportColTimeMin'), t('legacyMiscUi.exportColBoards'), t('legacyMiscUi.exportColPerBoard')],
|
||||
...s.processes.map((p) => [p.name, p.minutes, p.boards, p.rate, p.scrapPct, num(p.boards > 0 ? (p.minutes * ((p.rate > 0 ? p.rate : r.laborPerHr) / 60)) / p.boards : 0)]),
|
||||
]), 'Processes');
|
||||
]), t('legacyMiscUi.exportLabelProcesses'));
|
||||
XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet([
|
||||
['Packaging', 'Time/board (min)', 'Materials/board', 'Per board'],
|
||||
[t('legacyMiscUi.exportSectionPackaging'), t('legacyMiscUi.exportColTimeBoardMin'), t('legacyMiscUi.exportColMaterialsBoard'), t('legacyMiscUi.exportColPerBoard')],
|
||||
...s.packaging.map((p) => [p.name, p.minutesPerBoard, p.materialsPerBoard, num(p.materialsPerBoard + p.minutesPerBoard * r.laborPerMin)]),
|
||||
]), 'Packaging');
|
||||
]), t('legacyMiscUi.exportSectionPackaging'));
|
||||
XLSX.writeFile(wb, `deck-cost-${new Date().toISOString().slice(0, 10)}.xlsx`);
|
||||
};
|
||||
|
||||
const copySummary = async () => {
|
||||
const text = [
|
||||
`Deck cost — ${currencyLabel(cur)}`,
|
||||
`Cost / board (with packaging): ${fmt(r.costWithPack)}`,
|
||||
`Selling price (no packaging): ${fmt(r.priceNoPack)}`,
|
||||
`Selling price (with packaging): ${fmt(r.priceWithPack)}`,
|
||||
t('legacyMiscUi.exportCopyTitle', { currency: currencyLabel(cur) }),
|
||||
t('legacyMiscUi.exportCopyCostWith', { amount: fmt(r.costWithPack) }),
|
||||
t('legacyMiscUi.exportCopySellingNo', { amount: fmt(r.priceNoPack) }),
|
||||
t('legacyMiscUi.exportCopySellingWith', { amount: fmt(r.priceWithPack) }),
|
||||
'',
|
||||
`Equipment ${fmt(r.equipment)} · Operations ${fmt(r.operations)} · Materials ${fmt(r.materials)} · Processes ${fmt(r.processes)} · Packaging ${fmt(r.packaging)}`,
|
||||
t('legacyMiscUi.exportCopyBreakdown', { equipment: fmt(r.equipment), operations: fmt(r.operations), materials: fmt(r.materials), processes: fmt(r.processes), packaging: fmt(r.packaging) }),
|
||||
].join('\n');
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
|
|
@ -1068,48 +1072,48 @@ export function DeckCostCalculator() {
|
|||
const exportCsv = () => download('deck-cost.csv', toCsv(calcCsvRows(s)), 'text/csv');
|
||||
const exportMarkdown = () => {
|
||||
const md = [
|
||||
'# Deck cost calculator',
|
||||
`# ${t('legacyMiscUi.exportMarkdownTitle')}`,
|
||||
'',
|
||||
`${new Date().toLocaleDateString(dateLocaleCode(), { year: 'numeric', month: 'long', day: 'numeric' })} · ${currencyLabel(cur)}`,
|
||||
'',
|
||||
'## Labor',
|
||||
`- Hourly rate: ${fmt(s.hourlyRate)}`,
|
||||
`- Labour burden: ${s.overheadFactor}×`,
|
||||
`- Loaded rate: ${fmt(s.hourlyRate * s.overheadFactor)}/hr`,
|
||||
`## ${t('legacyMiscUi.exportSectionLabor')}`,
|
||||
`- ${t('legacyMiscUi.exportMdHourlyRate', { value: fmt(s.hourlyRate) })}`,
|
||||
`- ${t('legacyMiscUi.exportMdLabourBurden', { value: `${s.overheadFactor}×` })}`,
|
||||
`- ${t('legacyMiscUi.exportMdLoadedRate', { value: `${fmt(s.hourlyRate * s.overheadFactor)}/hr` })}`,
|
||||
'',
|
||||
'## Equipment',
|
||||
...s.equipment.map((e) => `- ${e.name}: ${fmt(e.boards > 0 ? (e.cost + e.setupHrs * r.laborPerHr) / e.boards : 0)} (${fmt(e.cost)} · ${e.boards.toLocaleString()} boards)`),
|
||||
`- **Equipment per board: ${fmt(r.equipment)}**`,
|
||||
`## ${t('legacyMiscUi.exportSectionEquipment')}`,
|
||||
...s.equipment.map((e) => `- ${e.name}: ${fmt(e.boards > 0 ? (e.cost + e.setupHrs * r.laborPerHr) / e.boards : 0)} (${fmt(e.cost)} · ${t('legacyMiscUi.exportDetailBoards', { boards: e.boards.toLocaleString() })})`),
|
||||
`- **${t('legacyMiscUi.exportKvEquipmentPerBoard')}: ${fmt(r.equipment)}**`,
|
||||
'',
|
||||
'## Operation expenses',
|
||||
`- Boards/month: ${s.boardsPerMonth}`,
|
||||
`- Rent: ${fmt(s.rent)}`,
|
||||
`- Utilities: ${fmt(s.utilities)}`,
|
||||
`- Subscriptions: ${fmt(s.subscriptions)}`,
|
||||
`- **Per board: ${fmt(r.operations)}**`,
|
||||
`## ${t('legacyMiscUi.exportSectionOperations')}`,
|
||||
`- ${t('legacyMiscUi.exportMdBoardsPerMonth', { value: s.boardsPerMonth })}`,
|
||||
`- ${t('legacyMiscUi.exportMdRent', { value: fmt(s.rent) })}`,
|
||||
`- ${t('legacyMiscUi.exportMdUtilities', { value: fmt(s.utilities) })}`,
|
||||
`- ${t('legacyMiscUi.exportMdSubscriptions', { value: fmt(s.subscriptions) })}`,
|
||||
`- **${t('legacyMiscUi.exportMdPerBoard', { value: fmt(r.operations) })}**`,
|
||||
'',
|
||||
'## Materials',
|
||||
...s.materials.map((m) => `- ${m.name}: ${fmt(m.boards > 0 ? (m.orderCost + m.shipping + (m.orderMin + m.unloadMin) * r.laborPerMin) / m.boards : 0)} (${fmt(m.orderCost)} · ${m.boards.toLocaleString()} boards)`),
|
||||
`- **Materials per board: ${fmt(r.materials)}**`,
|
||||
`## ${t('legacyMiscUi.exportSectionMaterials')}`,
|
||||
...s.materials.map((m) => `- ${m.name}: ${fmt(m.boards > 0 ? (m.orderCost + m.shipping + (m.orderMin + m.unloadMin) * r.laborPerMin) / m.boards : 0)} (${fmt(m.orderCost)} · ${t('legacyMiscUi.exportDetailBoards', { boards: m.boards.toLocaleString() })})`),
|
||||
`- **${t('legacyMiscUi.exportKvMaterialsPerBoard')}: ${fmt(r.materials)}**`,
|
||||
'',
|
||||
'## Process costs',
|
||||
...s.processes.map((p) => `- ${p.name}: ${fmt(p.boards > 0 ? (p.minutes * ((p.rate > 0 ? p.rate : r.laborPerHr) / 60)) / p.boards : 0)} (${p.minutes} min${p.scrapPct ? ` · ${p.scrapPct}% scrap` : ''})`),
|
||||
`- **Processes per board: ${fmt(r.processes)}**`,
|
||||
`## ${t('legacyMiscUi.exportSectionProcesses')}`,
|
||||
...s.processes.map((p) => `- ${p.name}: ${fmt(p.boards > 0 ? (p.minutes * ((p.rate > 0 ? p.rate : r.laborPerHr) / 60)) / p.boards : 0)} (${t('legacyMiscUi.exportDetailMinutes', { minutes: p.minutes })}${p.scrapPct ? ` · ${t('legacyMiscUi.exportDetailScrap', { pct: p.scrapPct })}` : ''})`),
|
||||
`- **${t('legacyMiscUi.exportKvProcessesPerBoard')}: ${fmt(r.processes)}**`,
|
||||
'',
|
||||
'## Packaging',
|
||||
`## ${t('legacyMiscUi.exportSectionPackaging')}`,
|
||||
...s.packaging.map((p) => `- ${p.name}: ${fmt(p.materialsPerBoard + p.minutesPerBoard * r.laborPerMin)}`),
|
||||
`- **Packaging per board: ${fmt(r.packaging)}**`,
|
||||
`- **${t('legacyMiscUi.exportKvPackagingPerBoard')}: ${fmt(r.packaging)}**`,
|
||||
'',
|
||||
'## Result',
|
||||
`- Markup: ${s.profitMargin}× (≈ ${marginPct}% gross margin) · Waste: ${r.effectiveWastePct.toFixed(1)}%`,
|
||||
`- Cost/board (no packaging): ${fmt(r.costNoPackUnit)}`,
|
||||
`- Cost/board (with packaging): ${fmt(r.costWithPackUnit)}`,
|
||||
...(r.batchSetup > 0 ? [`- Batch setup/board: ${fmt(r.setupPerUnit)}`] : []),
|
||||
...(s.taxPct > 0 ? [`- Tax (${s.taxPct}%${s.taxInclusive ? ', included' : ''}): ${fmt(r.orderTax)}`] : []),
|
||||
`- **Selling price (no packaging): ${fmt(r.priceNoPack)}**`,
|
||||
`- **Selling price (with packaging): ${fmt(r.priceWithPack)}**`,
|
||||
`## ${t('legacyMiscUi.exportSectionResult')}`,
|
||||
`- ${t('legacyMiscUi.exportMdMarkupWaste', { markup: s.profitMargin, pct: marginPct, waste: r.effectiveWastePct.toFixed(1) })}`,
|
||||
`- ${t('legacyMiscUi.exportMdCostNoPack', { value: fmt(r.costNoPackUnit) })}`,
|
||||
`- ${t('legacyMiscUi.exportMdCostWithPack', { value: fmt(r.costWithPackUnit) })}`,
|
||||
...(r.batchSetup > 0 ? [`- ${t('legacyMiscUi.exportMdBatchSetup', { value: fmt(r.setupPerUnit) })}`] : []),
|
||||
...(s.taxPct > 0 ? [`- ${s.taxInclusive ? t('legacyMiscUi.exportKvTaxIncluded', { pct: s.taxPct }) : t('legacyMiscUi.exportKvTax', { pct: s.taxPct })}: ${fmt(r.orderTax)}`] : []),
|
||||
`- **${t('legacyMiscUi.exportMdSellingNo', { value: fmt(r.priceNoPack) })}**`,
|
||||
`- **${t('legacyMiscUi.exportMdSellingWith', { value: fmt(r.priceWithPack) })}**`,
|
||||
'',
|
||||
'_Does not include shipping to the buyer. Generated by SkateSpots._',
|
||||
`_${t('legacyMiscUi.exportMdDisclaimer')}_`,
|
||||
].join('\n');
|
||||
download('deck-cost.md', md, 'text/markdown');
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ImageWithFallback } from '@/components/ImageWithFallback';
|
||||
import { useTranslation } from '@/contexts/language';
|
||||
|
||||
export interface LightboxImage {
|
||||
url: string;
|
||||
|
|
@ -33,6 +34,7 @@ interface ImageLightboxProps {
|
|||
* so it reuses ImageWithFallback to degrade gracefully on dead URLs.
|
||||
*/
|
||||
export function ImageLightbox({ images, index, onIndexChange, onClose }: ImageLightboxProps) {
|
||||
const { t } = useTranslation();
|
||||
const open = index !== null && index >= 0 && index < images.length;
|
||||
const current = open ? images[index as number] : undefined;
|
||||
|
||||
|
|
@ -91,7 +93,7 @@ export function ImageLightbox({ images, index, onIndexChange, onClose }: ImageLi
|
|||
type="button"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
aria-label="Previous photo"
|
||||
aria-label={t('legacyMiscUi.ariaPrevPhoto')}
|
||||
onClick={() => go(-1)}
|
||||
className="absolute left-2 top-1/2 h-11 w-11 -translate-y-1/2 rounded-full bg-black/50 text-white hover:bg-black/80 hover:text-white"
|
||||
>
|
||||
|
|
@ -101,7 +103,7 @@ export function ImageLightbox({ images, index, onIndexChange, onClose }: ImageLi
|
|||
type="button"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
aria-label="Next photo"
|
||||
aria-label={t('legacyMiscUi.ariaNextPhoto')}
|
||||
onClick={() => go(1)}
|
||||
className="absolute right-2 top-1/2 h-11 w-11 -translate-y-1/2 rounded-full bg-black/50 text-white hover:bg-black/80 hover:text-white"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { useEffect, useState } from 'react';
|
|||
import { Maximize2 } from 'lucide-react';
|
||||
import { drawCardToCanvas, type CanvasCardData } from '@/lib/riderCardCanvas';
|
||||
import type { ThemeId } from '@/lib/riderCard';
|
||||
import { useTranslation } from '@/contexts/language';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export interface BadgeAward {
|
||||
|
|
@ -90,6 +91,7 @@ function toCanvasData(d: CardData): CanvasCardData {
|
|||
* a canvas + QR render on every keystroke.
|
||||
*/
|
||||
export function CardPreview({ d, className, zoomable = false }: { d: CardData; className?: string; zoomable?: boolean }) {
|
||||
const { t } = useTranslation();
|
||||
const [src, setSrc] = useState<string | null>(null);
|
||||
const [zoom, setZoom] = useState(false);
|
||||
|
||||
|
|
@ -120,7 +122,7 @@ export function CardPreview({ d, className, zoomable = false }: { d: CardData; c
|
|||
aria-label={zoomable ? 'Zoom card' : undefined}
|
||||
>
|
||||
{src
|
||||
? <img src={src} alt="Card preview" className="h-full w-full object-cover" style={{ display: 'block' }} />
|
||||
? <img src={src} alt={t('legacyMiscUi.altCardPreview')} className="h-full w-full object-cover" style={{ display: 'block' }} />
|
||||
: <div className="h-full w-full animate-pulse rounded-2xl bg-white/5" />}
|
||||
{zoomable && src && (
|
||||
<span className="absolute bottom-2 right-2 rounded-full bg-black/60 p-1.5 text-white opacity-0 transition-opacity group-hover:opacity-100">
|
||||
|
|
@ -136,7 +138,7 @@ export function CardPreview({ d, className, zoomable = false }: { d: CardData; c
|
|||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<img src={src} alt="Card" className="max-h-[92vh] w-auto max-w-[95vw] rounded-2xl shadow-2xl" />
|
||||
<img src={src} alt={t('legacyMiscUi.altCard')} className="max-h-[92vh] w-auto max-w-[95vw] rounded-2xl shadow-2xl" />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigge
|
|||
import { MessageSquare, ChevronDown, ChevronRight, MoreHorizontal, Flag } from 'lucide-react';
|
||||
import { formatDistanceToNow } from '@/lib/dateFnsI18n';
|
||||
import { genUserName } from '@/lib/genUserName';
|
||||
import { useTranslation } from '@/contexts/language';
|
||||
|
||||
interface CommentProps {
|
||||
root: NostrEvent | URL;
|
||||
|
|
@ -25,6 +26,7 @@ interface CommentProps {
|
|||
}
|
||||
|
||||
export function Comment({ root, comment, depth = 0, maxDepth = 3, limit }: CommentProps) {
|
||||
const { t } = useTranslation();
|
||||
const [showReplyForm, setShowReplyForm] = useState(false);
|
||||
const [showReplies, setShowReplies] = useState(depth < 2); // Auto-expand first 2 levels
|
||||
const [showReportDialog, setShowReportDialog] = useState(false);
|
||||
|
|
@ -109,7 +111,7 @@ export function Comment({ root, comment, depth = 0, maxDepth = 3, limit }: Comme
|
|||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 px-2 text-xs"
|
||||
aria-label="Comment options"
|
||||
aria-label={t('legacyMiscUi.ariaCommentOptions')}
|
||||
>
|
||||
<MoreHorizontal className="h-3 w-3" />
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -72,6 +72,122 @@ const jaBatch20 = {
|
|||
// Zap errors
|
||||
zapNoActiveMint: '有効なCashuミントが見つかりません。',
|
||||
modHelp: '非表示は org.skatespots.moderation / hidden を発行します。確認は … / verified を、元に戻すは … / cleared(中立に戻す)を発行します。3つとも公開・可逆のラベルです — リレーから何かが削除されることはありません。',
|
||||
// Image alt text + aria labels
|
||||
altProfile: 'プロフィール',
|
||||
altCardPreview: 'カードのプレビュー',
|
||||
altCard: 'カード',
|
||||
altCaptured: '撮影済み',
|
||||
ariaPrevPhoto: '前の写真',
|
||||
ariaNextPhoto: '次の写真',
|
||||
ariaCommentOptions: 'コメントのオプション',
|
||||
ariaMessageConversation: 'メッセージの会話',
|
||||
|
||||
// Deck cost calculator — generated export documents
|
||||
exportTitle: 'SkateSpots — Deck 原価計算',
|
||||
exportQuoteTitle: '見積書',
|
||||
exportPngTitle: 'Deck 原価',
|
||||
exportMarkdownTitle: 'Deck 原価計算',
|
||||
exportSectionLabor: '人件費',
|
||||
exportSectionEquipment: '設備',
|
||||
exportSectionOperations: '運営費',
|
||||
exportSectionMaterials: '材料',
|
||||
exportSectionProcesses: '工程費',
|
||||
exportSectionPackaging: '梱包',
|
||||
exportSectionResult: '結果',
|
||||
exportLabelOperations: '運営',
|
||||
exportLabelProcesses: '工程',
|
||||
exportKvHourlyRate: '時給',
|
||||
exportKvLabourBurden: '労務負荷',
|
||||
exportKvLoadedRate: '込み単価',
|
||||
exportKvEquipmentPerBoard: '1枚あたりの設備費',
|
||||
exportKvBoardsPerMonth: 'ボード / 月',
|
||||
exportKvRentPerMonth: '家賃 / 月',
|
||||
exportKvUtilitiesPerMonth: '光熱費 / 月',
|
||||
exportKvSubscriptionsPerMonth: 'サブスク / 月',
|
||||
exportKvPerBoard: '1枚あたり',
|
||||
exportKvMaterialsPerBoard: '1枚あたりの材料費',
|
||||
exportKvProcessesPerBoard: '1枚あたりの工程費',
|
||||
exportKvPackagingPerBoard: '1枚あたりの梱包費',
|
||||
exportKvMarkup: 'マークアップ',
|
||||
exportMarkupValue: '{{markup}}× (≈ 粗利益率 {{pct}}%)',
|
||||
exportKvWasteEffective: 'ロス (実効)',
|
||||
exportKvCostNoPack: '原価 / 枚 (梱包なし)',
|
||||
exportKvCostWithPack: '原価 / 枚 (梱包あり)',
|
||||
exportKvBatchSetupPerBoard: 'ロット段取り / 枚',
|
||||
exportKvTax: '税 ({{pct}}%)',
|
||||
exportKvTaxIncluded: '税 ({{pct}}%、税込)',
|
||||
exportKvSellingNoPack: '販売価格 (梱包なし)',
|
||||
exportKvSellingWithPack: '販売価格 (梱包あり)',
|
||||
exportShippingNote: '買い手への送料は含まれていません。',
|
||||
exportGeneratedBy: 'SkateSpots が生成 · skate-spots.org',
|
||||
exportGeneratedWith: 'SkateSpots で生成 · skate-spots.org',
|
||||
exportDetailSetup: '+ 段取り {{hours}} 時間',
|
||||
exportDetailBoards: '{{boards}} 枚',
|
||||
exportDetailShip: '+ 送料 {{amount}}',
|
||||
exportDetailMinutes: '{{minutes}} 分',
|
||||
exportDetailPerBoards: '/ {{boards}} 枚',
|
||||
exportDetailScrap: 'ロス {{pct}}%',
|
||||
exportDetailMaterials: '材料 {{materials}}',
|
||||
exportQuoteRef: '整理番号 {{ref}}',
|
||||
exportQuoteLineItem: 'カスタムスケートボード Deck',
|
||||
exportQuoteLineItemQty: 'カスタムスケートボード Deck × {{qty}}',
|
||||
exportQuoteLineDesc: '手作りで仕上げ、梱包済み。数量 1。',
|
||||
exportQuoteLineDescQty: '手作りで仕上げ、梱包済み。各 {{price}}。',
|
||||
exportQuoteSubtotal: '小計',
|
||||
exportQuoteSubtotalQty: '小計 ({{qty}} 枚)',
|
||||
exportQuoteTax: '税 / VAT ({{pct}}%)',
|
||||
exportQuoteTotal: '合計',
|
||||
exportQuoteTotalQty: '合計 ({{qty}} 枚)',
|
||||
exportQuoteIncludesTax: '税 {{pct}}% を含む ({{amount}})',
|
||||
exportQuoteDeposit: '今すぐ支払う手付金 ({{pct}}%)',
|
||||
exportQuoteBalance: '完成時の残金',
|
||||
exportQuotePayment: '支払い: {{value}}',
|
||||
exportQuoteLightning: 'Lightning: {{value}}',
|
||||
exportQuoteDetails: '詳細: {{value}}',
|
||||
exportQuoteTerms: '条件: {{value}}',
|
||||
exportQuoteDelivery: '配送: {{value}}',
|
||||
exportQuoteValidUntil: '送料は含まれていません。見積の有効期限は {{date}} です。',
|
||||
exportPngSellingWith: '販売価格 · 梱包あり',
|
||||
exportPngSellingNo: '販売価格 · 梱包なし',
|
||||
exportPngCostWith: '原価 / 枚 (梱包あり)',
|
||||
exportPngDeposit: '手付金 {{pct}}%: {{amount}}',
|
||||
exportXlsxSummary: '概要',
|
||||
exportXlsxCurrency: '通貨',
|
||||
exportXlsxDate: '日付',
|
||||
exportXlsxOperationsPerBoard: '1枚あたりの運営費',
|
||||
exportColCost: '費用',
|
||||
exportColSetupH: '段取り (h)',
|
||||
exportColBoards: '枚数',
|
||||
exportColPerBoard: '1枚あたり',
|
||||
exportColMaterial: '材料',
|
||||
exportColOrderCost: '発注費',
|
||||
exportColShipping: '送料',
|
||||
exportColOrderMin: '発注 (分)',
|
||||
exportColUnloadMin: '荷下ろし (分)',
|
||||
exportColProcess: '工程',
|
||||
exportColTimeMin: '時間 (分)',
|
||||
exportColTimeBoardMin: '時間/枚 (分)',
|
||||
exportColMaterialsBoard: '材料/枚',
|
||||
exportCopyTitle: 'Deck 原価 — {{currency}}',
|
||||
exportCopyCostWith: '原価 / 枚 (梱包あり): {{amount}}',
|
||||
exportCopySellingNo: '販売価格 (梱包なし): {{amount}}',
|
||||
exportCopySellingWith: '販売価格 (梱包あり): {{amount}}',
|
||||
exportCopyBreakdown: '設備 {{equipment}} · 運営 {{operations}} · 材料 {{materials}} · 工程 {{processes}} · 梱包 {{packaging}}',
|
||||
exportMdHourlyRate: '時給: {{value}}',
|
||||
exportMdLabourBurden: '労務負荷: {{value}}',
|
||||
exportMdLoadedRate: '込み単価: {{value}}',
|
||||
exportMdBoardsPerMonth: 'ボード/月: {{value}}',
|
||||
exportMdRent: '家賃: {{value}}',
|
||||
exportMdUtilities: '光熱費: {{value}}',
|
||||
exportMdSubscriptions: 'サブスク: {{value}}',
|
||||
exportMdPerBoard: '1枚あたり: {{value}}',
|
||||
exportMdMarkupWaste: 'マークアップ: {{markup}}× (≈ 粗利益率 {{pct}}%) · ロス: {{waste}}%',
|
||||
exportMdCostNoPack: '原価/枚 (梱包なし): {{value}}',
|
||||
exportMdCostWithPack: '原価/枚 (梱包あり): {{value}}',
|
||||
exportMdBatchSetup: 'ロット段取り/枚: {{value}}',
|
||||
exportMdSellingNo: '販売価格 (梱包なし): {{value}}',
|
||||
exportMdSellingWith: '販売価格 (梱包あり): {{value}}',
|
||||
exportMdDisclaimer: '買い手への送料は含まれていません。SkateSpots が生成。',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -68,6 +68,123 @@ const legacyMiscUi = {
|
|||
|
||||
// Zap errors
|
||||
zapNoActiveMint: 'No active Cashu mint found.',
|
||||
|
||||
// Image alt text + aria labels
|
||||
altProfile: 'Profile',
|
||||
altCardPreview: 'Card preview',
|
||||
altCard: 'Card',
|
||||
altCaptured: 'Captured',
|
||||
ariaPrevPhoto: 'Previous photo',
|
||||
ariaNextPhoto: 'Next photo',
|
||||
ariaCommentOptions: 'Comment options',
|
||||
ariaMessageConversation: 'Message conversation',
|
||||
|
||||
// Deck cost calculator — generated export documents
|
||||
exportTitle: 'SkateSpots — Deck cost calculator',
|
||||
exportQuoteTitle: 'QUOTE',
|
||||
exportPngTitle: 'Deck cost',
|
||||
exportMarkdownTitle: 'Deck cost calculator',
|
||||
exportSectionLabor: 'Labor',
|
||||
exportSectionEquipment: 'Equipment',
|
||||
exportSectionOperations: 'Operation expenses',
|
||||
exportSectionMaterials: 'Materials',
|
||||
exportSectionProcesses: 'Process costs',
|
||||
exportSectionPackaging: 'Packaging',
|
||||
exportSectionResult: 'Result',
|
||||
exportLabelOperations: 'Operations',
|
||||
exportLabelProcesses: 'Processes',
|
||||
exportKvHourlyRate: 'Hourly rate',
|
||||
exportKvLabourBurden: 'Labour burden',
|
||||
exportKvLoadedRate: 'Loaded rate',
|
||||
exportKvEquipmentPerBoard: 'Equipment per board',
|
||||
exportKvBoardsPerMonth: 'Boards / month',
|
||||
exportKvRentPerMonth: 'Rent / month',
|
||||
exportKvUtilitiesPerMonth: 'Utilities / month',
|
||||
exportKvSubscriptionsPerMonth: 'Subscriptions / month',
|
||||
exportKvPerBoard: 'Per board',
|
||||
exportKvMaterialsPerBoard: 'Materials per board',
|
||||
exportKvProcessesPerBoard: 'Processes per board',
|
||||
exportKvPackagingPerBoard: 'Packaging per board',
|
||||
exportKvMarkup: 'Markup',
|
||||
exportMarkupValue: '{{markup}}× (≈ {{pct}}% gross margin)',
|
||||
exportKvWasteEffective: 'Waste (effective)',
|
||||
exportKvCostNoPack: 'Cost / board (no packaging)',
|
||||
exportKvCostWithPack: 'Cost / board (with packaging)',
|
||||
exportKvBatchSetupPerBoard: 'Batch setup / board',
|
||||
exportKvTax: 'Tax ({{pct}}%)',
|
||||
exportKvTaxIncluded: 'Tax ({{pct}}%, included)',
|
||||
exportKvSellingNoPack: 'Selling price (no packaging)',
|
||||
exportKvSellingWithPack: 'Selling price (with packaging)',
|
||||
exportShippingNote: 'Does not include shipping to the buyer.',
|
||||
exportGeneratedBy: 'Generated by SkateSpots · skate-spots.org',
|
||||
exportGeneratedWith: 'Generated with SkateSpots · skate-spots.org',
|
||||
exportDetailSetup: '+ {{hours}}h setup',
|
||||
exportDetailBoards: '{{boards}} boards',
|
||||
exportDetailShip: '+ {{amount}} ship',
|
||||
exportDetailMinutes: '{{minutes}} min',
|
||||
exportDetailPerBoards: '/ {{boards}} boards',
|
||||
exportDetailScrap: '{{pct}}% scrap',
|
||||
exportDetailMaterials: '{{materials}} materials',
|
||||
exportQuoteRef: 'Ref {{ref}}',
|
||||
exportQuoteLineItem: 'Custom skateboard deck',
|
||||
exportQuoteLineItemQty: 'Custom skateboard deck × {{qty}}',
|
||||
exportQuoteLineDesc: 'Hand-built, finished and packed. Quantity 1.',
|
||||
exportQuoteLineDescQty: 'Hand-built, finished and packed. {{price}} each.',
|
||||
exportQuoteSubtotal: 'Subtotal',
|
||||
exportQuoteSubtotalQty: 'Subtotal ({{qty}} decks)',
|
||||
exportQuoteTax: 'Tax / VAT ({{pct}}%)',
|
||||
exportQuoteTotal: 'Total',
|
||||
exportQuoteTotalQty: 'Total ({{qty}} decks)',
|
||||
exportQuoteIncludesTax: 'Includes {{pct}}% tax ({{amount}})',
|
||||
exportQuoteDeposit: 'Deposit due now ({{pct}}%)',
|
||||
exportQuoteBalance: 'Balance on completion',
|
||||
exportQuotePayment: 'Payment: {{value}}',
|
||||
exportQuoteLightning: 'Lightning: {{value}}',
|
||||
exportQuoteDetails: 'Details: {{value}}',
|
||||
exportQuoteTerms: 'Terms: {{value}}',
|
||||
exportQuoteDelivery: 'Delivery: {{value}}',
|
||||
exportQuoteValidUntil: 'Does not include shipping. Quote valid until {{date}}.',
|
||||
exportPngSellingWith: 'SELLING PRICE · WITH PACKAGING',
|
||||
exportPngSellingNo: 'SELLING PRICE · NO PACKAGING',
|
||||
exportPngCostWith: 'COST / BOARD (WITH PACKAGING)',
|
||||
exportPngDeposit: 'Deposit {{pct}}%: {{amount}}',
|
||||
exportXlsxSummary: 'Summary',
|
||||
exportXlsxCurrency: 'Currency',
|
||||
exportXlsxDate: 'Date',
|
||||
exportXlsxOperationsPerBoard: 'Operations per board',
|
||||
exportColCost: 'Cost',
|
||||
exportColSetupH: 'Setup (h)',
|
||||
exportColBoards: 'Boards',
|
||||
exportColPerBoard: 'Per board',
|
||||
exportColMaterial: 'Material',
|
||||
exportColOrderCost: 'Order cost',
|
||||
exportColShipping: 'Shipping',
|
||||
exportColOrderMin: 'Order (min)',
|
||||
exportColUnloadMin: 'Unload (min)',
|
||||
exportColProcess: 'Process',
|
||||
exportColTimeMin: 'Time (min)',
|
||||
exportColTimeBoardMin: 'Time/board (min)',
|
||||
exportColMaterialsBoard: 'Materials/board',
|
||||
exportCopyTitle: 'Deck cost — {{currency}}',
|
||||
exportCopyCostWith: 'Cost / board (with packaging): {{amount}}',
|
||||
exportCopySellingNo: 'Selling price (no packaging): {{amount}}',
|
||||
exportCopySellingWith: 'Selling price (with packaging): {{amount}}',
|
||||
exportCopyBreakdown: 'Equipment {{equipment}} · Operations {{operations}} · Materials {{materials}} · Processes {{processes}} · Packaging {{packaging}}',
|
||||
exportMdHourlyRate: 'Hourly rate: {{value}}',
|
||||
exportMdLabourBurden: 'Labour burden: {{value}}',
|
||||
exportMdLoadedRate: 'Loaded rate: {{value}}',
|
||||
exportMdBoardsPerMonth: 'Boards/month: {{value}}',
|
||||
exportMdRent: 'Rent: {{value}}',
|
||||
exportMdUtilities: 'Utilities: {{value}}',
|
||||
exportMdSubscriptions: 'Subscriptions: {{value}}',
|
||||
exportMdPerBoard: 'Per board: {{value}}',
|
||||
exportMdMarkupWaste: 'Markup: {{markup}}× (≈ {{pct}}% gross margin) · Waste: {{waste}}%',
|
||||
exportMdCostNoPack: 'Cost/board (no packaging): {{value}}',
|
||||
exportMdCostWithPack: 'Cost/board (with packaging): {{value}}',
|
||||
exportMdBatchSetup: 'Batch setup/board: {{value}}',
|
||||
exportMdSellingNo: 'Selling price (no packaging): {{value}}',
|
||||
exportMdSellingWith: 'Selling price (with packaging): {{value}}',
|
||||
exportMdDisclaimer: 'Does not include shipping to the buyer. Generated by SkateSpots.',
|
||||
},
|
||||
es: {
|
||||
// Hosting dashboard
|
||||
|
|
@ -130,6 +247,123 @@ const legacyMiscUi = {
|
|||
|
||||
// Zap errors
|
||||
zapNoActiveMint: 'No se encontró ninguna casa de moneda Cashu activa.',
|
||||
|
||||
// Image alt text + aria labels
|
||||
altProfile: 'Perfil',
|
||||
altCardPreview: 'Vista previa de la tarjeta',
|
||||
altCard: 'Tarjeta',
|
||||
altCaptured: 'Capturado',
|
||||
ariaPrevPhoto: 'Foto anterior',
|
||||
ariaNextPhoto: 'Foto siguiente',
|
||||
ariaCommentOptions: 'Opciones del comentario',
|
||||
ariaMessageConversation: 'Conversación de mensajes',
|
||||
|
||||
// Deck cost calculator — generated export documents
|
||||
exportTitle: 'SkateSpots — Calculadora de coste de Deck',
|
||||
exportQuoteTitle: 'PRESUPUESTO',
|
||||
exportPngTitle: 'Coste del Deck',
|
||||
exportMarkdownTitle: 'Calculadora de coste de Deck',
|
||||
exportSectionLabor: 'Mano de obra',
|
||||
exportSectionEquipment: 'Equipamiento',
|
||||
exportSectionOperations: 'Gastos de operación',
|
||||
exportSectionMaterials: 'Materiales',
|
||||
exportSectionProcesses: 'Costes de proceso',
|
||||
exportSectionPackaging: 'Embalaje',
|
||||
exportSectionResult: 'Resultado',
|
||||
exportLabelOperations: 'Operaciones',
|
||||
exportLabelProcesses: 'Procesos',
|
||||
exportKvHourlyRate: 'Tarifa por hora',
|
||||
exportKvLabourBurden: 'Carga laboral',
|
||||
exportKvLoadedRate: 'Tarifa cargada',
|
||||
exportKvEquipmentPerBoard: 'Equipamiento por tabla',
|
||||
exportKvBoardsPerMonth: 'Tablas / mes',
|
||||
exportKvRentPerMonth: 'Alquiler / mes',
|
||||
exportKvUtilitiesPerMonth: 'Suministros / mes',
|
||||
exportKvSubscriptionsPerMonth: 'Suscripciones / mes',
|
||||
exportKvPerBoard: 'Por tabla',
|
||||
exportKvMaterialsPerBoard: 'Materiales por tabla',
|
||||
exportKvProcessesPerBoard: 'Procesos por tabla',
|
||||
exportKvPackagingPerBoard: 'Embalaje por tabla',
|
||||
exportKvMarkup: 'Margen',
|
||||
exportMarkupValue: '{{markup}}× (≈ {{pct}}% de margen bruto)',
|
||||
exportKvWasteEffective: 'Desperdicio (efectivo)',
|
||||
exportKvCostNoPack: 'Coste / tabla (sin embalaje)',
|
||||
exportKvCostWithPack: 'Coste / tabla (con embalaje)',
|
||||
exportKvBatchSetupPerBoard: 'Preparación de lote / tabla',
|
||||
exportKvTax: 'Impuesto ({{pct}}%)',
|
||||
exportKvTaxIncluded: 'Impuesto ({{pct}}%, incluido)',
|
||||
exportKvSellingNoPack: 'Precio de venta (sin embalaje)',
|
||||
exportKvSellingWithPack: 'Precio de venta (con embalaje)',
|
||||
exportShippingNote: 'No incluye el envío al comprador.',
|
||||
exportGeneratedBy: 'Generado por SkateSpots · skate-spots.org',
|
||||
exportGeneratedWith: 'Generado con SkateSpots · skate-spots.org',
|
||||
exportDetailSetup: '+ {{hours}} h de preparación',
|
||||
exportDetailBoards: '{{boards}} tablas',
|
||||
exportDetailShip: '+ {{amount}} de envío',
|
||||
exportDetailMinutes: '{{minutes}} min',
|
||||
exportDetailPerBoards: '/ {{boards}} tablas',
|
||||
exportDetailScrap: '{{pct}}% de desperdicio',
|
||||
exportDetailMaterials: '{{materials}} de materiales',
|
||||
exportQuoteRef: 'Ref. {{ref}}',
|
||||
exportQuoteLineItem: 'Deck de skateboard personalizado',
|
||||
exportQuoteLineItemQty: 'Deck de skateboard personalizado × {{qty}}',
|
||||
exportQuoteLineDesc: 'Fabricado a mano, acabado y embalado. Cantidad 1.',
|
||||
exportQuoteLineDescQty: 'Fabricado a mano, acabado y embalado. {{price}} cada uno.',
|
||||
exportQuoteSubtotal: 'Subtotal',
|
||||
exportQuoteSubtotalQty: 'Subtotal ({{qty}} tablas)',
|
||||
exportQuoteTax: 'Impuesto / IVA ({{pct}}%)',
|
||||
exportQuoteTotal: 'Total',
|
||||
exportQuoteTotalQty: 'Total ({{qty}} tablas)',
|
||||
exportQuoteIncludesTax: 'Incluye {{pct}}% de impuesto ({{amount}})',
|
||||
exportQuoteDeposit: 'Señal a pagar ahora ({{pct}}%)',
|
||||
exportQuoteBalance: 'Saldo al finalizar',
|
||||
exportQuotePayment: 'Pago: {{value}}',
|
||||
exportQuoteLightning: 'Lightning: {{value}}',
|
||||
exportQuoteDetails: 'Datos: {{value}}',
|
||||
exportQuoteTerms: 'Condiciones: {{value}}',
|
||||
exportQuoteDelivery: 'Entrega: {{value}}',
|
||||
exportQuoteValidUntil: 'No incluye el envío. Presupuesto válido hasta el {{date}}.',
|
||||
exportPngSellingWith: 'PRECIO DE VENTA · CON EMBALAJE',
|
||||
exportPngSellingNo: 'PRECIO DE VENTA · SIN EMBALAJE',
|
||||
exportPngCostWith: 'COSTE / TABLA (CON EMBALAJE)',
|
||||
exportPngDeposit: 'Señal {{pct}}%: {{amount}}',
|
||||
exportXlsxSummary: 'Resumen',
|
||||
exportXlsxCurrency: 'Moneda',
|
||||
exportXlsxDate: 'Fecha',
|
||||
exportXlsxOperationsPerBoard: 'Operaciones por tabla',
|
||||
exportColCost: 'Coste',
|
||||
exportColSetupH: 'Preparación (h)',
|
||||
exportColBoards: 'Tablas',
|
||||
exportColPerBoard: 'Por tabla',
|
||||
exportColMaterial: 'Material',
|
||||
exportColOrderCost: 'Coste del pedido',
|
||||
exportColShipping: 'Envío',
|
||||
exportColOrderMin: 'Pedido (min)',
|
||||
exportColUnloadMin: 'Descarga (min)',
|
||||
exportColProcess: 'Proceso',
|
||||
exportColTimeMin: 'Tiempo (min)',
|
||||
exportColTimeBoardMin: 'Tiempo/tabla (min)',
|
||||
exportColMaterialsBoard: 'Materiales/tabla',
|
||||
exportCopyTitle: 'Coste del Deck — {{currency}}',
|
||||
exportCopyCostWith: 'Coste / tabla (con embalaje): {{amount}}',
|
||||
exportCopySellingNo: 'Precio de venta (sin embalaje): {{amount}}',
|
||||
exportCopySellingWith: 'Precio de venta (con embalaje): {{amount}}',
|
||||
exportCopyBreakdown: 'Equipamiento {{equipment}} · Operaciones {{operations}} · Materiales {{materials}} · Procesos {{processes}} · Embalaje {{packaging}}',
|
||||
exportMdHourlyRate: 'Tarifa por hora: {{value}}',
|
||||
exportMdLabourBurden: 'Carga laboral: {{value}}',
|
||||
exportMdLoadedRate: 'Tarifa cargada: {{value}}',
|
||||
exportMdBoardsPerMonth: 'Tablas/mes: {{value}}',
|
||||
exportMdRent: 'Alquiler: {{value}}',
|
||||
exportMdUtilities: 'Suministros: {{value}}',
|
||||
exportMdSubscriptions: 'Suscripciones: {{value}}',
|
||||
exportMdPerBoard: 'Por tabla: {{value}}',
|
||||
exportMdMarkupWaste: 'Margen: {{markup}}× (≈ {{pct}}% de margen bruto) · Desperdicio: {{waste}}%',
|
||||
exportMdCostNoPack: 'Coste/tabla (sin embalaje): {{value}}',
|
||||
exportMdCostWithPack: 'Coste/tabla (con embalaje): {{value}}',
|
||||
exportMdBatchSetup: 'Preparación de lote/tabla: {{value}}',
|
||||
exportMdSellingNo: 'Precio de venta (sin embalaje): {{value}}',
|
||||
exportMdSellingWith: 'Precio de venta (con embalaje): {{value}}',
|
||||
exportMdDisclaimer: 'No incluye el envío al comprador. Generado por SkateSpots.',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -71,6 +71,122 @@ const thBatch19 = {
|
|||
// Zap errors
|
||||
zapNoActiveMint: 'ไม่พบ Cashu mint ที่ใช้งานอยู่',
|
||||
modHelp: 'ซ่อนจะเผยแพร่ org.skatespots.moderation / hidden; ยืนยันจะเผยแพร่ … / verified; เลิกทำจะเผยแพร่ … / cleared (กลับเป็นกลาง) ทั้งสามเป็นเลเบลสาธารณะที่ย้อนกลับได้ — ไม่มีอะไรถูกลบออกจาก relay',
|
||||
// Image alt text + aria labels
|
||||
altProfile: 'โปรไฟล์',
|
||||
altCardPreview: 'ตัวอย่างการ์ด',
|
||||
altCard: 'การ์ด',
|
||||
altCaptured: 'ถ่ายแล้ว',
|
||||
ariaPrevPhoto: 'รูปก่อนหน้า',
|
||||
ariaNextPhoto: 'รูปถัดไป',
|
||||
ariaCommentOptions: 'ตัวเลือกความคิดเห็น',
|
||||
ariaMessageConversation: 'บทสนทนาข้อความ',
|
||||
|
||||
// Deck cost calculator — generated export documents
|
||||
exportTitle: 'SkateSpots — เครื่องคำนวณต้นทุน Deck',
|
||||
exportQuoteTitle: 'ใบเสนอราคา',
|
||||
exportPngTitle: 'ต้นทุน Deck',
|
||||
exportMarkdownTitle: 'เครื่องคำนวณต้นทุน Deck',
|
||||
exportSectionLabor: 'ค่าแรง',
|
||||
exportSectionEquipment: 'อุปกรณ์',
|
||||
exportSectionOperations: 'ค่าใช้จ่ายในการดำเนินงาน',
|
||||
exportSectionMaterials: 'วัสดุ',
|
||||
exportSectionProcesses: 'ต้นทุนกระบวนการ',
|
||||
exportSectionPackaging: 'บรรจุภัณฑ์',
|
||||
exportSectionResult: 'ผลลัพธ์',
|
||||
exportLabelOperations: 'การดำเนินงาน',
|
||||
exportLabelProcesses: 'กระบวนการ',
|
||||
exportKvHourlyRate: 'อัตราต่อชั่วโมง',
|
||||
exportKvLabourBurden: 'ภาระค่าแรง',
|
||||
exportKvLoadedRate: 'อัตรารวมภาระ',
|
||||
exportKvEquipmentPerBoard: 'อุปกรณ์ต่อบอร์ด',
|
||||
exportKvBoardsPerMonth: 'บอร์ด / เดือน',
|
||||
exportKvRentPerMonth: 'ค่าเช่า / เดือน',
|
||||
exportKvUtilitiesPerMonth: 'ค่าสาธารณูปโภค / เดือน',
|
||||
exportKvSubscriptionsPerMonth: 'ค่าสมาชิก / เดือน',
|
||||
exportKvPerBoard: 'ต่อบอร์ด',
|
||||
exportKvMaterialsPerBoard: 'วัสดุต่อบอร์ด',
|
||||
exportKvProcessesPerBoard: 'กระบวนการต่อบอร์ด',
|
||||
exportKvPackagingPerBoard: 'บรรจุภัณฑ์ต่อบอร์ด',
|
||||
exportKvMarkup: 'กำไรที่บวกเพิ่ม',
|
||||
exportMarkupValue: '{{markup}}× (≈ {{pct}}% กำไรขั้นต้น)',
|
||||
exportKvWasteEffective: 'ของเสีย (สุทธิ)',
|
||||
exportKvCostNoPack: 'ต้นทุน / บอร์ด (ไม่รวมบรรจุภัณฑ์)',
|
||||
exportKvCostWithPack: 'ต้นทุน / บอร์ด (รวมบรรจุภัณฑ์)',
|
||||
exportKvBatchSetupPerBoard: 'ตั้งเครื่องต่อล็อต / บอร์ด',
|
||||
exportKvTax: 'ภาษี ({{pct}}%)',
|
||||
exportKvTaxIncluded: 'ภาษี ({{pct}}%, รวมแล้ว)',
|
||||
exportKvSellingNoPack: 'ราคาขาย (ไม่รวมบรรจุภัณฑ์)',
|
||||
exportKvSellingWithPack: 'ราคาขาย (รวมบรรจุภัณฑ์)',
|
||||
exportShippingNote: 'ไม่รวมค่าจัดส่งถึงผู้ซื้อ',
|
||||
exportGeneratedBy: 'สร้างโดย SkateSpots · skate-spots.org',
|
||||
exportGeneratedWith: 'สร้างด้วย SkateSpots · skate-spots.org',
|
||||
exportDetailSetup: '+ ตั้งเครื่อง {{hours}} ชม.',
|
||||
exportDetailBoards: '{{boards}} บอร์ด',
|
||||
exportDetailShip: '+ ค่าจัดส่ง {{amount}}',
|
||||
exportDetailMinutes: '{{minutes}} นาที',
|
||||
exportDetailPerBoards: '/ {{boards}} บอร์ด',
|
||||
exportDetailScrap: 'ของเสีย {{pct}}%',
|
||||
exportDetailMaterials: 'วัสดุ {{materials}}',
|
||||
exportQuoteRef: 'อ้างอิง {{ref}}',
|
||||
exportQuoteLineItem: 'Deck สเก็ตบอร์ดสั่งทำ',
|
||||
exportQuoteLineItemQty: 'Deck สเก็ตบอร์ดสั่งทำ × {{qty}}',
|
||||
exportQuoteLineDesc: 'ทำมือ ขัดแต่ง และบรรจุพร้อมส่ง จำนวน 1',
|
||||
exportQuoteLineDescQty: 'ทำมือ ขัดแต่ง และบรรจุพร้อมส่ง ชิ้นละ {{price}}',
|
||||
exportQuoteSubtotal: 'ยอดรวมย่อย',
|
||||
exportQuoteSubtotalQty: 'ยอดรวมย่อย ({{qty}} บอร์ด)',
|
||||
exportQuoteTax: 'ภาษี / VAT ({{pct}}%)',
|
||||
exportQuoteTotal: 'ยอดรวม',
|
||||
exportQuoteTotalQty: 'ยอดรวม ({{qty}} บอร์ด)',
|
||||
exportQuoteIncludesTax: 'รวมภาษี {{pct}}% ({{amount}})',
|
||||
exportQuoteDeposit: 'มัดจำจ่ายตอนนี้ ({{pct}}%)',
|
||||
exportQuoteBalance: 'ยอดคงเหลือเมื่อเสร็จ',
|
||||
exportQuotePayment: 'ชำระเงิน: {{value}}',
|
||||
exportQuoteLightning: 'Lightning: {{value}}',
|
||||
exportQuoteDetails: 'รายละเอียด: {{value}}',
|
||||
exportQuoteTerms: 'เงื่อนไข: {{value}}',
|
||||
exportQuoteDelivery: 'การจัดส่ง: {{value}}',
|
||||
exportQuoteValidUntil: 'ไม่รวมค่าจัดส่ง ใบเสนอราคาใช้ได้ถึง {{date}}',
|
||||
exportPngSellingWith: 'ราคาขาย · รวมบรรจุภัณฑ์',
|
||||
exportPngSellingNo: 'ราคาขาย · ไม่รวมบรรจุภัณฑ์',
|
||||
exportPngCostWith: 'ต้นทุน / บอร์ด (รวมบรรจุภัณฑ์)',
|
||||
exportPngDeposit: 'มัดจำ {{pct}}%: {{amount}}',
|
||||
exportXlsxSummary: 'สรุป',
|
||||
exportXlsxCurrency: 'สกุลเงิน',
|
||||
exportXlsxDate: 'วันที่',
|
||||
exportXlsxOperationsPerBoard: 'การดำเนินงานต่อบอร์ด',
|
||||
exportColCost: 'ต้นทุน',
|
||||
exportColSetupH: 'ตั้งเครื่อง (ชม.)',
|
||||
exportColBoards: 'บอร์ด',
|
||||
exportColPerBoard: 'ต่อบอร์ด',
|
||||
exportColMaterial: 'วัสดุ',
|
||||
exportColOrderCost: 'ต้นทุนการสั่งซื้อ',
|
||||
exportColShipping: 'ค่าจัดส่ง',
|
||||
exportColOrderMin: 'สั่งซื้อ (นาที)',
|
||||
exportColUnloadMin: 'ขนลง (นาที)',
|
||||
exportColProcess: 'กระบวนการ',
|
||||
exportColTimeMin: 'เวลา (นาที)',
|
||||
exportColTimeBoardMin: 'เวลา/บอร์ด (นาที)',
|
||||
exportColMaterialsBoard: 'วัสดุ/บอร์ด',
|
||||
exportCopyTitle: 'ต้นทุน Deck — {{currency}}',
|
||||
exportCopyCostWith: 'ต้นทุน / บอร์ด (รวมบรรจุภัณฑ์): {{amount}}',
|
||||
exportCopySellingNo: 'ราคาขาย (ไม่รวมบรรจุภัณฑ์): {{amount}}',
|
||||
exportCopySellingWith: 'ราคาขาย (รวมบรรจุภัณฑ์): {{amount}}',
|
||||
exportCopyBreakdown: 'อุปกรณ์ {{equipment}} · การดำเนินงาน {{operations}} · วัสดุ {{materials}} · กระบวนการ {{processes}} · บรรจุภัณฑ์ {{packaging}}',
|
||||
exportMdHourlyRate: 'อัตราต่อชั่วโมง: {{value}}',
|
||||
exportMdLabourBurden: 'ภาระค่าแรง: {{value}}',
|
||||
exportMdLoadedRate: 'อัตรารวมภาระ: {{value}}',
|
||||
exportMdBoardsPerMonth: 'บอร์ด/เดือน: {{value}}',
|
||||
exportMdRent: 'ค่าเช่า: {{value}}',
|
||||
exportMdUtilities: 'ค่าสาธารณูปโภค: {{value}}',
|
||||
exportMdSubscriptions: 'ค่าสมาชิก: {{value}}',
|
||||
exportMdPerBoard: 'ต่อบอร์ด: {{value}}',
|
||||
exportMdMarkupWaste: 'กำไรที่บวกเพิ่ม: {{markup}}× (≈ {{pct}}% กำไรขั้นต้น) · ของเสีย: {{waste}}%',
|
||||
exportMdCostNoPack: 'ต้นทุน/บอร์ด (ไม่รวมบรรจุภัณฑ์): {{value}}',
|
||||
exportMdCostWithPack: 'ต้นทุน/บอร์ด (รวมบรรจุภัณฑ์): {{value}}',
|
||||
exportMdBatchSetup: 'ตั้งเครื่องต่อล็อต/บอร์ด: {{value}}',
|
||||
exportMdSellingNo: 'ราคาขาย (ไม่รวมบรรจุภัณฑ์): {{value}}',
|
||||
exportMdSellingWith: 'ราคาขาย (รวมบรรจุภัณฑ์): {{value}}',
|
||||
exportMdDisclaimer: 'ไม่รวมค่าจัดส่งถึงผู้ซื้อ สร้างโดย SkateSpots',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -667,7 +667,7 @@ export default function BecomeBuilderPage() {
|
|||
<Label>{t('labUi.builderForm.logoPhoto')}</Label>
|
||||
{image ? (
|
||||
<div className="relative w-32 h-32 rounded-full overflow-hidden bg-gray-800">
|
||||
<img src={image} alt="Profile" className="w-full h-full object-cover" />
|
||||
<img src={image} alt={t('legacyMiscUi.altProfile')} className="w-full h-full object-cover" />
|
||||
<button type="button" onClick={() => setImage(null)} className="absolute top-2 right-2 p-1 rounded-full bg-black/80 hover:bg-red-500">
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -971,7 +971,7 @@ export default function BecomeCoachPage() {
|
|||
<Label>{t('labUi.become.profilePhoto')}</Label>
|
||||
{image ? (
|
||||
<div className="relative w-32 h-32 rounded-full overflow-hidden bg-gray-800">
|
||||
<img src={image} alt="Profile" className="w-full h-full object-cover" />
|
||||
<img src={image} alt={t('legacyMiscUi.altProfile')} className="w-full h-full object-cover" />
|
||||
<button type="button" onClick={() => setImage(null)} className="absolute top-2 right-2 p-1 rounded-full bg-black/80 hover:bg-red-500">
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -631,7 +631,7 @@ export default function BecomeFilmerPage() {
|
|||
<Label>{t('labUi.become.profilePhoto')}</Label>
|
||||
{image ? (
|
||||
<div className="relative w-32 h-32 rounded-full overflow-hidden bg-gray-800">
|
||||
<img src={image} alt="Profile" className="w-full h-full object-cover" />
|
||||
<img src={image} alt={t('legacyMiscUi.altProfile')} className="w-full h-full object-cover" />
|
||||
<button type="button" onClick={() => setImage(null)} className="absolute top-2 right-2 p-1 rounded-full bg-black/80 hover:bg-red-500">
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -696,7 +696,7 @@ export default function BecomePhotographerPage() {
|
|||
<Label>{t('labUi.become.profilePhoto')}</Label>
|
||||
{image ? (
|
||||
<div className="relative w-32 h-32 rounded-full overflow-hidden bg-gray-800">
|
||||
<img src={image} alt="Profile" className="w-full h-full object-cover" />
|
||||
<img src={image} alt={t('legacyMiscUi.altProfile')} className="w-full h-full object-cover" />
|
||||
<button type="button" onClick={() => setImage(null)} className="absolute top-2 right-2 p-1 rounded-full bg-black/80 hover:bg-red-500">
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -345,7 +345,7 @@ export default function BecomeSkaterPage() {
|
|||
<div className="flex items-center gap-4">
|
||||
<div className="relative w-28 h-28 rounded-2xl overflow-hidden bg-gray-800 flex-shrink-0 ring-2 ring-white/10">
|
||||
{avatar ? (
|
||||
<img src={avatar} alt="Profile" className="w-full h-full object-cover" />
|
||||
<img src={avatar} alt={t('legacyMiscUi.altProfile')} className="w-full h-full object-cover" />
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center bg-[#ff0040]/15 text-[#ff0040] text-2xl font-black">
|
||||
{displayName.slice(0, 2).toUpperCase()}
|
||||
|
|
|
|||
|
|
@ -403,7 +403,7 @@ export default function ConversationPage() {
|
|||
className="flex-1 overflow-y-auto py-3 sm:py-4 space-y-2 sm:space-y-3 min-h-0"
|
||||
role="log"
|
||||
aria-live="polite"
|
||||
aria-label="Message conversation"
|
||||
aria-label={t('legacyMiscUi.ariaMessageConversation')}
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
|
|
|
|||
Loading…
Reference in a new issue