Browse Source

fix(labels): replace incorrect ams_30x15 preset with correct AMS holder sizes (#1426)

  Reporter — the same person who originally requested the labels
  feature in #809 — discovered that the ams_30x15 preset's 30x15 mm
  dimension didn't actually fit any variant of the MakerWorld AMS
  Filament Label Holder (model 752566) it advertised. Two new
  presets replace it:

  - ams_holder_74x33 (74 x 33 mm) matches the printable label STL
    bundled in the MakerWorld project
  - ams_holder_75x55 (75 x 55 mm) fits the cardstock-insert variant
    the reporter validated on bench

  Both cross the 20 mm height threshold so they land in the roomy
  layout branch — swatch on the left, QR on the right, multi-line
  text (brand, material, hex code, spool ID) in the middle. The
  old 30x15 mm preset couldn't fit a QR code; the new ones do.

  No DB migration: the preset name was never persisted. Callers
  scripting the old ams_30x15 value get a clean 422 at the route's
  Literal validator with the new valid values listed.

  i18n: replaced inventory.labels.templates.ams.{label,hint} with
  amsHolderSmall and amsHolderLarge across all 8 locales with real
  translations; parity guard cleaned of the stale English-fallback
  cognate entries. Parity holds at 4856 leaves per locale.

  Tests: backend label renderer + integration tests cover both new
  presets; LabelTemplatePickerModal test updated for the 6-button
  grid and the new template value in the API-call assertion.
maziggy 1 tuần trước cách đây
mục cha
commit
1677efb2c6

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
CHANGELOG.md


+ 10 - 2
backend/app/api/routes/labels.py

@@ -37,7 +37,8 @@ logger = logging.getLogger(__name__)
 router = APIRouter(tags=["labels"])
 
 _VALID_TEMPLATES: tuple[TemplateName, ...] = (
-    "ams_30x15",
+    "ams_holder_74x33",
+    "ams_holder_75x55",
     "box_40x30",
     "box_62x29",
     "avery_5160",
@@ -51,7 +52,14 @@ MAX_LABELS_PER_REQUEST = 500
 
 class LabelRequest(BaseModel):
     spool_ids: list[int] = Field(..., min_length=1, max_length=MAX_LABELS_PER_REQUEST)
-    template: Literal["ams_30x15", "box_40x30", "box_62x29", "avery_5160", "avery_l7160"]
+    template: Literal[
+        "ams_holder_74x33",
+        "ams_holder_75x55",
+        "box_40x30",
+        "box_62x29",
+        "avery_5160",
+        "avery_l7160",
+    ]
 
 
 def _split_extra_colors(raw: str | None) -> list[str] | None:

+ 32 - 17
backend/app/services/label_renderer.py

@@ -1,9 +1,12 @@
 """PDF spool label rendering.
 
-Five fixed templates:
+Six fixed templates:
 
-- ``ams_30x15``  — 30×15 mm single label, fits the popular Makerworld AMS
-  Filament Label Holder (model 752566). One label per page.
+- ``ams_holder_74x33`` — 74×33 mm single label, matches the printable label
+  STL bundled with the Makerworld AMS Filament Label Holder (model 752566).
+  Smaller variant — the visible window in the holder. One label per page.
+- ``ams_holder_75x55`` — 75×55 mm single label, fits the cardstock-insert
+  variant of the same holder. Roomier — swatch + QR + full text column.
 - ``box_40x30``  — 40×30 mm single label, common DK/Brother roll size and a
   good fit for filament-bag/storage-bin labels (#809 follow-up). Roomy
   layout — swatch, QR, full text column with hex code.
@@ -12,6 +15,10 @@ Five fixed templates:
 - ``avery_5160`` — US Letter sheet, 25.4×66.7 mm × 30 per sheet.
 - ``avery_l7160`` — A4 sheet, 38.1×63.5 mm × 21 per sheet.
 
+The legacy ``ams_30x15`` preset (#809) was incorrect — the original 30×15 mm
+dimension didn't fit any documented variant of model 752566. Replaced by the
+two ``ams_holder_*`` presets above (#1426).
+
 The renderer is decoupled from the Spool model: callers build a ``LabelData``
 list from whatever source (local DB, Spoolman, future) so the same code path
 works in both modes.
@@ -34,7 +41,14 @@ from reportlab.lib.pagesizes import A4, letter
 from reportlab.lib.units import mm
 from reportlab.pdfgen import canvas as rl_canvas
 
-TemplateName = Literal["ams_30x15", "box_40x30", "box_62x29", "avery_5160", "avery_l7160"]
+TemplateName = Literal[
+    "ams_holder_74x33",
+    "ams_holder_75x55",
+    "box_40x30",
+    "box_62x29",
+    "avery_5160",
+    "avery_l7160",
+]
 
 
 @dataclass
@@ -180,17 +194,17 @@ def _draw_label(c: rl_canvas.Canvas, x: float, y: float, w: float, h: float, dat
 
     Two layouts, picked by available height:
 
-    - **Tight** (h < 20 mm — AMS holder): swatch on the left, three lines of
-      text on the right (brand, material+subtype, big spool ID). No QR — at
-      30×15 mm there is not enough horizontal room for swatch + text + QR
-      without truncating away the user-need fields, and the AMS holder is an
-      at-a-glance identifier where the spool ID is the killer field. The
-      box-label and Avery templates carry the QR for the other use cases.
-
-    - **Roomy** (h >= 20 mm — box label, Avery sheets): swatch on the left,
-      QR on the right, multi-line text in the middle column. Large spool ID
-      anchored at bottom-left under the swatch so it stays readable when the
-      label is on a box on a shelf at arm's length.
+    - **Tight** (h < 20 mm): swatch on the left, three lines of text on the
+      right (brand, material+subtype, big spool ID). No QR — at very small
+      heights there is not enough horizontal room for swatch + text + QR
+      without truncating away the user-need fields. Kept as the safety
+      branch for any future ultra-small preset; the shipped templates all
+      land in the roomy layout below.
+
+    - **Roomy** (h >= 20 mm — AMS holder, box label, Avery sheets): swatch
+      on the left, QR on the right, multi-line text in the middle column.
+      Large spool ID anchored at bottom-left under the swatch so it stays
+      readable at arm's length.
     """
     pad = 1.2 * mm
     inner_x, inner_y = x + pad, y + pad
@@ -223,7 +237,7 @@ def _draw_label_tight(
     pad: float,
     data: LabelData,
 ) -> None:
-    """AMS-holder layout (e.g. 30×15 mm). Swatch + brand/material/hex/ID, no QR."""
+    """Tight layout (h < 20 mm). Swatch + brand/material/hex/ID, no QR."""
     swatch_w = min(inner_h, inner_w * 0.35)
     swatch_y = inner_y + (inner_h - swatch_w) / 2
     _draw_swatch(c, inner_x, swatch_y, swatch_w, swatch_w, data)
@@ -365,7 +379,8 @@ def _draw_label_roomy(
 
 # (label_w_mm, label_h_mm) for single-label-per-page templates.
 _SINGLE_LABEL_SIZES_MM: dict[str, tuple[float, float]] = {
-    "ams_30x15": (30.0, 15.0),
+    "ams_holder_74x33": (74.0, 33.0),
+    "ams_holder_75x55": (75.0, 55.0),
     "box_40x30": (40.0, 30.0),
     "box_62x29": (62.0, 29.0),
 }

+ 8 - 2
backend/tests/integration/test_labels.py

@@ -66,7 +66,13 @@ class TestLocalInventoryLabels:
     @pytest.mark.integration
     async def test_all_four_templates_succeed(self, async_client: AsyncClient, spool_factory):
         s = await spool_factory()
-        for template in ("ams_30x15", "box_62x29", "avery_5160", "avery_l7160"):
+        for template in (
+            "ams_holder_74x33",
+            "ams_holder_75x55",
+            "box_62x29",
+            "avery_5160",
+            "avery_l7160",
+        ):
             resp = await async_client.post(
                 "/api/v1/inventory/labels",
                 json={"spool_ids": [s.id], "template": template},
@@ -100,7 +106,7 @@ class TestLocalInventoryLabels:
         s = await spool_factory()
         resp = await async_client.post(
             "/api/v1/inventory/labels",
-            json={"spool_ids": [s.id, 99999], "template": "ams_30x15"},
+            json={"spool_ids": [s.id, 99999], "template": "ams_holder_74x33"},
         )
         assert resp.status_code == 404
         assert "99999" in resp.text

+ 17 - 9
backend/tests/unit/services/test_label_renderer.py

@@ -6,7 +6,14 @@ import pytest
 
 from backend.app.services.label_renderer import LabelData, render_labels
 
-ALL_TEMPLATES = ("ams_30x15", "box_40x30", "box_62x29", "avery_5160", "avery_l7160")
+ALL_TEMPLATES = (
+    "ams_holder_74x33",
+    "ams_holder_75x55",
+    "box_40x30",
+    "box_62x29",
+    "avery_5160",
+    "avery_l7160",
+)
 
 
 def _sample(spool_id: int = 1, **overrides) -> LabelData:
@@ -58,7 +65,7 @@ def test_missing_optional_fields_does_not_crash():
             deeplink_url="https://example.test/inventory?spool=42",
         )
     ]
-    pdf = render_labels("ams_30x15", data)
+    pdf = render_labels("ams_holder_74x33", data)
     assert pdf.startswith(b"%PDF")
 
 
@@ -74,7 +81,7 @@ def test_long_strings_are_truncated_not_overflowed():
     long_brand = "A" * 200
     long_name = "B" * 300
     data = [_sample(brand=long_brand, name=long_name)]
-    pdf = render_labels("ams_30x15", data)
+    pdf = render_labels("ams_holder_74x33", data)
     assert pdf.startswith(b"%PDF")
 
 
@@ -121,9 +128,10 @@ def _render_uncompressed(template, data):
     from backend.app.services.label_renderer import _draw_label  # noqa: PLC0415
 
     # Mirror the page-size choice from render_labels but force pageCompression=0.
-    if template in ("ams_30x15", "box_40x30", "box_62x29"):
+    if template in ("ams_holder_74x33", "ams_holder_75x55", "box_40x30", "box_62x29"):
         sizes = {
-            "ams_30x15": (30.0, 15.0),
+            "ams_holder_74x33": (74.0, 33.0),
+            "ams_holder_75x55": (75.0, 55.0),
             "box_40x30": (40.0, 30.0),
             "box_62x29": (62.0, 29.0),
         }
@@ -167,9 +175,9 @@ def _render_uncompressed(template, data):
 def test_ams_template_actually_renders_text():
     """Regression: the first cut of the AMS-holder layout produced labels with
     only swatch + QR and no text at all because the side-by-side layout left
-    <5 mm for the text column. The redesign drops the QR on this template and
-    gives the right side to brand + material + spool ID. This pins that the
-    rendered PDF contains all three fields.
+    <5 mm for the text column. The current AMS templates use the roomy layout
+    (swatch + QR + multi-line text); this pins that the rendered PDF contains
+    brand + material + spool ID for the smaller AMS preset.
     """
     data = [
         LabelData(
@@ -182,7 +190,7 @@ def test_ams_template_actually_renders_text():
             deeplink_url="https://example.test/inventory?spool=42",
         )
     ]
-    pdf = _render_uncompressed("ams_30x15", data)
+    pdf = _render_uncompressed("ams_holder_74x33", data)
     assert b"Polymaker" in pdf, "AMS template must render the brand"
     assert b"PLA" in pdf, "AMS template must render the material"
     # The bracketed-hash style is what the renderer uses for the spool ID;

+ 7 - 7
frontend/scripts/check-i18n-parity.mjs

@@ -160,7 +160,7 @@ const DE_COGNATES = [
   'Hex', 'Warm', 'Neutral', 'Navigation', 'Screenshot', 'Architecture',
   'Backend & Auth', 'Stream Overlay', 'Bambuddy Backend URL',
   'Material (optional)', 'Custom Headers (JSON)', '({{count}}/8)',
-  'AMS holder (30 × 15 mm)', 'Box label (62 × 29 mm)',
+  'Box label (62 × 29 mm)',
   'Avery L7160 — A4 sheet (38.1 × 63.5 mm × 21)',
   'Avery 5160 — US Letter sheet (25.4 × 66.7 mm × 30)',
   'China', 'Proxy', 'Start',
@@ -192,7 +192,7 @@ const FR_COGNATES = [
   '{{count}} filament', '{{count}} filaments', '{{count}} permissions',
   '{{count}} downloads', '{{count}} item', '{{count}} selected',
   '({{count}} item)', 'Provisioning...', 'Pressure Advance',
-  'AMS holder (30 × 15 mm)', 'Box label (62 × 29 mm)',
+  'Box label (62 × 29 mm)',
   'Avery L7160 — A4 sheet (38.1 × 63.5 mm × 21)',
   'Avery 5160 — US Letter sheet (25.4 × 66.7 mm × 30)',
   '({{count}}/8)', 'Custom Headers (JSON)', 'Permissions',
@@ -223,7 +223,7 @@ const IT_COGNATES = [
   '{{name}} - Timelapse', 'Box label (62 × 29 mm)',
   'Avery L7160 — A4 sheet (38.1 × 63.5 mm × 21)',
   'Avery 5160 — US Letter sheet (25.4 × 66.7 mm × 30)',
-  'AMS holder (30 × 15 mm)', 'Hex: #{{hex}}',
+  'Hex: #{{hex}}',
   'EC984C,#6CD4BC,A66EB9,D87694',
   'Proxy', 'Designer',
 ];
@@ -234,7 +234,7 @@ const JA_COGNATES = [
   'OK', 'Bambu', 'Code',
   'EU (DD/MM/YYYY)', 'US (MM/DD/YYYY)', 'ON, true, 1',
   '({{count}}/8)', 'Custom Headers (JSON)',
-  'AMS holder (30 × 15 mm)', 'Box label (62 × 29 mm)',
+  'Box label (62 × 29 mm)',
   'Avery L7160 — A4 sheet (38.1 × 63.5 mm × 21)',
   'Avery 5160 — US Letter sheet (25.4 × 66.7 mm × 30)',
   'EC984C,#6CD4BC,A66EB9,D87694',
@@ -257,7 +257,7 @@ const PT_BR_COGNATES = [
   'Base: {{name}}', 'ETA {{minutes}} min', '{{count}} item',
   '{{count}} downloads', '({{count}} item)', '(25%, 50%, 75%)',
   '({{count}}/8)', 'Custom Headers (JSON)', '{{name}} - Timelapse',
-  'AMS holder (30 × 15 mm)', 'Box label (62 × 29 mm)',
+  'Box label (62 × 29 mm)',
   'Avery L7160 — A4 sheet (38.1 × 63.5 mm × 21)',
   'Avery 5160 — US Letter sheet (25.4 × 66.7 mm × 30)',
   'Cancelling upload...', 'EC984C,#6CD4BC,A66EB9,D87694',
@@ -269,7 +269,7 @@ const PT_BR_COGNATES = [
 // Chinese (Simplified): very few cognates beyond brand names.
 const ZH_CN_COGNATES = [
   'OK', 'Bambu',
-  '({{count}}/8)', 'Custom Headers (JSON)', 'AMS holder (30 × 15 mm)',
+  '({{count}}/8)', 'Custom Headers (JSON)',
   'Box label (62 × 29 mm)',
   'Avery L7160 — A4 sheet (38.1 × 63.5 mm × 21)',
   'Avery 5160 — US Letter sheet (25.4 × 66.7 mm × 30)',
@@ -278,7 +278,7 @@ const ZH_CN_COGNATES = [
 
 const ZH_TW_COGNATES = [
   'OK', 'Bambu',
-  '({{count}}/8)', 'Custom Headers (JSON)', 'AMS holder (30 × 15 mm)',
+  '({{count}}/8)', 'Custom Headers (JSON)',
   'Box label (62 × 29 mm)',
   'Avery L7160 — A4 sheet (38.1 × 63.5 mm × 21)',
   'Avery 5160 — US Letter sheet (25.4 × 66.7 mm × 30)',

+ 16 - 7
frontend/src/__tests__/components/LabelTemplatePickerModal.test.tsx

@@ -176,7 +176,13 @@ describe('LabelTemplatePickerModal', () => {
         spoolmanMode={false}
       />,
     );
-    expect(screen.getByText(/AMS holder/i).closest('button')).toBeDisabled();
+    // Two AMS holder variants exist (#1426). Both must be disabled when no
+    // spools are selected — the empty-selection guard is global, not per-template.
+    const amsButtons = screen.getAllByText(/AMS holder/i).map((el) => el.closest('button'));
+    expect(amsButtons).toHaveLength(2);
+    for (const btn of amsButtons) {
+      expect(btn).toBeDisabled();
+    }
   });
 
   it('sends only the currently checked IDs to the local endpoint', async () => {
@@ -218,12 +224,14 @@ describe('LabelTemplatePickerModal', () => {
       />,
     );
 
-    fireEvent.click(screen.getByText(/AMS holder/i));
+    // Pick the larger AMS holder variant explicitly (#1426: two AMS templates
+     // exist now — pin which one the test sends so the assertion stays meaningful).
+    fireEvent.click(screen.getByText(/AMS holder — large \(75 × 55 mm\)/i));
 
     await waitFor(() => {
       expect(api.printSpoolmanSpoolLabels).toHaveBeenCalledWith({
         spool_ids: [1],
-        template: 'ams_30x15',
+        template: 'ams_holder_75x55',
       });
     });
     expect(api.printSpoolLabels).not.toHaveBeenCalled();
@@ -296,9 +304,10 @@ describe('LabelTemplatePickerModal', () => {
       />,
     );
 
-    // All five templates must be in the DOM. Use the dimension suffix to
-    // disambiguate the two "Box label …" entries.
-    expect(screen.getByText(/AMS holder/i)).toBeInTheDocument();
+    // All six templates must be in the DOM (#1426 added two AMS variants).
+    // Use the dimension suffix to disambiguate same-family entries.
+    expect(screen.getByText(/AMS holder — small \(74 × 33 mm\)/i)).toBeInTheDocument();
+    expect(screen.getByText(/AMS holder — large \(75 × 55 mm\)/i)).toBeInTheDocument();
     expect(screen.getByText(/Box label \(40 × 30 mm\)/i)).toBeInTheDocument();
     expect(screen.getByText(/Box label \(62 × 29 mm\)/i)).toBeInTheDocument();
     expect(screen.getByText(/Avery L7160/i)).toBeInTheDocument();
@@ -311,7 +320,7 @@ describe('LabelTemplatePickerModal', () => {
     const templatesSection = container.querySelector('div.grid.sm\\:grid-cols-2');
     expect(templatesSection).not.toBeNull();
     expect(templatesSection!.className).toContain('grid-cols-1');
-    expect(templatesSection!.querySelectorAll('button').length).toBe(5);
+    expect(templatesSection!.querySelectorAll('button').length).toBe(6);
 
     // Spool list still uses min-h-0 so it can yield further on very tight viewports.
     const spoolListScroller = container.querySelector('div.flex-1.overflow-y-auto');

+ 7 - 1
frontend/src/api/client.ts

@@ -2415,7 +2415,13 @@ export interface SpoolmanFilamentEntry {
 
 // Inventory types
 // Label printing (#809). Mirror of backend.app.services.label_renderer.TemplateName.
-export type SpoolLabelTemplate = 'ams_30x15' | 'box_40x30' | 'box_62x29' | 'avery_5160' | 'avery_l7160';
+export type SpoolLabelTemplate =
+  | 'ams_holder_74x33'
+  | 'ams_holder_75x55'
+  | 'box_40x30'
+  | 'box_62x29'
+  | 'avery_5160'
+  | 'avery_l7160';
 
 export interface InventorySpool {
   id: number;

+ 10 - 4
frontend/src/components/LabelTemplatePickerModal.tsx

@@ -33,10 +33,16 @@ interface TemplateOption {
 
 const TEMPLATE_OPTIONS: TemplateOption[] = [
   {
-    value: 'ams_30x15',
-    i18nKey: 'ams',
-    fallbackLabel: 'AMS holder (30 × 15 mm)',
-    fallbackHint: 'Single label per page; fits the popular AMS filament label holder.',
+    value: 'ams_holder_74x33',
+    i18nKey: 'amsHolderSmall',
+    fallbackLabel: 'AMS holder — small (74 × 33 mm)',
+    fallbackHint: 'Single label per page; matches the printable label from MakerWorld model 752566 (AMS Filament Label Holder).',
+  },
+  {
+    value: 'ams_holder_75x55',
+    i18nKey: 'amsHolderLarge',
+    fallbackLabel: 'AMS holder — large (75 × 55 mm)',
+    fallbackHint: 'Single label per page; fits the cardstock-insert variant of the AMS Filament Label Holder. Roomy enough for swatch, brand, material, ID, and QR code.',
   },
   {
     value: 'box_40x30',

+ 7 - 3
frontend/src/i18n/locales/de.ts

@@ -3542,9 +3542,13 @@ export default {
         color: 'Nach Farbe',
       },
       templates: {
-        ams: {
-          label: 'AMS holder (30 × 15 mm)',
-          hint: 'Ein Etikett pro Seite; passt in den beliebten AMS-Filament-Etikettenhalter.',
+        amsHolderSmall: {
+          label: 'AMS-Halter — klein (74 × 33 mm)',
+          hint: 'Ein Etikett pro Seite; passt zum druckbaren Etikett aus dem MakerWorld-Modell 752566 (AMS-Filament-Etikettenhalter).',
+        },
+        amsHolderLarge: {
+          label: 'AMS-Halter — groß (75 × 55 mm)',
+          hint: 'Ein Etikett pro Seite; passt zur Kartoneinleger-Variante des AMS-Filament-Etikettenhalters. Genug Platz für Farbprobe, Marke, Material, ID und QR-Code.',
         },
         box40x30: {
           label: 'Boxetikett (40 × 30 mm)',

+ 7 - 3
frontend/src/i18n/locales/en.ts

@@ -3545,9 +3545,13 @@ export default {
         color: 'By colour',
       },
       templates: {
-        ams: {
-          label: 'AMS holder (30 × 15 mm)',
-          hint: 'Single label per page; fits the popular AMS filament label holder.',
+        amsHolderSmall: {
+          label: 'AMS holder — small (74 × 33 mm)',
+          hint: 'Single label per page; matches the printable label from MakerWorld model 752566 (AMS Filament Label Holder).',
+        },
+        amsHolderLarge: {
+          label: 'AMS holder — large (75 × 55 mm)',
+          hint: 'Single label per page; fits the cardstock-insert variant of the AMS Filament Label Holder. Roomy enough for swatch, brand, material, ID, and QR code.',
         },
         box40x30: {
           label: 'Box label (40 × 30 mm)',

+ 7 - 3
frontend/src/i18n/locales/fr.ts

@@ -3531,9 +3531,13 @@ export default {
         color: 'Par couleur',
       },
       templates: {
-        ams: {
-          label: 'AMS holder (30 × 15 mm)',
-          hint: 'Une étiquette par page ; convient au support d\'étiquettes AMS populaire.',
+        amsHolderSmall: {
+          label: 'Support AMS — petit (74 × 33 mm)',
+          hint: 'Une étiquette par page ; correspond à l\'étiquette imprimable du modèle MakerWorld 752566 (Support d\'étiquettes pour filament AMS).',
+        },
+        amsHolderLarge: {
+          label: 'Support AMS — grand (75 × 55 mm)',
+          hint: 'Une étiquette par page ; s\'adapte à la variante carton du support d\'étiquettes pour filament AMS. Assez d\'espace pour pastille, marque, matériau, ID et QR code.',
         },
         box40x30: {
           label: 'Étiquette boîte (40 × 30 mm)',

+ 7 - 3
frontend/src/i18n/locales/it.ts

@@ -3530,9 +3530,13 @@ export default {
         color: 'Per colore',
       },
       templates: {
-        ams: {
-          label: 'AMS holder (30 × 15 mm)',
-          hint: 'Una etichetta per pagina; adatta al popolare portaetichette AMS.',
+        amsHolderSmall: {
+          label: 'Supporto AMS — piccolo (74 × 33 mm)',
+          hint: 'Un\'etichetta per pagina; corrisponde all\'etichetta stampabile del modello MakerWorld 752566 (Porta-etichette filamento AMS).',
+        },
+        amsHolderLarge: {
+          label: 'Supporto AMS — grande (75 × 55 mm)',
+          hint: 'Un\'etichetta per pagina; si adatta alla variante cartoncino del porta-etichette filamento AMS. Spazio sufficiente per campione colore, marca, materiale, ID e codice QR.',
         },
         box40x30: {
           label: 'Etichetta scatola (40 × 30 mm)',

+ 7 - 3
frontend/src/i18n/locales/ja.ts

@@ -3542,9 +3542,13 @@ export default {
         color: '色順',
       },
       templates: {
-        ams: {
-          label: 'AMS holder (30 × 15 mm)',
-          hint: '1ページに1ラベル、人気のAMSフィラメントラベルホルダーに適合。',
+        amsHolderSmall: {
+          label: 'AMS ホルダー — 小 (74 × 33 mm)',
+          hint: '1ページにつき1枚;MakerWorld モデル 752566 (AMS フィラメントラベルホルダー) の印刷可能なラベルに対応します。',
+        },
+        amsHolderLarge: {
+          label: 'AMS ホルダー — 大 (75 × 55 mm)',
+          hint: '1ページにつき1枚;AMS フィラメントラベルホルダーのカード紙挿入バージョンに対応します。色見本、ブランド、素材、ID、QR コードを表示できる広さです。',
         },
         box40x30: {
           label: 'ボックスラベル (40 × 30 mm)',

+ 7 - 3
frontend/src/i18n/locales/pt-BR.ts

@@ -3530,9 +3530,13 @@ export default {
         color: 'Por cor',
       },
       templates: {
-        ams: {
-          label: 'AMS holder (30 × 15 mm)',
-          hint: 'Uma etiqueta por página; serve no porta-etiquetas AMS popular.',
+        amsHolderSmall: {
+          label: 'Suporte AMS — pequeno (74 × 33 mm)',
+          hint: 'Uma etiqueta por página; corresponde à etiqueta imprimível do modelo MakerWorld 752566 (Suporte de etiquetas de filamento AMS).',
+        },
+        amsHolderLarge: {
+          label: 'Suporte AMS — grande (75 × 55 mm)',
+          hint: 'Uma etiqueta por página; encaixa na variante com inserto em cartão do Suporte de etiquetas de filamento AMS. Espaço suficiente para amostra de cor, marca, material, ID e QR code.',
         },
         box40x30: {
           label: 'Etiqueta de caixa (40 × 30 mm)',

+ 7 - 3
frontend/src/i18n/locales/zh-CN.ts

@@ -3530,9 +3530,13 @@ export default {
         color: '按颜色',
       },
       templates: {
-        ams: {
-          label: 'AMS holder (30 × 15 mm)',
-          hint: '每页一个标签;适用于流行的 AMS 耗材标签托架。',
+        amsHolderSmall: {
+          label: 'AMS 支架 — 小 (74 × 33 mm)',
+          hint: '每页一个标签;适配 MakerWorld 模型 752566 (AMS 耗材标签支架) 中可打印的标签。',
+        },
+        amsHolderLarge: {
+          label: 'AMS 支架 — 大 (75 × 55 mm)',
+          hint: '每页一个标签;适配 AMS 耗材标签支架的卡片插入款。空间充足,可放置色板、品牌、材料、ID 与二维码。',
         },
         box40x30: {
           label: '盒标签 (40 × 30 mm)',

+ 7 - 3
frontend/src/i18n/locales/zh-TW.ts

@@ -3530,9 +3530,13 @@ export default {
         color: '按顏色',
       },
       templates: {
-        ams: {
-          label: 'AMS holder (30 × 15 mm)',
-          hint: '每頁一個標籤;適用於熱門的 AMS 耗材標籤托架。',
+        amsHolderSmall: {
+          label: 'AMS 支架 — 小 (74 × 33 mm)',
+          hint: '每頁一個標籤;對應 MakerWorld 模型 752566 (AMS 耗材標籤支架) 中可列印的標籤。',
+        },
+        amsHolderLarge: {
+          label: 'AMS 支架 — 大 (75 × 55 mm)',
+          hint: '每頁一個標籤;對應 AMS 耗材標籤支架的紙卡插入款。空間足夠放置色塊、品牌、材質、ID 與 QR 碼。',
         },
         box40x30: {
           label: '盒標籤 (40 × 30 mm)',

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
static/assets/index-Brndhbee.js


+ 1 - 1
static/index.html

@@ -26,7 +26,7 @@
 
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-D9JqDSB2.js"></script>
+    <script type="module" crossorigin src="/assets/index-Brndhbee.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-KYwGxnG9.css">
   </head>
   <body>

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác