feat(34-06): wire useTranslation into 10 remaining components
- ThreadTabs: tab labels (gear, planning, setups) via collection namespace - PlanningView: section title, tab labels, empty state steps, CTAs via threads namespace - TotalsBar: 'Sign in' link via common.auth.signIn - ThreadCard: resolved badge and candidate count (plural) via threads namespace - PublicSetupCard: by/anonymous and item count (plural) via setups namespace - SetupImpactSelector: compare dropdown placeholder via setups.impact.compareWith - ClassificationBadge: base/worn/consumable labels via collection.classificationBadge - ImpactDeltaBadge: add mode label via setups.impact.adding - ImageUpload: click-to-add, error messages via common.imageUpload - DashboardCard: skipped (renders props only, no hardcoded UI strings) - Add card, planning keys to en/de threads.json - Add classificationBadge, tabs, totals keys to en/de collection.json - Add card.by, card.anonymous, impact.compareWith to en/de setups.json - Add imageUpload keys to en/de common.json - Build passes, all 19 i18n parity tests pass
This commit is contained in:
@@ -1,8 +1,4 @@
|
|||||||
const CLASSIFICATION_LABELS: Record<string, string> = {
|
import { useTranslation } from "react-i18next";
|
||||||
base: "Base Weight",
|
|
||||||
worn: "Worn",
|
|
||||||
consumable: "Consumable",
|
|
||||||
};
|
|
||||||
|
|
||||||
interface ClassificationBadgeProps {
|
interface ClassificationBadgeProps {
|
||||||
classification: string;
|
classification: string;
|
||||||
@@ -13,7 +9,10 @@ export function ClassificationBadge({
|
|||||||
classification,
|
classification,
|
||||||
onCycle,
|
onCycle,
|
||||||
}: ClassificationBadgeProps) {
|
}: ClassificationBadgeProps) {
|
||||||
const label = CLASSIFICATION_LABELS[classification] ?? "Base Weight";
|
const { t } = useTranslation("collection");
|
||||||
|
const label = t(`classificationBadge.${classification}`, {
|
||||||
|
defaultValue: t("classificationBadge.base"),
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { apiUpload } from "../lib/api";
|
import { apiUpload } from "../lib/api";
|
||||||
import { GearImage, imageContainerBg } from "./GearImage";
|
import { GearImage, imageContainerBg } from "./GearImage";
|
||||||
import { ImageCropEditor } from "./ImageCropEditor";
|
import { ImageCropEditor } from "./ImageCropEditor";
|
||||||
@@ -21,6 +22,7 @@ export function ImageUpload({
|
|||||||
onChange,
|
onChange,
|
||||||
onCropChange,
|
onCropChange,
|
||||||
}: ImageUploadProps) {
|
}: ImageUploadProps) {
|
||||||
|
const { t } = useTranslation("common");
|
||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [localPreview, setLocalPreview] = useState<string | null>(null);
|
const [localPreview, setLocalPreview] = useState<string | null>(null);
|
||||||
@@ -39,12 +41,12 @@ export function ImageUpload({
|
|||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
if (!ACCEPTED_TYPES.includes(file.type)) {
|
if (!ACCEPTED_TYPES.includes(file.type)) {
|
||||||
setError("Please select a JPG, PNG, or WebP image.");
|
setError(t("imageUpload.invalidType"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file.size > MAX_SIZE_BYTES) {
|
if (file.size > MAX_SIZE_BYTES) {
|
||||||
setError("Image must be under 5MB.");
|
setError(t("imageUpload.tooLarge"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +65,7 @@ export function ImageUpload({
|
|||||||
setShowCropEditor(true);
|
setShowCropEditor(true);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
setError("Upload failed. Please try again.");
|
setError(t("imageUpload.uploadFailed"));
|
||||||
setLocalPreview(null);
|
setLocalPreview(null);
|
||||||
} finally {
|
} finally {
|
||||||
setUploading(false);
|
setUploading(false);
|
||||||
@@ -183,7 +185,7 @@ export function ImageUpload({
|
|||||||
<path d="M12.5 5.5h3" />
|
<path d="M12.5 5.5h3" />
|
||||||
</svg>
|
</svg>
|
||||||
<span className="mt-2 text-sm text-gray-400 group-hover:text-gray-500 transition-colors">
|
<span className="mt-2 text-sm text-gray-400 group-hover:text-gray-500 transition-colors">
|
||||||
Click to add photo
|
{t("imageUpload.clickToAdd")}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import type { CandidateDelta } from "../hooks/useImpactDeltas";
|
import type { CandidateDelta } from "../hooks/useImpactDeltas";
|
||||||
|
|
||||||
interface ImpactDeltaBadgeProps {
|
interface ImpactDeltaBadgeProps {
|
||||||
@@ -11,6 +12,8 @@ export function ImpactDeltaBadge({
|
|||||||
type,
|
type,
|
||||||
formatFn,
|
formatFn,
|
||||||
}: ImpactDeltaBadgeProps) {
|
}: ImpactDeltaBadgeProps) {
|
||||||
|
const { t } = useTranslation("setups");
|
||||||
|
|
||||||
if (!delta || delta.mode === "none") return null;
|
if (!delta || delta.mode === "none") return null;
|
||||||
|
|
||||||
const value = type === "weight" ? delta.weightDelta : delta.priceDelta;
|
const value = type === "weight" ? delta.weightDelta : delta.priceDelta;
|
||||||
@@ -28,7 +31,7 @@ export function ImpactDeltaBadge({
|
|||||||
<span className="text-xs text-green-600">
|
<span className="text-xs text-green-600">
|
||||||
+{formatFn(value)}
|
+{formatFn(value)}
|
||||||
{delta.mode === "add" && (
|
{delta.mode === "add" && (
|
||||||
<span className="ml-0.5 text-green-500">(add)</span>
|
<span className="ml-0.5 text-green-500">({t("impact.adding")})</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useCategories } from "../hooks/useCategories";
|
import { useCategories } from "../hooks/useCategories";
|
||||||
import { useThreads } from "../hooks/useThreads";
|
import { useThreads } from "../hooks/useThreads";
|
||||||
import { useUIStore } from "../stores/uiStore";
|
import { useUIStore } from "../stores/uiStore";
|
||||||
@@ -7,6 +8,7 @@ import { CreateThreadModal } from "./CreateThreadModal";
|
|||||||
import { ThreadCard } from "./ThreadCard";
|
import { ThreadCard } from "./ThreadCard";
|
||||||
|
|
||||||
export function PlanningView() {
|
export function PlanningView() {
|
||||||
|
const { t } = useTranslation(["threads", "common"]);
|
||||||
const [activeTab, setActiveTab] = useState<"active" | "resolved">("active");
|
const [activeTab, setActiveTab] = useState<"active" | "resolved">("active");
|
||||||
const [categoryFilter, setCategoryFilter] = useState<number | null>(null);
|
const [categoryFilter, setCategoryFilter] = useState<number | null>(null);
|
||||||
|
|
||||||
@@ -41,7 +43,7 @@ export function PlanningView() {
|
|||||||
{/* Header row */}
|
{/* Header row */}
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<h2 className="text-lg font-semibold text-gray-900">
|
<h2 className="text-lg font-semibold text-gray-900">
|
||||||
Planning Threads
|
{t("threads:planning.title")}
|
||||||
</h2>
|
</h2>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -62,7 +64,7 @@ export function PlanningView() {
|
|||||||
d="M12 4v16m8-8H4"
|
d="M12 4v16m8-8H4"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
New Thread
|
{t("threads:create.title")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -79,7 +81,7 @@ export function PlanningView() {
|
|||||||
: "text-gray-600 hover:bg-gray-200"
|
: "text-gray-600 hover:bg-gray-200"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Active
|
{t("threads:status.active")}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -90,7 +92,7 @@ export function PlanningView() {
|
|||||||
: "text-gray-600 hover:bg-gray-200"
|
: "text-gray-600 hover:bg-gray-200"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Resolved
|
{t("threads:status.resolved")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -107,7 +109,7 @@ export function PlanningView() {
|
|||||||
<div className="py-16">
|
<div className="py-16">
|
||||||
<div className="max-w-lg mx-auto text-center">
|
<div className="max-w-lg mx-auto text-center">
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-8">
|
<h2 className="text-xl font-semibold text-gray-900 mb-8">
|
||||||
Plan your next purchase
|
{t("threads:planning.emptyTitle")}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="space-y-6 text-left mb-10">
|
<div className="space-y-6 text-left mb-10">
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
@@ -115,9 +117,9 @@ export function PlanningView() {
|
|||||||
1
|
1
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-gray-900">Create a thread</p>
|
<p className="font-medium text-gray-900">{t("threads:planning.step1Title")}</p>
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
Start a research thread for gear you're considering
|
{t("threads:planning.step1Description")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -126,9 +128,9 @@ export function PlanningView() {
|
|||||||
2
|
2
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-gray-900">Add candidates</p>
|
<p className="font-medium text-gray-900">{t("threads:planning.step2Title")}</p>
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
Add products you're comparing with prices and weights
|
{t("threads:planning.step2Description")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -137,9 +139,9 @@ export function PlanningView() {
|
|||||||
3
|
3
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-gray-900">Pick a winner</p>
|
<p className="font-medium text-gray-900">{t("threads:planning.step3Title")}</p>
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
Resolve the thread and the winner joins your collection
|
{t("threads:planning.step3Description")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -163,13 +165,13 @@ export function PlanningView() {
|
|||||||
d="M12 4v16m8-8H4"
|
d="M12 4v16m8-8H4"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
Create your first thread
|
{t("threads:planning.createFirst")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : filteredThreads.length === 0 ? (
|
) : filteredThreads.length === 0 ? (
|
||||||
<div className="py-12 text-center">
|
<div className="py-12 text-center">
|
||||||
<p className="text-sm text-gray-500">No threads found</p>
|
<p className="text-sm text-gray-500">{t("threads:empty.noThreads")}</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Link } from "@tanstack/react-router";
|
import { Link } from "@tanstack/react-router";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface PublicSetupCardProps {
|
interface PublicSetupCardProps {
|
||||||
setup: {
|
setup: {
|
||||||
@@ -11,6 +12,7 @@ interface PublicSetupCardProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function PublicSetupCard({ setup }: PublicSetupCardProps) {
|
export function PublicSetupCard({ setup }: PublicSetupCardProps) {
|
||||||
|
const { t } = useTranslation("setups");
|
||||||
const formattedDate = new Date(setup.createdAt).toLocaleDateString(
|
const formattedDate = new Date(setup.createdAt).toLocaleDateString(
|
||||||
undefined,
|
undefined,
|
||||||
{
|
{
|
||||||
@@ -30,13 +32,13 @@ export function PublicSetupCard({ setup }: PublicSetupCardProps) {
|
|||||||
{setup.name}
|
{setup.name}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs text-gray-400 mt-0.5">
|
<p className="text-xs text-gray-400 mt-0.5">
|
||||||
by {setup.creatorName || "Anonymous"}
|
{t("card.by", { name: setup.creatorName || t("card.anonymous") })}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center justify-between mt-2">
|
<div className="flex items-center justify-between mt-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{setup.itemCount != null && setup.itemCount > 0 && (
|
{setup.itemCount != null && setup.itemCount > 0 && (
|
||||||
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-50 text-blue-400">
|
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-50 text-blue-400">
|
||||||
{setup.itemCount} {setup.itemCount === 1 ? "item" : "items"}
|
{t("card.items", { count: setup.itemCount })}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useSetups } from "../hooks/useSetups";
|
import { useSetups } from "../hooks/useSetups";
|
||||||
import { useUIStore } from "../stores/uiStore";
|
import { useUIStore } from "../stores/uiStore";
|
||||||
|
|
||||||
@@ -8,6 +9,7 @@ interface SetupImpactSelectorProps {
|
|||||||
export function SetupImpactSelector({
|
export function SetupImpactSelector({
|
||||||
threadStatus,
|
threadStatus,
|
||||||
}: SetupImpactSelectorProps) {
|
}: SetupImpactSelectorProps) {
|
||||||
|
const { t } = useTranslation("setups");
|
||||||
const { data: setups } = useSetups();
|
const { data: setups } = useSetups();
|
||||||
const selectedSetupId = useUIStore((s) => s.selectedSetupId);
|
const selectedSetupId = useUIStore((s) => s.selectedSetupId);
|
||||||
const setSelectedSetupId = useUIStore((s) => s.setSelectedSetupId);
|
const setSelectedSetupId = useUIStore((s) => s.setSelectedSetupId);
|
||||||
@@ -23,7 +25,7 @@ export function SetupImpactSelector({
|
|||||||
}
|
}
|
||||||
className="border border-gray-200 rounded-lg text-sm px-3 py-1.5 text-gray-700 bg-white focus:outline-none focus:ring-2 focus:ring-gray-300"
|
className="border border-gray-200 rounded-lg text-sm px-3 py-1.5 text-gray-700 bg-white focus:outline-none focus:ring-2 focus:ring-gray-300"
|
||||||
>
|
>
|
||||||
<option value="">Compare with setup...</option>
|
<option value="">{t("impact.compareWith")}</option>
|
||||||
{setups.map((setup) => (
|
{setups.map((setup) => (
|
||||||
<option key={setup.id} value={setup.id}>
|
<option key={setup.id} value={setup.id}>
|
||||||
{setup.name}
|
{setup.name}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useFormatters } from "../hooks/useFormatters";
|
import { useFormatters } from "../hooks/useFormatters";
|
||||||
import { LucideIcon } from "../lib/iconData";
|
import { LucideIcon } from "../lib/iconData";
|
||||||
|
|
||||||
@@ -31,6 +32,7 @@ export function ThreadCard({
|
|||||||
categoryIcon,
|
categoryIcon,
|
||||||
}: ThreadCardProps) {
|
}: ThreadCardProps) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { t } = useTranslation("threads");
|
||||||
const { price } = useFormatters();
|
const { price } = useFormatters();
|
||||||
|
|
||||||
function formatPriceRange(
|
function formatPriceRange(
|
||||||
@@ -62,7 +64,7 @@ export function ThreadCard({
|
|||||||
<h3 className="text-sm font-semibold text-gray-900 truncate">{name}</h3>
|
<h3 className="text-sm font-semibold text-gray-900 truncate">{name}</h3>
|
||||||
{isResolved && (
|
{isResolved && (
|
||||||
<span className="ml-2 inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-500 shrink-0">
|
<span className="ml-2 inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-500 shrink-0">
|
||||||
Resolved
|
{t("status.resolved")}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -76,7 +78,7 @@ export function ThreadCard({
|
|||||||
{categoryName}
|
{categoryName}
|
||||||
</span>
|
</span>
|
||||||
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-purple-50 text-purple-500">
|
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-purple-50 text-purple-500">
|
||||||
{candidateCount} {candidateCount === 1 ? "candidate" : "candidates"}
|
{t("card.candidates", { count: candidateCount })}
|
||||||
</span>
|
</span>
|
||||||
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-50 text-gray-600">
|
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-50 text-gray-600">
|
||||||
{formatDate(createdAt)}
|
{formatDate(createdAt)}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
type TabKey = "gear" | "planning" | "setups";
|
type TabKey = "gear" | "planning" | "setups";
|
||||||
|
|
||||||
interface CollectionTabsProps {
|
interface CollectionTabsProps {
|
||||||
@@ -5,13 +7,14 @@ interface CollectionTabsProps {
|
|||||||
onChange: (tab: TabKey) => void;
|
onChange: (tab: TabKey) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tabs = [
|
|
||||||
{ key: "gear" as const, label: "My Gear" },
|
|
||||||
{ key: "planning" as const, label: "Planning" },
|
|
||||||
{ key: "setups" as const, label: "Setups" },
|
|
||||||
];
|
|
||||||
|
|
||||||
export function CollectionTabs({ active, onChange }: CollectionTabsProps) {
|
export function CollectionTabs({ active, onChange }: CollectionTabsProps) {
|
||||||
|
const { t } = useTranslation("collection");
|
||||||
|
const tabs = [
|
||||||
|
{ key: "gear" as const, label: t("gear") },
|
||||||
|
{ key: "planning" as const, label: t("planning") },
|
||||||
|
{ key: "setups" as const, label: t("tabs.setups") },
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex border-b border-gray-200">
|
<div className="flex border-b border-gray-200">
|
||||||
{tabs.map((tab) => (
|
{tabs.map((tab) => (
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Link } from "@tanstack/react-router";
|
import { Link } from "@tanstack/react-router";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useAuth } from "../hooks/useAuth";
|
import { useAuth } from "../hooks/useAuth";
|
||||||
import { LucideIcon } from "../lib/iconData";
|
import { LucideIcon } from "../lib/iconData";
|
||||||
import { UserMenu } from "./UserMenu";
|
import { UserMenu } from "./UserMenu";
|
||||||
@@ -9,6 +10,7 @@ interface TotalsBarProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function TotalsBar({ title = "GearBox", linkTo }: TotalsBarProps) {
|
export function TotalsBar({ title = "GearBox", linkTo }: TotalsBarProps) {
|
||||||
|
const { t } = useTranslation("common");
|
||||||
const { data: auth } = useAuth();
|
const { data: auth } = useAuth();
|
||||||
const isAuthenticated = !!auth?.user;
|
const isAuthenticated = !!auth?.user;
|
||||||
|
|
||||||
@@ -43,7 +45,7 @@ export function TotalsBar({ title = "GearBox", linkTo }: TotalsBarProps) {
|
|||||||
to="/login"
|
to="/login"
|
||||||
className="text-xs text-gray-500 hover:text-gray-700 transition-colors"
|
className="text-xs text-gray-500 hover:text-gray-700 transition-colors"
|
||||||
>
|
>
|
||||||
Sign in
|
{t("auth.signIn")}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -27,5 +27,17 @@
|
|||||||
"light": "Leicht",
|
"light": "Leicht",
|
||||||
"medium": "Mittel",
|
"medium": "Mittel",
|
||||||
"heavy": "Schwer"
|
"heavy": "Schwer"
|
||||||
|
},
|
||||||
|
"tabs": {
|
||||||
|
"setups": "Setups"
|
||||||
|
},
|
||||||
|
"totals": {
|
||||||
|
"totalWeight": "Gesamtgewicht",
|
||||||
|
"totalCost": "Gesamtkosten"
|
||||||
|
},
|
||||||
|
"classificationBadge": {
|
||||||
|
"base": "Basisgewicht",
|
||||||
|
"worn": "Getragen",
|
||||||
|
"consumable": "Verbrauchsmaterial"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,6 +82,12 @@
|
|||||||
"recentlyAdded": "Kürzlich hinzugefügt",
|
"recentlyAdded": "Kürzlich hinzugefügt",
|
||||||
"trendingCategories": "Trend-Kategorien"
|
"trendingCategories": "Trend-Kategorien"
|
||||||
},
|
},
|
||||||
|
"imageUpload": {
|
||||||
|
"clickToAdd": "Zum Hinzufügen klicken",
|
||||||
|
"invalidType": "Bitte wählen Sie ein JPG-, PNG- oder WebP-Bild aus.",
|
||||||
|
"tooLarge": "Das Bild muss kleiner als 5 MB sein.",
|
||||||
|
"uploadFailed": "Upload fehlgeschlagen. Bitte versuchen Sie es erneut."
|
||||||
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"title": "Profil",
|
"title": "Profil",
|
||||||
"account": "Konto",
|
"account": "Konto",
|
||||||
|
|||||||
@@ -6,10 +6,12 @@
|
|||||||
"description": "Erstellen Sie ein Setup, um Ausruestung fuer bestimmte Reisen oder Aktivitaeten zu organisieren."
|
"description": "Erstellen Sie ein Setup, um Ausruestung fuer bestimmte Reisen oder Aktivitaeten zu organisieren."
|
||||||
},
|
},
|
||||||
"card": {
|
"card": {
|
||||||
"items": "{{count}} Gegenstaende",
|
"items": "{{count}} Gegenstände",
|
||||||
"items_one": "{{count}} Gegenstand",
|
"items_one": "{{count}} Gegenstand",
|
||||||
"weight": "Gewicht",
|
"weight": "Gewicht",
|
||||||
"price": "Preis"
|
"price": "Preis",
|
||||||
|
"by": "von {{name}}",
|
||||||
|
"anonymous": "Anonym"
|
||||||
},
|
},
|
||||||
"share": {
|
"share": {
|
||||||
"title": "Setup teilen",
|
"title": "Setup teilen",
|
||||||
@@ -37,7 +39,8 @@
|
|||||||
},
|
},
|
||||||
"impact": {
|
"impact": {
|
||||||
"title": "Auswirkungsvorschau",
|
"title": "Auswirkungsvorschau",
|
||||||
"adding": "Hinzufuegen",
|
"adding": "Hinzufügen",
|
||||||
"removing": "Entfernen"
|
"removing": "Entfernen",
|
||||||
|
"compareWith": "Mit Setup vergleichen..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,22 @@
|
|||||||
"message": "<bold>{{name}}</bold> als Gewinner waehlen? Der Gegenstand wird Ihrer Sammlung hinzugefuegt und der Thread archiviert."
|
"message": "<bold>{{name}}</bold> als Gewinner waehlen? Der Gegenstand wird Ihrer Sammlung hinzugefuegt und der Thread archiviert."
|
||||||
},
|
},
|
||||||
"empty": {
|
"empty": {
|
||||||
"noThreads": "Noch keine Recherche-Threads",
|
"noThreads": "Keine Threads gefunden",
|
||||||
"noCandidates": "Noch keine Kandidaten"
|
"noCandidates": "Noch keine Kandidaten"
|
||||||
|
},
|
||||||
|
"card": {
|
||||||
|
"candidates": "{{count}} Kandidaten",
|
||||||
|
"candidates_one": "{{count}} Kandidat"
|
||||||
|
},
|
||||||
|
"planning": {
|
||||||
|
"title": "Planungs-Threads",
|
||||||
|
"emptyTitle": "Nächsten Kauf planen",
|
||||||
|
"createFirst": "Ersten Thread erstellen",
|
||||||
|
"step1Title": "Thread erstellen",
|
||||||
|
"step1Description": "Starten Sie einen Recherche-Thread für Ausrüstung, die Sie in Betracht ziehen",
|
||||||
|
"step2Title": "Kandidaten hinzufügen",
|
||||||
|
"step2Description": "Fügen Sie Produkte hinzu, die Sie mit Preisen und Gewichten vergleichen",
|
||||||
|
"step3Title": "Gewinner wählen",
|
||||||
|
"step3Description": "Thread abschließen und der Gewinner kommt in Ihre Sammlung"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,5 +27,17 @@
|
|||||||
"light": "Light",
|
"light": "Light",
|
||||||
"medium": "Medium",
|
"medium": "Medium",
|
||||||
"heavy": "Heavy"
|
"heavy": "Heavy"
|
||||||
|
},
|
||||||
|
"tabs": {
|
||||||
|
"setups": "Setups"
|
||||||
|
},
|
||||||
|
"totals": {
|
||||||
|
"totalWeight": "Total Weight",
|
||||||
|
"totalCost": "Total Cost"
|
||||||
|
},
|
||||||
|
"classificationBadge": {
|
||||||
|
"base": "Base Weight",
|
||||||
|
"worn": "Worn",
|
||||||
|
"consumable": "Consumable"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,6 +82,12 @@
|
|||||||
"recentlyAdded": "Recently Added",
|
"recentlyAdded": "Recently Added",
|
||||||
"trendingCategories": "Trending Categories"
|
"trendingCategories": "Trending Categories"
|
||||||
},
|
},
|
||||||
|
"imageUpload": {
|
||||||
|
"clickToAdd": "Click to add photo",
|
||||||
|
"invalidType": "Please select a JPG, PNG, or WebP image.",
|
||||||
|
"tooLarge": "Image must be under 5MB.",
|
||||||
|
"uploadFailed": "Upload failed. Please try again."
|
||||||
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"title": "Profile",
|
"title": "Profile",
|
||||||
"account": "Account",
|
"account": "Account",
|
||||||
|
|||||||
@@ -9,7 +9,9 @@
|
|||||||
"items": "{{count}} items",
|
"items": "{{count}} items",
|
||||||
"items_one": "{{count}} item",
|
"items_one": "{{count}} item",
|
||||||
"weight": "Weight",
|
"weight": "Weight",
|
||||||
"price": "Price"
|
"price": "Price",
|
||||||
|
"by": "by {{name}}",
|
||||||
|
"anonymous": "Anonymous"
|
||||||
},
|
},
|
||||||
"share": {
|
"share": {
|
||||||
"title": "Share Setup",
|
"title": "Share Setup",
|
||||||
@@ -38,6 +40,7 @@
|
|||||||
"impact": {
|
"impact": {
|
||||||
"title": "Impact Preview",
|
"title": "Impact Preview",
|
||||||
"adding": "Adding",
|
"adding": "Adding",
|
||||||
"removing": "Removing"
|
"removing": "Removing",
|
||||||
|
"compareWith": "Compare with setup..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,22 @@
|
|||||||
"message": "Pick <bold>{{name}}</bold> as the winner? This will add it to your collection and archive the thread."
|
"message": "Pick <bold>{{name}}</bold> as the winner? This will add it to your collection and archive the thread."
|
||||||
},
|
},
|
||||||
"empty": {
|
"empty": {
|
||||||
"noThreads": "No research threads yet",
|
"noThreads": "No threads found",
|
||||||
"noCandidates": "No candidates yet"
|
"noCandidates": "No candidates yet"
|
||||||
|
},
|
||||||
|
"card": {
|
||||||
|
"candidates": "{{count}} candidates",
|
||||||
|
"candidates_one": "{{count}} candidate"
|
||||||
|
},
|
||||||
|
"planning": {
|
||||||
|
"title": "Planning Threads",
|
||||||
|
"emptyTitle": "Plan your next purchase",
|
||||||
|
"createFirst": "Create your first thread",
|
||||||
|
"step1Title": "Create a thread",
|
||||||
|
"step1Description": "Start a research thread for gear you're considering",
|
||||||
|
"step2Title": "Add candidates",
|
||||||
|
"step2Description": "Add products you're comparing with prices and weights",
|
||||||
|
"step3Title": "Pick a winner",
|
||||||
|
"step3Description": "Resolve the thread and the winner joins your collection"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user