diff --git a/client/public/locales/en/common.json b/client/public/locales/en/common.json
index 45df776..c1459e9 100644
--- a/client/public/locales/en/common.json
+++ b/client/public/locales/en/common.json
@@ -208,6 +208,9 @@
},
"messages": {
"archive": "Are you sure you want to archive this spool?"
+ },
+ "formats": {
+ "last_used": "Last used {{date}}"
}
},
"filament": {
@@ -377,5 +380,9 @@
"create": "Create Manufacturer | Spoolman",
"clone": "#{{id}} Clone Manufacturer | Spoolman"
}
+ },
+ "locations": {
+ "new_location": "New Location",
+ "no_location": "No Location"
}
}
diff --git a/client/public/locales/sv/common.json b/client/public/locales/sv/common.json
index e36c408..39977fc 100644
--- a/client/public/locales/sv/common.json
+++ b/client/public/locales/sv/common.json
@@ -210,6 +210,9 @@
"measurement_type_label": "Mättyp",
"adjust_filament_value": "Mängd att konsumera",
"adjust_filament_help": "Här kan du direkt lägga till eller subtrahera filament från spolen. Ett positivt värde kommer att konsumera filament, ett negativt värde kommer att lägga till det."
+ },
+ "formats": {
+ "last_used": "Senast använd {{date}}"
}
},
"filament": {
@@ -376,5 +379,9 @@
"description": "
HjälpHär är några tips för att komma igång.
Spoolman innehåller 3 olika typer av data:
För att lägga till en ny spole i databasen måste du först skapa ett Filament-objekt för den. När det är klart kan du sedan skapa ett Spol-objekt för den individuella spolen. Om du sedan köper ytterligare spolar av samma filament kan du bara skapa ytterligare Spol-objekt och återanvända samma Filament-objekt.
Du kan valfritt också skapa ett Tillverkar-objekt för företaget som tillverkar filamentet om du vill spåra den informationen.
Du kan koppla andra 3D-skrivartjänster till Spoolman, som Moonraker, som sedan automatiskt kan spåra filamentanvändning och uppdatera Spol-objekten åt dig. Se Spoolman README för hur du gör det.
",
"help": "Hjälp"
},
- "kofi": "Dricksa mig på Ko-fi"
+ "kofi": "Dricksa mig på Ko-fi",
+ "locations": {
+ "new_location": "Ny plats",
+ "no_location": "Ingen plats"
+ }
}
diff --git a/client/src/i18n.ts b/client/src/i18n.ts
index a83382e..ac1f79b 100644
--- a/client/src/i18n.ts
+++ b/client/src/i18n.ts
@@ -1,3 +1,4 @@
+import dayjs from "dayjs";
import i18n from "i18next";
import detector from "i18next-browser-languagedetector";
import Backend from "i18next-http-backend";
@@ -8,6 +9,7 @@ interface Language {
name: string;
countryCode: string;
fullCode: string;
+ djs: () => Promise;
}
/**
@@ -16,107 +18,128 @@ interface Language {
* name: Name of the language in the list
* countryCode: Country code of the country's flag to display for this language
* fullCode: Full language code, used for Ant Design's locale
+ * djs: Function to load the dayjs locale, see https://github.com/iamkun/dayjs/tree/dev/src/locale for list of locales
*/
export const languages: { [key: string]: Language } = {
["en"]: {
name: "English",
countryCode: "gb",
fullCode: "en-GB",
+ djs: () => import("dayjs/locale/en"),
},
["sv"]: {
name: "Svenska",
countryCode: "se",
fullCode: "sv-SE",
+ djs: () => import("dayjs/locale/sv"),
},
["de"]: {
name: "Deutsch",
countryCode: "de",
fullCode: "de-DE",
+ djs: () => import("dayjs/locale/de"),
},
["es"]: {
name: "Español",
countryCode: "es",
fullCode: "es-ES",
+ djs: () => import("dayjs/locale/es"),
},
["zh"]: {
name: "简体中文",
countryCode: "cn",
fullCode: "zh-CN",
+ djs: () => import("dayjs/locale/zh-cn"),
},
["zh-Hant"]: {
name: "繁體中文",
countryCode: "cn",
fullCode: "zh-TW",
+ djs: () => import("dayjs/locale/zh-hk"),
},
["pl"]: {
name: "Polski",
countryCode: "pl",
fullCode: "pl-PL",
+ djs: () => import("dayjs/locale/pl"),
},
["ru"]: {
name: "Русский",
countryCode: "ru",
fullCode: "ru-RU",
+ djs: () => import("dayjs/locale/ru"),
},
["cs"]: {
name: "Česky",
countryCode: "cz",
fullCode: "cs-CZ",
+ djs: () => import("dayjs/locale/cs"),
},
["nb-NO"]: {
name: "Norsk bokmål",
countryCode: "no",
fullCode: "nb-NO",
+ djs: () => import("dayjs/locale/nb"),
},
["nl"]: {
name: "Nederlands",
countryCode: "nl",
fullCode: "nl-NL",
+ djs: () => import("dayjs/locale/nl"),
},
["fr"]: {
name: "Français",
countryCode: "fr",
fullCode: "fr-FR",
+ djs: () => import("dayjs/locale/fr"),
},
["hu"]: {
name: "Magyar",
countryCode: "hu",
fullCode: "hu-HU",
+ djs: () => import("dayjs/locale/hu"),
},
["it"]: {
name: "Italiano",
countryCode: "it",
fullCode: "it-IT",
+ djs: () => import("dayjs/locale/it"),
},
["uk"]: {
name: "Українська",
countryCode: "ua",
fullCode: "uk-UA",
+ djs: () => import("dayjs/locale/uk"),
},
["el"]: {
name: "Ελληνικά",
countryCode: "gr",
fullCode: "el-GR",
+ djs: () => import("dayjs/locale/el"),
},
["da"]: {
name: "Dansk",
countryCode: "dk",
fullCode: "da-DK",
+ djs: () => import("dayjs/locale/da"),
},
["pt"]: {
name: "Português",
countryCode: "pt",
fullCode: "pt-PT",
+ djs: () => import("dayjs/locale/pt"),
},
["fa"]: {
name: "فارسی",
countryCode: "ir",
fullCode: "fa-IR",
+ djs: () => import("dayjs/locale/fa"),
},
["ro"]: {
name: "Român",
countryCode: "ro",
fullCode: "ro-RO",
+ djs: () => import("dayjs/locale/ro"),
},
};
@@ -134,4 +157,8 @@ i18n
fallbackLng: "en",
});
+i18n.on("languageChanged", function (lng) {
+ languages[lng].djs().then((djs) => dayjs.locale(djs.name));
+});
+
export default i18n;
diff --git a/client/src/pages/locations/components/locationContainer.tsx b/client/src/pages/locations/components/locationContainer.tsx
index 1cac77b..cecb607 100644
--- a/client/src/pages/locations/components/locationContainer.tsx
+++ b/client/src/pages/locations/components/locationContainer.tsx
@@ -41,7 +41,7 @@ export function LocationContainer() {
const grouped: Record = {};
spools.forEach((spool) => {
- const loc = spool.location ?? t("spool.no_location");
+ const loc = spool.location ?? t("locations.no_location");
if (!grouped[loc]) {
grouped[loc] = [];
}
@@ -155,12 +155,13 @@ export function LocationContainer() {
}
const addNewLocation = () => {
- let newLocationName = "New Location";
+ const baseLocationName = t("locations.new_location");
+ let newLocationName = baseLocationName;
const newLocs = [...locationsList];
let i = 1;
while (newLocs.includes(newLocationName)) {
- newLocationName = "New Location " + i;
+ newLocationName = baseLocationName + " " + i;
i++;
}
newLocs.push(newLocationName);
diff --git a/client/src/pages/locations/components/spoolCard.tsx b/client/src/pages/locations/components/spoolCard.tsx
index 6da9a60..0f2c7f1 100644
--- a/client/src/pages/locations/components/spoolCard.tsx
+++ b/client/src/pages/locations/components/spoolCard.tsx
@@ -4,12 +4,18 @@ import type { Identifier, XYCoord } from "dnd-core";
import { useDrag, useDrop } from "react-dnd";
import { EditOutlined, EyeOutlined } from "@ant-design/icons";
+import dayjs from "dayjs";
+import relativeTime from "dayjs/plugin/relativeTime";
+import utc from "dayjs/plugin/utc";
import { useEffect, useRef } from "react";
import { Link } from "react-router-dom";
import SpoolIcon from "../../../components/spoolIcon";
import { ISpool } from "../../spools/model";
import { ItemTypes, SpoolDragItem, useCurrentDraggedSpool } from "../dnd";
+dayjs.extend(utc);
+dayjs.extend(relativeTime);
+
const { useToken } = theme;
export function SpoolCard({
@@ -134,6 +140,21 @@ export function SpoolCard({
};
drag(drop(ref));
+ function formatSubtitle(spool: ISpool) {
+ let str = "";
+ if (spool.filament.material) str += spool.filament.material + " - ";
+ if (spool.filament.weight) {
+ const remaining_weight = spool.remaining_weight ?? spool.filament.weight;
+ str += `${remaining_weight} / ${spool.filament.weight} g`;
+ }
+ if (spool.last_used) {
+ // Format like "last used X time ago"
+ const dt = dayjs(spool.last_used);
+ str += ` - ${t("spool.formats.last_used", { date: dt.fromNow() })}`;
+ }
+ return str;
+ }
+
return (
@@ -157,7 +178,7 @@ export function SpoolCard({
color: token.colorTextSecondary,
}}
>
- {spool.remaining_weight} / {spool.filament.weight} g
+ {formatSubtitle(spool)}