diff --git a/.gitignore b/.gitignore index 45ecfaf8..6f1f542d 100644 --- a/.gitignore +++ b/.gitignore @@ -69,5 +69,6 @@ apps/web/.codex apps/desktop/src-tauri/target/debug/* apps/desktop/src-tauri/target/release/ .claude +/docs - +.superpowers \ No newline at end of file diff --git a/apps/backend/app/api/routes/api_client/api.py b/apps/backend/app/api/routes/api_client/api.py index 3684aa21..d6ce504a 100644 --- a/apps/backend/app/api/routes/api_client/api.py +++ b/apps/backend/app/api/routes/api_client/api.py @@ -13,9 +13,11 @@ ApiClientHistoryOut, HISTORY_MAX_ITEMS, ) +from app.api.routes.api_client import collections_delta router = APIRouter(prefix="/api-client", tags=["api-client"]) +router.include_router(collections_delta.router) @router.get("/collections", response_model=list[ApiClientCollectionOut], summary="List API client collections") diff --git a/apps/backend/app/api/routes/api_client/collections_delta.py b/apps/backend/app/api/routes/api_client/collections_delta.py new file mode 100644 index 00000000..acacf310 --- /dev/null +++ b/apps/backend/app/api/routes/api_client/collections_delta.py @@ -0,0 +1,280 @@ +"""Service + route for the collection-items delta endpoint. + +POST /api-client/collections/{collection_id}/items:apply-delta + +Applies a list of ops (add / update / delete / move) to a collection's items +tree in-memory, then persists the result in a single atomic update_one call. +Cache is invalidated via bump_version exactly as the existing patch_collection +service does. +""" +from __future__ import annotations + +from typing import Any + +from bson import ObjectId +from bson.errors import InvalidId +from fastapi import APIRouter, Depends, HTTPException, status +from pymongo import ReturnDocument +from pymongo.errors import PyMongoError + +from app.api.routes.auth.services import get_current_uid +from app.api.routes.api_client.schema import ( + AddItemOp, + ApplyDeltaRequest, + ApplyDeltaResponse, + ApiClientCollectionOut, + DeleteItemOp, + MoveItemOp, + Op, + UpdateItemOp, +) +from app.core.cache import bump_version +from app.database import db_manager +from app.utils.collection_name import API_CLIENT_COLLECTIONS + +router = APIRouter() + + +def _parse_oid(raw: str, *, kind: str) -> ObjectId: + try: + return ObjectId(raw) + except InvalidId as exc: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Invalid {kind} id.", + ) from exc + + +def _collection_to_out(doc: dict[str, Any]) -> ApiClientCollectionOut: + oid = doc.get("_id") + return ApiClientCollectionOut( + id=str(oid) if oid is not None else "", + name=doc.get("name", ""), + items=list(doc.get("items") or []), + ) + + +# ── In-memory tree walkers ──────────────────────────────────────────────────── + +def _apply_add( + items: list[dict[str, Any]], + parent_id: str, + collection_id: str, + new_item: dict[str, Any], + position: int | None, +) -> list[dict[str, Any]]: + """Insert new_item under parent_id. parent_id may be the collection root.""" + if parent_id == collection_id: + # Insert at root level + if position is None or position >= len(items): + return items + [new_item] + result = list(items) + result.insert(position, new_item) + return result + + return _add_in_children(items, parent_id, new_item, position) + + +def _add_in_children( + items: list[dict[str, Any]], + parent_id: str, + new_item: dict[str, Any], + position: int | None, +) -> list[dict[str, Any]]: + result = [] + for item in items: + if item.get("id") == parent_id and item.get("type") == "folder": + children = list(item.get("items") or []) + if position is None or position >= len(children): + children = children + [new_item] + else: + children.insert(position, new_item) + result.append({**item, "items": children}) + elif item.get("type") == "folder": + result.append({**item, "items": _add_in_children(item.get("items") or [], parent_id, new_item, position)}) + else: + result.append(item) + return result + + +def _apply_update( + items: list[dict[str, Any]], + item_id: str, + patch: dict[str, Any], +) -> tuple[list[dict[str, Any]], bool]: + """Recursively apply patch to the item with item_id. Returns (new_items, found).""" + result = [] + found = False + for item in items: + if item.get("id") == item_id: + result.append({**item, **patch}) + found = True + elif item.get("type") == "folder": + new_children, child_found = _apply_update(item.get("items") or [], item_id, patch) + result.append({**item, "items": new_children}) + if child_found: + found = True + else: + result.append(item) + return result, found + + +def _apply_delete( + items: list[dict[str, Any]], + item_id: str, +) -> tuple[list[dict[str, Any]], bool]: + """Recursively remove item_id. Returns (new_items, found).""" + new_items = [] + found = False + for item in items: + if item.get("id") == item_id: + found = True + # skip (delete) + elif item.get("type") == "folder": + new_children, child_found = _apply_delete(item.get("items") or [], item_id) + new_items.append({**item, "items": new_children}) + if child_found: + found = True + else: + new_items.append(item) + return new_items, found + + +def _extract_item( + items: list[dict[str, Any]], + item_id: str, +) -> tuple[list[dict[str, Any]], dict[str, Any] | None]: + """Remove and return item_id from the tree.""" + new_items = [] + extracted: dict[str, Any] | None = None + for item in items: + if item.get("id") == item_id: + extracted = item + elif item.get("type") == "folder": + new_children, child_extracted = _extract_item(item.get("items") or [], item_id) + new_items.append({**item, "items": new_children}) + if child_extracted is not None: + extracted = child_extracted + else: + new_items.append(item) + return new_items, extracted + + +def _id_exists_in_tree(items: list[dict[str, Any]], node_id: str) -> bool: + """Return True if node_id appears anywhere in the tree (any level).""" + for item in items: + if item.get("id") == node_id: + return True + if item.get("type") == "folder": + if _id_exists_in_tree(item.get("items") or [], node_id): + return True + return False + + +def _apply_move( + items: list[dict[str, Any]], + item_id: str, + new_parent_id: str, + new_index: int, + collection_id: str, +) -> tuple[list[dict[str, Any]], bool]: + """Move item_id to new_parent_id at new_index. Returns (new_items, success).""" + # Validate that new_parent_id exists (root or a folder in the tree). + if new_parent_id != collection_id and not _id_exists_in_tree(items, new_parent_id): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="move: new_parent_id not found", + ) + + # Step 1: extract the item + items_without, target = _extract_item(items, item_id) + if target is None: + return items, False + + # Step 2: insert at new parent + new_items = _apply_add(items_without, new_parent_id, collection_id, target, new_index) + return new_items, True + + +# ── Service ─────────────────────────────────────────────────────────────────── + +async def apply_collection_delta( + uid: str, + collection_id: str, + ops: list[Op], +) -> ApiClientCollectionOut: + oid = _parse_oid(collection_id, kind="collection") + + # Fetch + ownership check BEFORE any mutation + doc = await db_manager.find_one(API_CLIENT_COLLECTIONS, {"_id": oid, "created_by": uid}) + if not doc: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Collection not found.") + + items: list[dict[str, Any]] = list(doc.get("items") or []) + + # Apply all ops in-memory (atomic within a single request) + for op in ops: + if isinstance(op, AddItemOp): + items = _apply_add(items, op.parent_id, collection_id, op.item, op.position) + + elif isinstance(op, UpdateItemOp): + items, found = _apply_update(items, op.item_id, op.patch) + if not found: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Item {op.item_id!r} not found.", + ) + + elif isinstance(op, DeleteItemOp): + items, found = _apply_delete(items, op.item_id) + if not found: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Item {op.item_id!r} not found.", + ) + + elif isinstance(op, MoveItemOp): + items, ok = _apply_move(items, op.item_id, op.new_parent_id, op.new_index, collection_id) + if not ok: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Item {op.item_id!r} not found.", + ) + + # Single atomic update + try: + updated_doc = await db_manager.find_one_and_update( + API_CLIENT_COLLECTIONS, + {"_id": oid, "created_by": uid}, + {"$set": {"items": items}}, + return_document=ReturnDocument.AFTER, + ) + except PyMongoError as exc: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to update collection.", + ) from exc + + if not updated_doc: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Collection not found.") + + # Invalidate cache (same namespace as patch_collection / delete_collection) + await bump_version(ns="api_client", uid=uid) + + return _collection_to_out(updated_doc) + + +# ── Route ───────────────────────────────────────────────────────────────────── + +@router.post( + "/collections/{collection_id}/items:apply-delta", + response_model=ApplyDeltaResponse, + summary="Apply delta ops to a collection's items tree", +) +async def apply_delta( + collection_id: str, + body: ApplyDeltaRequest, + uid: str = Depends(get_current_uid), +) -> ApplyDeltaResponse: + collection = await apply_collection_delta(uid, collection_id, body.ops) + return ApplyDeltaResponse(collection=collection) diff --git a/apps/backend/app/api/routes/api_client/schema.py b/apps/backend/app/api/routes/api_client/schema.py index d609d84c..58f383a1 100644 --- a/apps/backend/app/api/routes/api_client/schema.py +++ b/apps/backend/app/api/routes/api_client/schema.py @@ -1,4 +1,4 @@ -from typing import Any, Optional +from typing import Annotated, Any, Literal, Optional, Union from pydantic import BaseModel, ConfigDict, Field @@ -50,6 +50,47 @@ class ApiClientEnvironmentOut(ApiClientEnvironmentBase): HISTORY_MAX_ITEMS = 100 +# ── Delta ops ──────────────────────────────────────────────────────────────── + +class AddItemOp(BaseModel): + type: Literal["add"] + parent_id: str + item: dict[str, Any] + position: Optional[int] = None + + +class UpdateItemOp(BaseModel): + type: Literal["update"] + item_id: str + patch: dict[str, Any] + + +class DeleteItemOp(BaseModel): + type: Literal["delete"] + item_id: str + + +class MoveItemOp(BaseModel): + type: Literal["move"] + item_id: str + new_parent_id: str + new_index: int + + +Op = Annotated[ + Union[AddItemOp, UpdateItemOp, DeleteItemOp, MoveItemOp], + Field(discriminator="type"), +] + + +class ApplyDeltaRequest(BaseModel): + ops: list[Op] + + +class ApplyDeltaResponse(BaseModel): + collection: ApiClientCollectionOut + + class ApiClientHistoryCreate(BaseModel): """One sent request snapshot (matches web HistoryRequest minus id).""" diff --git a/apps/backend/tests/api/routes/api_client/__init__.py b/apps/backend/tests/api/routes/api_client/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/backend/tests/api/routes/api_client/test_collections_delta.py b/apps/backend/tests/api/routes/api_client/test_collections_delta.py new file mode 100644 index 00000000..bb6472be --- /dev/null +++ b/apps/backend/tests/api/routes/api_client/test_collections_delta.py @@ -0,0 +1,249 @@ +"""Tests for POST /api-client/collections/{id}/items:apply-delta. + +Auth pattern: override get_current_uid via FastAPI dependency_overrides +(same pattern used in test_auth_memo.py). MongoDB is monkeypatched in-process +so no real DB is needed. +""" +from __future__ import annotations + +import copy +from typing import Any +from unittest.mock import AsyncMock, MagicMock + +import pytest +from bson import ObjectId +from httpx import ASGITransport, AsyncClient + +from app.api.routes.auth.services import get_current_uid +from app.main import app + +# Fixed IDs used throughout +OWNER_UID = "test-owner-uid" +OTHER_UID = "other-uid" +COLLECTION_OID = ObjectId() +COLLECTION_ID = str(COLLECTION_OID) + +FOLDER_ID = "folder-1" +REQUEST_ID = "request-1" + +BASE_COLLECTION: dict[str, Any] = { + "_id": COLLECTION_OID, + "created_by": OWNER_UID, + "name": "My Collection", + "items": [ + { + "id": FOLDER_ID, + "name": "Folder 1", + "type": "folder", + "items": [ + { + "id": REQUEST_ID, + "name": "Existing Request", + "type": "request", + "method": "GET", + "url": "https://existing", + "params": [], + "headers": [], + "body": {"type": "none", "content": "", "formData": [], "urlEncoded": []}, + "auth": {"type": "none"}, + } + ], + "isOpen": True, + } + ], +} + + +def _make_client(uid: str) -> AsyncClient: + """Return an AsyncClient that authenticates as uid.""" + app.dependency_overrides[get_current_uid] = lambda: uid + return AsyncClient(transport=ASGITransport(app=app), base_url="http://test") + + +def _mock_find_one(collection_doc: dict | None): + """Monkeypatch db_manager.find_one to return collection_doc.""" + async def _find_one(collection_name, query, projection=None): + if collection_doc is None: + return None + # Honour ownership filter + if query.get("created_by") != collection_doc.get("created_by"): + return None + return copy.deepcopy(collection_doc) + + return _find_one + + +def _mock_find_one_and_update(updated_doc: dict | None): + """Monkeypatch db_manager.find_one_and_update to return updated_doc. + + The inner function stores the most-recent ``update_query`` on itself so + tests can inspect the ``$set`` payload without resorting to AsyncMock. + """ + async def _find_one_and_update(collection_name, query, update_query, return_document=False): + _find_one_and_update.last_update_query = update_query # type: ignore[attr-defined] + if updated_doc is None: + return None + return copy.deepcopy(updated_doc) + + _find_one_and_update.last_update_query = None # type: ignore[attr-defined] + return _find_one_and_update + + +@pytest.fixture(autouse=True) +def cleanup_overrides(): + yield + app.dependency_overrides.pop(get_current_uid, None) + + +# ── Test: add op inserts a new item under parent_id ────────────────────────── + +@pytest.mark.asyncio +async def test_apply_delta_adds_item(monkeypatch): + new_item = { + "id": "new-req-1", + "name": "New Request", + "type": "request", + "method": "POST", + "url": "https://new", + "params": [], + "headers": [], + "body": {"type": "none", "content": "", "formData": [], "urlEncoded": []}, + "auth": {"type": "none"}, + } + + # After the update, the collection has the new item under the folder + updated_collection = copy.deepcopy(BASE_COLLECTION) + updated_collection["items"][0]["items"].append(new_item) + + mock_update = _mock_find_one_and_update(updated_collection) + monkeypatch.setattr("app.api.routes.api_client.collections_delta.db_manager.find_one", _mock_find_one(BASE_COLLECTION)) + monkeypatch.setattr( + "app.api.routes.api_client.collections_delta.db_manager.find_one_and_update", + mock_update, + ) + monkeypatch.setattr("app.api.routes.api_client.collections_delta.bump_version", AsyncMock()) + + async with _make_client(OWNER_UID) as ac: + resp = await ac.post( + f"/api/v1/api-client/collections/{COLLECTION_ID}/items:apply-delta", + json={"ops": [{"type": "add", "parent_id": FOLDER_ID, "item": new_item}]}, + ) + + assert resp.status_code == 200, resp.text + body = resp.json() + assert "collection" in body + folder = next(i for i in body["collection"]["items"] if i["id"] == FOLDER_ID) + item_ids = {it["id"] for it in folder["items"]} + assert "new-req-1" in item_ids + + # Assert the $set payload sent to MongoDB contains the new item + update_query = mock_update.last_update_query + assert update_query is not None + persisted_items = update_query["$set"]["items"] + persisted_folder = next(i for i in persisted_items if i["id"] == FOLDER_ID) + persisted_ids = {it["id"] for it in persisted_folder["items"]} + assert "new-req-1" in persisted_ids + + +# ── Test: delete op removes an item ────────────────────────────────────────── + +@pytest.mark.asyncio +async def test_apply_delta_deletes_item(monkeypatch): + # After delete, folder has no children + updated_collection = copy.deepcopy(BASE_COLLECTION) + updated_collection["items"][0]["items"] = [] + + mock_update = _mock_find_one_and_update(updated_collection) + monkeypatch.setattr("app.api.routes.api_client.collections_delta.db_manager.find_one", _mock_find_one(BASE_COLLECTION)) + monkeypatch.setattr( + "app.api.routes.api_client.collections_delta.db_manager.find_one_and_update", + mock_update, + ) + monkeypatch.setattr("app.api.routes.api_client.collections_delta.bump_version", AsyncMock()) + + async with _make_client(OWNER_UID) as ac: + resp = await ac.post( + f"/api/v1/api-client/collections/{COLLECTION_ID}/items:apply-delta", + json={"ops": [{"type": "delete", "item_id": REQUEST_ID}]}, + ) + + assert resp.status_code == 200, resp.text + body = resp.json() + folder = next(i for i in body["collection"]["items"] if i["id"] == FOLDER_ID) + assert all(i["id"] != REQUEST_ID for i in folder["items"]) + + # Assert the $set payload sent to MongoDB has the item removed + update_query = mock_update.last_update_query + assert update_query is not None + persisted_items = update_query["$set"]["items"] + persisted_folder = next(i for i in persisted_items if i["id"] == FOLDER_ID) + assert all(it["id"] != REQUEST_ID for it in persisted_folder["items"]) + + +# ── Test: update op patches fields ─────────────────────────────────────────── + +@pytest.mark.asyncio +async def test_apply_delta_updates_item(monkeypatch): + updated_collection = copy.deepcopy(BASE_COLLECTION) + # Patch the nested request's name + updated_collection["items"][0]["items"][0]["name"] = "Renamed Request" + updated_collection["items"][0]["items"][0]["url"] = "https://updated" + + monkeypatch.setattr("app.api.routes.api_client.collections_delta.db_manager.find_one", _mock_find_one(BASE_COLLECTION)) + monkeypatch.setattr( + "app.api.routes.api_client.collections_delta.db_manager.find_one_and_update", + _mock_find_one_and_update(updated_collection), + ) + monkeypatch.setattr("app.api.routes.api_client.collections_delta.bump_version", AsyncMock()) + + async with _make_client(OWNER_UID) as ac: + resp = await ac.post( + f"/api/v1/api-client/collections/{COLLECTION_ID}/items:apply-delta", + json={"ops": [{"type": "update", "item_id": REQUEST_ID, "patch": {"name": "Renamed Request", "url": "https://updated"}}]}, + ) + + assert resp.status_code == 200, resp.text + body = resp.json() + folder = next(i for i in body["collection"]["items"] if i["id"] == FOLDER_ID) + req = next(i for i in folder["items"] if i["id"] == REQUEST_ID) + assert req["name"] == "Renamed Request" + assert req["url"] == "https://updated" + + +# ── Test: unauthorized user (different uid) gets 404 ───────────────────────── + +@pytest.mark.asyncio +async def test_apply_delta_wrong_uid_gets_404(monkeypatch): + # find_one returns None for OTHER_UID (ownership filter fails) + monkeypatch.setattr("app.api.routes.api_client.collections_delta.db_manager.find_one", _mock_find_one(BASE_COLLECTION)) + # bump_version should NOT be called + bump_mock = AsyncMock() + monkeypatch.setattr("app.api.routes.api_client.collections_delta.bump_version", bump_mock) + + async with _make_client(OTHER_UID) as ac: + resp = await ac.post( + f"/api/v1/api-client/collections/{COLLECTION_ID}/items:apply-delta", + json={"ops": [{"type": "delete", "item_id": REQUEST_ID}]}, + ) + + assert resp.status_code == 404 + bump_mock.assert_not_called() + + +# ── Test: move op with unknown new_parent_id returns 400 ───────────────────── + +@pytest.mark.asyncio +async def test_apply_delta_move_unknown_parent_returns_400(monkeypatch): + monkeypatch.setattr("app.api.routes.api_client.collections_delta.db_manager.find_one", _mock_find_one(BASE_COLLECTION)) + bump_mock = AsyncMock() + monkeypatch.setattr("app.api.routes.api_client.collections_delta.bump_version", bump_mock) + + async with _make_client(OWNER_UID) as ac: + resp = await ac.post( + f"/api/v1/api-client/collections/{COLLECTION_ID}/items:apply-delta", + json={"ops": [{"type": "move", "item_id": REQUEST_ID, "new_parent_id": "does-not-exist", "new_index": 0}]}, + ) + + assert resp.status_code == 400, resp.text + assert "new_parent_id" in resp.json().get("detail", "") + bump_mock.assert_not_called() diff --git a/apps/web/messages/af.json b/apps/web/messages/af.json index 9309f407..010ef05d 100644 --- a/apps/web/messages/af.json +++ b/apps/web/messages/af.json @@ -1467,7 +1467,8 @@ "requestFailed": "Versoek het misluk: {message}", "curlPasted": "cURL geplak en suksesvol ontleed", "responseCopied": "Antwoord na knipbord gekopieer", - "codeCopied": "Kode na knipbord gekopieer" + "codeCopied": "Kode na knipbord gekopieer", + "copyFailed": "Failed to copy to clipboard" }, "layout": { "collections": "Versamelings", @@ -1480,7 +1481,8 @@ "methodPlaceholder": "Metode", "urlPlaceholder": "https://api.example.com/v1/...", "sending": "Besig om te stuur...", - "send": "Stuur" + "send": "Stuur", + "invalidJsonBodyHelp": "Cannot send: the JSON body is invalid" }, "requestTabs": { "params": "Params", @@ -1528,7 +1530,9 @@ "previewTab": "Voorskou", "headersTab": "Koptekste", "binaryRawView": "Binêre data kan nie in rouaansig vertoon word nie.", - "errorStatusLabel": "Fout" + "errorStatusLabel": "Fout", + "truncatedBanner": "Showing first {shownKb} KB of a large response.", + "downloadFullBody": "Download full body" }, "tabBar": { "addTabAria": "Nuwe oortjie" @@ -1613,6 +1617,21 @@ "labelFolder": "Vouer", "placeholderFolder": "Kies 'n vouer", "save": "Stoor" + }, + "shortcuts": { + "ariaLabel": "Keyboard shortcuts", + "title": "Keyboard shortcuts", + "newTab": "New tab", + "closeTab": "Close tab", + "sendRequest": "Send request" + }, + "toolbar": { + "moreActions": "More actions", + "code": "Generate code", + "save": "Save request", + "importCurl": "Import cURL", + "environments": "Environments", + "shortcuts": "Keyboard shortcuts" } }, "RichEditor": { diff --git a/apps/web/messages/ar.json b/apps/web/messages/ar.json index 2d52c645..13d6c111 100644 --- a/apps/web/messages/ar.json +++ b/apps/web/messages/ar.json @@ -1467,7 +1467,8 @@ "requestFailed": "فشل الطلب: {message}", "curlPasted": "تم لصق cURL وتحليله بنجاح", "responseCopied": "تم نسخ الاستجابة", - "codeCopied": "تم نسخ الكود" + "codeCopied": "تم نسخ الكود", + "copyFailed": "فشل نسخ المحتوى إلى الحافظة" }, "layout": { "collections": "المجموعات", @@ -1480,7 +1481,8 @@ "methodPlaceholder": "الطريقة", "urlPlaceholder": "https://api.example.com/v1/...", "sending": "جاري الإرسال...", - "send": "إرسال" + "send": "إرسال", + "invalidJsonBodyHelp": "لا يمكن الإرسال: نص JSON غير صالح" }, "requestTabs": { "params": "معاملات", @@ -1528,7 +1530,9 @@ "previewTab": "معاينة", "headersTab": "الرؤوس", "binaryRawView": "لا يمكن عرض البيانات الثنائية في العرض الخام.", - "errorStatusLabel": "خطأ" + "errorStatusLabel": "خطأ", + "truncatedBanner": "Showing first {shownKb} KB of a large response.", + "downloadFullBody": "Download full body" }, "tabBar": { "addTabAria": "تبويب جديد" @@ -1613,6 +1617,21 @@ "labelFolder": "المجلد", "placeholderFolder": "اختر مجلداً", "save": "حفظ" + }, + "shortcuts": { + "ariaLabel": "Keyboard shortcuts", + "title": "Keyboard shortcuts", + "newTab": "New tab", + "closeTab": "Close tab", + "sendRequest": "Send request" + }, + "toolbar": { + "moreActions": "More actions", + "code": "Generate code", + "save": "Save request", + "importCurl": "Import cURL", + "environments": "Environments", + "shortcuts": "Keyboard shortcuts" } }, "RichEditor": { diff --git a/apps/web/messages/ca.json b/apps/web/messages/ca.json index 7610b53b..f1cbba47 100644 --- a/apps/web/messages/ca.json +++ b/apps/web/messages/ca.json @@ -1467,7 +1467,8 @@ "requestFailed": "Petició fallida: {message}", "curlPasted": "cURL enganxat i analitzat correctament", "responseCopied": "Resposta copiada al porta-retalls", - "codeCopied": "Codi copiat al porta-retalls" + "codeCopied": "Codi copiat al porta-retalls", + "copyFailed": "No s'ha pogut copiar al porta-retalls" }, "layout": { "collections": "Col·leccions", @@ -1480,7 +1481,8 @@ "methodPlaceholder": "Mètode", "urlPlaceholder": "https://api.exemple.com/v1/...", "sending": "S'està enviant...", - "send": "Envia" + "send": "Envia", + "invalidJsonBodyHelp": "No es pot enviar: el cos JSON no és vàlid" }, "requestTabs": { "params": "Params", @@ -1528,7 +1530,9 @@ "previewTab": "Vista prèvia", "headersTab": "Capçaleres", "binaryRawView": "Les dades binàries no es poden mostrar en vista en brut.", - "errorStatusLabel": "Error" + "errorStatusLabel": "Error", + "truncatedBanner": "Showing first {shownKb} KB of a large response.", + "downloadFullBody": "Download full body" }, "tabBar": { "addTabAria": "Pestanya nova" @@ -1613,6 +1617,21 @@ "labelFolder": "Carpeta", "placeholderFolder": "Selecciona una carpeta", "save": "Desa" + }, + "shortcuts": { + "ariaLabel": "Keyboard shortcuts", + "title": "Keyboard shortcuts", + "newTab": "New tab", + "closeTab": "Close tab", + "sendRequest": "Send request" + }, + "toolbar": { + "moreActions": "More actions", + "code": "Generate code", + "save": "Save request", + "importCurl": "Import cURL", + "environments": "Environments", + "shortcuts": "Keyboard shortcuts" } }, "RichEditor": { diff --git a/apps/web/messages/cs.json b/apps/web/messages/cs.json index fae1d684..bf3d722f 100644 --- a/apps/web/messages/cs.json +++ b/apps/web/messages/cs.json @@ -1467,7 +1467,8 @@ "requestFailed": "Požadavek selhal: {message}", "curlPasted": "cURL vložen a zpracován", "responseCopied": "Odpověď zkopírována", - "codeCopied": "Kód zkopírován" + "codeCopied": "Kód zkopírován", + "copyFailed": "Nepodařilo se zkopírovat do schránky" }, "layout": { "collections": "Kolekce", @@ -1480,7 +1481,8 @@ "methodPlaceholder": "Metoda", "urlPlaceholder": "https://api.example.com/v1/...", "sending": "Odesílání...", - "send": "Odeslat" + "send": "Odeslat", + "invalidJsonBodyHelp": "Nelze odeslat: tělo JSON není platné" }, "requestTabs": { "params": "Parametry", @@ -1528,7 +1530,9 @@ "previewTab": "Náhled", "headersTab": "Hlavičky", "binaryRawView": "Binární data nelze zobrazit v surovém zobrazení.", - "errorStatusLabel": "Chyba" + "errorStatusLabel": "Chyba", + "truncatedBanner": "Showing first {shownKb} KB of a large response.", + "downloadFullBody": "Download full body" }, "tabBar": { "addTabAria": "Nová karta" @@ -1613,6 +1617,21 @@ "labelFolder": "Složka", "placeholderFolder": "Vyberte složku", "save": "Uložit" + }, + "shortcuts": { + "ariaLabel": "Keyboard shortcuts", + "title": "Keyboard shortcuts", + "newTab": "New tab", + "closeTab": "Close tab", + "sendRequest": "Send request" + }, + "toolbar": { + "moreActions": "More actions", + "code": "Generate code", + "save": "Save request", + "importCurl": "Import cURL", + "environments": "Environments", + "shortcuts": "Keyboard shortcuts" } }, "RichEditor": { diff --git a/apps/web/messages/da.json b/apps/web/messages/da.json index 6f585d63..c23b6f96 100644 --- a/apps/web/messages/da.json +++ b/apps/web/messages/da.json @@ -1467,7 +1467,8 @@ "requestFailed": "Forespørgsel mislykkedes: {message}", "curlPasted": "cURL blev indsat og parset", "responseCopied": "Svar kopieret til udklipsholder", - "codeCopied": "Kode kopieret til udklipsholder" + "codeCopied": "Kode kopieret til udklipsholder", + "copyFailed": "Kopiering til udklipsholder mislykkedes" }, "layout": { "collections": "Samlinger", @@ -1480,7 +1481,8 @@ "methodPlaceholder": "Metode", "urlPlaceholder": "https://api.example.com/v1/...", "sending": "Sender...", - "send": "Sende" + "send": "Sende", + "invalidJsonBodyHelp": "Kan ikke sende: JSON-brødteksten er ugyldig" }, "requestTabs": { "params": "Params", @@ -1528,7 +1530,9 @@ "previewTab": "Forhåndsvisning", "headersTab": "Overskrifter", "binaryRawView": "Binære data kan ikke vises i rå visning.", - "errorStatusLabel": "Fejl" + "errorStatusLabel": "Fejl", + "truncatedBanner": "Showing first {shownKb} KB of a large response.", + "downloadFullBody": "Download full body" }, "tabBar": { "addTabAria": "Ny fane" @@ -1613,6 +1617,21 @@ "labelFolder": "Folder", "placeholderFolder": "Vælg en mappe", "save": "Spare" + }, + "shortcuts": { + "ariaLabel": "Keyboard shortcuts", + "title": "Keyboard shortcuts", + "newTab": "New tab", + "closeTab": "Close tab", + "sendRequest": "Send request" + }, + "toolbar": { + "moreActions": "More actions", + "code": "Generate code", + "save": "Save request", + "importCurl": "Import cURL", + "environments": "Environments", + "shortcuts": "Keyboard shortcuts" } }, "RichEditor": { diff --git a/apps/web/messages/de.json b/apps/web/messages/de.json index d2c3a8e4..6cc45e90 100644 --- a/apps/web/messages/de.json +++ b/apps/web/messages/de.json @@ -1467,7 +1467,8 @@ "requestFailed": "Anfrage fehlgeschlagen: {message}", "curlPasted": "cURL eingefügt und erfolgreich gelesen", "responseCopied": "Antwort in die Zwischenablage kopiert", - "codeCopied": "Code in die Zwischenablage kopiert" + "codeCopied": "Code in die Zwischenablage kopiert", + "copyFailed": "In die Zwischenablage kopieren fehlgeschlagen" }, "layout": { "collections": "Sammlungen", @@ -1480,7 +1481,8 @@ "methodPlaceholder": "Methode", "urlPlaceholder": "https://api.example.com/v1/...", "sending": "Senden...", - "send": "Senden" + "send": "Senden", + "invalidJsonBodyHelp": "Senden nicht möglich: Der JSON-Body ist ungültig" }, "requestTabs": { "params": "Parameter", @@ -1528,7 +1530,9 @@ "previewTab": "Vorschau", "headersTab": "Header", "binaryRawView": "Binärdaten können in der Rohansicht nicht angezeigt werden.", - "errorStatusLabel": "Fehler" + "errorStatusLabel": "Fehler", + "truncatedBanner": "Showing first {shownKb} KB of a large response.", + "downloadFullBody": "Download full body" }, "tabBar": { "addTabAria": "Neue Registerkarte" @@ -1613,6 +1617,21 @@ "labelFolder": "Ordner", "placeholderFolder": "Ordner wählen", "save": "Speichern" + }, + "shortcuts": { + "ariaLabel": "Keyboard shortcuts", + "title": "Keyboard shortcuts", + "newTab": "New tab", + "closeTab": "Close tab", + "sendRequest": "Send request" + }, + "toolbar": { + "moreActions": "More actions", + "code": "Generate code", + "save": "Save request", + "importCurl": "Import cURL", + "environments": "Environments", + "shortcuts": "Keyboard shortcuts" } }, "RichEditor": { diff --git a/apps/web/messages/el.json b/apps/web/messages/el.json index 3344caa8..b1d4ecf1 100644 --- a/apps/web/messages/el.json +++ b/apps/web/messages/el.json @@ -1467,7 +1467,8 @@ "requestFailed": "Το αίτημα απέτυχε: {message}", "curlPasted": "Το cURL επικολλήθηκε και αναλύθηκε με επιτυχία", "responseCopied": "Η απάντηση αντιγράφηκε στο πρόχειρο", - "codeCopied": "Ο κώδικας αντιγράφηκε στο πρόχειρο" + "codeCopied": "Ο κώδικας αντιγράφηκε στο πρόχειρο", + "copyFailed": "Αποτυχία αντιγραφής στο πρόχειρο" }, "layout": { "collections": "Συλλογές", @@ -1480,7 +1481,8 @@ "methodPlaceholder": "Μέθοδος", "urlPlaceholder": "https://api.example.com/v1/...", "sending": "Αποστολή...", - "send": "Στέλνω" + "send": "Στέλνω", + "invalidJsonBodyHelp": "Αδύνατη η αποστολή: το σώμα JSON δεν είναι έγκυρο" }, "requestTabs": { "params": "Params", @@ -1528,7 +1530,9 @@ "previewTab": "Πρεμιέρα", "headersTab": "Κεφαλίδες", "binaryRawView": "Τα δυαδικά δεδομένα δεν μπορούν να εμφανιστούν σε μη επεξεργασμένη προβολή.", - "errorStatusLabel": "Σφάλμα" + "errorStatusLabel": "Σφάλμα", + "truncatedBanner": "Showing first {shownKb} KB of a large response.", + "downloadFullBody": "Download full body" }, "tabBar": { "addTabAria": "Νέα καρτέλα" @@ -1613,6 +1617,21 @@ "labelFolder": "Ντοσιέ", "placeholderFolder": "Επιλέξτε ένα φάκελο", "save": "Εκτός" + }, + "shortcuts": { + "ariaLabel": "Keyboard shortcuts", + "title": "Keyboard shortcuts", + "newTab": "New tab", + "closeTab": "Close tab", + "sendRequest": "Send request" + }, + "toolbar": { + "moreActions": "More actions", + "code": "Generate code", + "save": "Save request", + "importCurl": "Import cURL", + "environments": "Environments", + "shortcuts": "Keyboard shortcuts" } }, "RichEditor": { diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 73bec8a4..839577b7 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -1488,7 +1488,8 @@ "requestFailed": "Request failed: {message}", "curlPasted": "cURL pasted and parsed successfully", "responseCopied": "Response copied to clipboard", - "codeCopied": "Code copied to clipboard" + "codeCopied": "Code copied to clipboard", + "copyFailed": "Failed to copy to clipboard" }, "layout": { "collections": "Collections", @@ -1501,7 +1502,8 @@ "methodPlaceholder": "Method", "urlPlaceholder": "https://api.example.com/v1/...", "sending": "Sending...", - "send": "Send" + "send": "Send", + "invalidJsonBodyHelp": "Cannot send: the JSON body is invalid" }, "requestTabs": { "params": "Params", @@ -1549,7 +1551,9 @@ "previewTab": "Preview", "headersTab": "Headers", "binaryRawView": "Binary data cannot be displayed in raw view.", - "errorStatusLabel": "Error" + "errorStatusLabel": "Error", + "truncatedBanner": "Showing first {shownKb} KB of a large response.", + "downloadFullBody": "Download full body" }, "tabBar": { "addTabAria": "New tab" @@ -1637,6 +1641,21 @@ "labelFolder": "Folder", "placeholderFolder": "Select a folder", "save": "Save" + }, + "shortcuts": { + "ariaLabel": "Keyboard shortcuts", + "title": "Keyboard shortcuts", + "newTab": "New tab", + "closeTab": "Close tab", + "sendRequest": "Send request" + }, + "toolbar": { + "moreActions": "More actions", + "code": "Generate code", + "save": "Save request", + "importCurl": "Import cURL", + "environments": "Environments", + "shortcuts": "Keyboard shortcuts" } }, "RichEditor": { diff --git a/apps/web/messages/es.json b/apps/web/messages/es.json index 4c9257d9..b6be8c10 100644 --- a/apps/web/messages/es.json +++ b/apps/web/messages/es.json @@ -1467,7 +1467,8 @@ "requestFailed": "Error en la solicitud: {message}", "curlPasted": "cURL pegado y analizado correctamente", "responseCopied": "Respuesta copiada al portapapeles", - "codeCopied": "Código copiado al portapapeles" + "codeCopied": "Código copiado al portapapeles", + "copyFailed": "Error al copiar al portapapeles" }, "layout": { "collections": "Colecciones", @@ -1480,7 +1481,8 @@ "methodPlaceholder": "Método", "urlPlaceholder": "https://api.ejemplo.com/v1/...", "sending": "Enviando...", - "send": "Enviar" + "send": "Enviar", + "invalidJsonBodyHelp": "No se puede enviar: el cuerpo JSON no es válido" }, "requestTabs": { "params": "Parámetros", @@ -1528,7 +1530,9 @@ "previewTab": "Vista previa", "headersTab": "Cabeceras", "binaryRawView": "Los datos binarios no se pueden mostrar en vista en bruto.", - "errorStatusLabel": "Error" + "errorStatusLabel": "Error", + "truncatedBanner": "Showing first {shownKb} KB of a large response.", + "downloadFullBody": "Download full body" }, "tabBar": { "addTabAria": "Nueva pestaña" @@ -1613,6 +1617,21 @@ "labelFolder": "Carpeta", "placeholderFolder": "Selecciona una carpeta", "save": "Guardar" + }, + "shortcuts": { + "ariaLabel": "Keyboard shortcuts", + "title": "Keyboard shortcuts", + "newTab": "New tab", + "closeTab": "Close tab", + "sendRequest": "Send request" + }, + "toolbar": { + "moreActions": "More actions", + "code": "Generate code", + "save": "Save request", + "importCurl": "Import cURL", + "environments": "Environments", + "shortcuts": "Keyboard shortcuts" } }, "RichEditor": { diff --git a/apps/web/messages/fa.json b/apps/web/messages/fa.json index e2b52967..cdb1e4dd 100644 --- a/apps/web/messages/fa.json +++ b/apps/web/messages/fa.json @@ -1467,7 +1467,8 @@ "requestFailed": "درخواست ناموفق بود: {message}", "curlPasted": "cURL با موفقیت جای‌گذاری و تجزیه شد", "responseCopied": "پاسخ در کلیپ بورد کپی شد", - "codeCopied": "کد در کلیپ بورد کپی شد" + "codeCopied": "کد در کلیپ بورد کپی شد", + "copyFailed": "کپی در کلیپ‌بورد ناموفق بود" }, "layout": { "collections": "مجموعه ها", @@ -1480,7 +1481,8 @@ "methodPlaceholder": "روش", "urlPlaceholder": "https://api.example.com/v1/...", "sending": "ارسال...", - "send": "ارسال کنید" + "send": "ارسال کنید", + "invalidJsonBodyHelp": "ارسال امکان‌پذیر نیست: بدنه JSON نامعتبر است" }, "requestTabs": { "params": "پارامترها", @@ -1528,7 +1530,9 @@ "previewTab": "پیش نمایش", "headersTab": "سرصفحه ها", "binaryRawView": "داده های باینری را نمی توان در نمای خام نمایش داد.", - "errorStatusLabel": "خطا" + "errorStatusLabel": "خطا", + "truncatedBanner": "Showing first {shownKb} KB of a large response.", + "downloadFullBody": "Download full body" }, "tabBar": { "addTabAria": "برگه جدید" @@ -1613,6 +1617,21 @@ "labelFolder": "پوشه", "placeholderFolder": "یک پوشه را انتخاب کنید", "save": "ذخیره کنید" + }, + "shortcuts": { + "ariaLabel": "Keyboard shortcuts", + "title": "Keyboard shortcuts", + "newTab": "New tab", + "closeTab": "Close tab", + "sendRequest": "Send request" + }, + "toolbar": { + "moreActions": "More actions", + "code": "Generate code", + "save": "Save request", + "importCurl": "Import cURL", + "environments": "Environments", + "shortcuts": "Keyboard shortcuts" } }, "RichEditor": { diff --git a/apps/web/messages/fr.json b/apps/web/messages/fr.json index 694642da..41199de9 100644 --- a/apps/web/messages/fr.json +++ b/apps/web/messages/fr.json @@ -1467,7 +1467,8 @@ "requestFailed": "Échec de la requête : {message}", "curlPasted": "cURL collé et analysé", "responseCopied": "Réponse copiée", - "codeCopied": "Code copié" + "codeCopied": "Code copié", + "copyFailed": "Impossible de copier dans le presse-papiers" }, "layout": { "collections": "Collections", @@ -1480,7 +1481,8 @@ "methodPlaceholder": "Méthode", "urlPlaceholder": "https://api.example.com/v1/...", "sending": "Envoi…", - "send": "Envoyer" + "send": "Envoyer", + "invalidJsonBodyHelp": "Envoi impossible : le corps JSON est invalide" }, "requestTabs": { "params": "Paramètres", @@ -1528,7 +1530,9 @@ "previewTab": "Aperçu", "headersTab": "En-têtes", "binaryRawView": "Les données binaires ne peuvent pas s’afficher en brut.", - "errorStatusLabel": "Erreur" + "errorStatusLabel": "Erreur", + "truncatedBanner": "Showing first {shownKb} KB of a large response.", + "downloadFullBody": "Download full body" }, "tabBar": { "addTabAria": "Nouvel onglet" @@ -1613,6 +1617,21 @@ "labelFolder": "Dossier", "placeholderFolder": "Choisir un dossier", "save": "Enregistrer" + }, + "shortcuts": { + "ariaLabel": "Keyboard shortcuts", + "title": "Keyboard shortcuts", + "newTab": "New tab", + "closeTab": "Close tab", + "sendRequest": "Send request" + }, + "toolbar": { + "moreActions": "More actions", + "code": "Generate code", + "save": "Save request", + "importCurl": "Import cURL", + "environments": "Environments", + "shortcuts": "Keyboard shortcuts" } }, "RichEditor": { diff --git a/apps/web/messages/id.json b/apps/web/messages/id.json index d6ea2183..2e54a9eb 100644 --- a/apps/web/messages/id.json +++ b/apps/web/messages/id.json @@ -1467,7 +1467,8 @@ "requestFailed": "Permintaan gagal: {message}", "curlPasted": "cURL berhasil ditempelkan dan diuraikan", "responseCopied": "Tanggapan disalin ke clipboard", - "codeCopied": "Disalin ke papan klip" + "codeCopied": "Disalin ke papan klip", + "copyFailed": "Gagal menyalin ke papan klip" }, "layout": { "collections": "Penagihan", @@ -1480,7 +1481,8 @@ "methodPlaceholder": "Metode ", "urlPlaceholder": "https://api.example.com/v1/...", "sending": "Mengirim...", - "send": "Kirim" + "send": "Kirim", + "invalidJsonBodyHelp": "Tidak dapat mengirim: isi JSON tidak valid" }, "requestTabs": { "params": "parameter", @@ -1528,7 +1530,9 @@ "previewTab": "Preview", "headersTab": "Headers", "binaryRawView": "Binary data cannot be displayed in raw view.", - "errorStatusLabel": "Error" + "errorStatusLabel": "Error", + "truncatedBanner": "Showing first {shownKb} KB of a large response.", + "downloadFullBody": "Download full body" }, "tabBar": { "addTabAria": "New tab" @@ -1613,6 +1617,21 @@ "labelFolder": "Folder", "placeholderFolder": "Select a folder", "save": "Save" + }, + "shortcuts": { + "ariaLabel": "Keyboard shortcuts", + "title": "Keyboard shortcuts", + "newTab": "New tab", + "closeTab": "Close tab", + "sendRequest": "Send request" + }, + "toolbar": { + "moreActions": "More actions", + "code": "Generate code", + "save": "Save request", + "importCurl": "Import cURL", + "environments": "Environments", + "shortcuts": "Keyboard shortcuts" } }, "RichEditor": { diff --git a/apps/web/messages/it.json b/apps/web/messages/it.json index 5d893705..a02d2c7a 100644 --- a/apps/web/messages/it.json +++ b/apps/web/messages/it.json @@ -1467,7 +1467,8 @@ "requestFailed": "Richiesta non riuscita: {message}", "curlPasted": "cURL incollato e analizzato correttamente", "responseCopied": "Risposta copiata negli appunti", - "codeCopied": "Codice copiato negli appunti" + "codeCopied": "Codice copiato negli appunti", + "copyFailed": "Copia negli appunti non riuscita" }, "layout": { "collections": "Collezioni", @@ -1480,7 +1481,8 @@ "methodPlaceholder": "Metodo", "urlPlaceholder": "https://api.esempio.com/v1/...", "sending": "Invio...", - "send": "Inviare" + "send": "Inviare", + "invalidJsonBodyHelp": "Impossibile inviare: il corpo JSON non è valido" }, "requestTabs": { "params": "Param", @@ -1528,7 +1530,9 @@ "previewTab": "Anteprima", "headersTab": "Intestazioni", "binaryRawView": "I dati binari non possono essere visualizzati nella vista grezza.", - "errorStatusLabel": "Errore" + "errorStatusLabel": "Errore", + "truncatedBanner": "Showing first {shownKb} KB of a large response.", + "downloadFullBody": "Download full body" }, "tabBar": { "addTabAria": "Nuova scheda" @@ -1613,6 +1617,21 @@ "labelFolder": "Cartella", "placeholderFolder": "Seleziona una cartella", "save": "Salva" + }, + "shortcuts": { + "ariaLabel": "Keyboard shortcuts", + "title": "Keyboard shortcuts", + "newTab": "New tab", + "closeTab": "Close tab", + "sendRequest": "Send request" + }, + "toolbar": { + "moreActions": "More actions", + "code": "Generate code", + "save": "Save request", + "importCurl": "Import cURL", + "environments": "Environments", + "shortcuts": "Keyboard shortcuts" } }, "RichEditor": { diff --git a/apps/web/messages/ja.json b/apps/web/messages/ja.json index 2651fe1e..11ff90ba 100644 --- a/apps/web/messages/ja.json +++ b/apps/web/messages/ja.json @@ -1467,7 +1467,8 @@ "requestFailed": "リクエストに失敗しました:{message}", "curlPasted": "cURLが貼り付けられ、正常に解析されました", "responseCopied": "レスポンスをクリップボードにコピーしました", - "codeCopied": "コードをクリップボードにコピーしました" + "codeCopied": "コードをクリップボードにコピーしました", + "copyFailed": "クリップボードへのコピーに失敗しました" }, "layout": { "collections": "コレクション", @@ -1480,7 +1481,8 @@ "methodPlaceholder": "メソッド", "urlPlaceholder": "https://api.example.com/v1/...", "sending": "送信中...", - "send": "送信" + "send": "送信", + "invalidJsonBodyHelp": "送信できません: JSONボディが無効です" }, "requestTabs": { "params": "パラメータ", @@ -1528,7 +1530,9 @@ "previewTab": "プレビュー", "headersTab": "ヘッダー", "binaryRawView": "バイナリデータはraw表示できません。", - "errorStatusLabel": "エラー" + "errorStatusLabel": "エラー", + "truncatedBanner": "Showing first {shownKb} KB of a large response.", + "downloadFullBody": "Download full body" }, "tabBar": { "addTabAria": "新しいタブ" @@ -1613,6 +1617,21 @@ "labelFolder": "フォルダー", "placeholderFolder": "フォルダーを選択", "save": "保存" + }, + "shortcuts": { + "ariaLabel": "Keyboard shortcuts", + "title": "Keyboard shortcuts", + "newTab": "New tab", + "closeTab": "Close tab", + "sendRequest": "Send request" + }, + "toolbar": { + "moreActions": "More actions", + "code": "Generate code", + "save": "Save request", + "importCurl": "Import cURL", + "environments": "Environments", + "shortcuts": "Keyboard shortcuts" } }, "RichEditor": { diff --git a/apps/web/messages/ko.json b/apps/web/messages/ko.json index f28edd5b..89d9983b 100644 --- a/apps/web/messages/ko.json +++ b/apps/web/messages/ko.json @@ -1467,7 +1467,8 @@ "requestFailed": "요청 실패: {message}", "curlPasted": "cURL이 붙여넣기 및 파싱되었습니다", "responseCopied": "응답이 클립보드에 복사되었습니다", - "codeCopied": "코드가 클립보드에 복사되었습니다" + "codeCopied": "코드가 클립보드에 복사되었습니다", + "copyFailed": "클립보드에 복사하지 못했습니다" }, "layout": { "collections": "컬렉션", @@ -1480,7 +1481,8 @@ "methodPlaceholder": "메서드", "urlPlaceholder": "https://api.example.com/v1/...", "sending": "전송 중...", - "send": "전송" + "send": "전송", + "invalidJsonBodyHelp": "전송 불가: JSON 본문이 유효하지 않습니다" }, "requestTabs": { "params": "매개변수", @@ -1528,7 +1530,9 @@ "previewTab": "미리보기", "headersTab": "헤더", "binaryRawView": "이진 데이터는 원시 보기에서 표시할 수 없습니다.", - "errorStatusLabel": "오류" + "errorStatusLabel": "오류", + "truncatedBanner": "Showing first {shownKb} KB of a large response.", + "downloadFullBody": "Download full body" }, "tabBar": { "addTabAria": "새 탭" @@ -1613,6 +1617,21 @@ "labelFolder": "폴더", "placeholderFolder": "폴더를 선택하세요", "save": "저장" + }, + "shortcuts": { + "ariaLabel": "Keyboard shortcuts", + "title": "Keyboard shortcuts", + "newTab": "New tab", + "closeTab": "Close tab", + "sendRequest": "Send request" + }, + "toolbar": { + "moreActions": "More actions", + "code": "Generate code", + "save": "Save request", + "importCurl": "Import cURL", + "environments": "Environments", + "shortcuts": "Keyboard shortcuts" } }, "RichEditor": { diff --git a/apps/web/messages/ms.json b/apps/web/messages/ms.json index 3c59c6b6..1a24c0bc 100644 --- a/apps/web/messages/ms.json +++ b/apps/web/messages/ms.json @@ -1467,7 +1467,8 @@ "requestFailed": "Permintaan gagal: {message}", "curlPasted": "cURL ditampal dan dihurai dengan berjaya", "responseCopied": "Respons disalin ke papan klip", - "codeCopied": "Kod disalin ke papan klip" + "codeCopied": "Kod disalin ke papan klip", + "copyFailed": "Gagal menyalin ke papan klip" }, "layout": { "collections": "Koleksi", @@ -1480,7 +1481,8 @@ "methodPlaceholder": "Kaedah", "urlPlaceholder": "https://api.contoh.com/v1/...", "sending": "Menghantar...", - "send": "Hantar" + "send": "Hantar", + "invalidJsonBodyHelp": "Tidak boleh hantar: kandungan JSON tidak sah" }, "requestTabs": { "params": "Parameter", @@ -1528,7 +1530,9 @@ "previewTab": "Pratonton", "headersTab": "Pengepala", "binaryRawView": "Data binari tidak boleh dipaparkan dalam paparan mentah.", - "errorStatusLabel": "Ralat" + "errorStatusLabel": "Ralat", + "truncatedBanner": "Showing first {shownKb} KB of a large response.", + "downloadFullBody": "Download full body" }, "tabBar": { "addTabAria": "Tab baharu" @@ -1613,6 +1617,21 @@ "labelFolder": "Folder", "placeholderFolder": "Pilih folder", "save": "Simpan" + }, + "shortcuts": { + "ariaLabel": "Keyboard shortcuts", + "title": "Keyboard shortcuts", + "newTab": "New tab", + "closeTab": "Close tab", + "sendRequest": "Send request" + }, + "toolbar": { + "moreActions": "More actions", + "code": "Generate code", + "save": "Save request", + "importCurl": "Import cURL", + "environments": "Environments", + "shortcuts": "Keyboard shortcuts" } }, "RichEditor": { diff --git a/apps/web/messages/nb.json b/apps/web/messages/nb.json index 5e324a41..da109173 100644 --- a/apps/web/messages/nb.json +++ b/apps/web/messages/nb.json @@ -1467,7 +1467,8 @@ "requestFailed": "Forespørsel mislyktes: {message}", "curlPasted": "cURL limt inn og analysert", "responseCopied": "Svar kopiert til utklippstavle", - "codeCopied": "Kode kopiert til utklippstavle" + "codeCopied": "Kode kopiert til utklippstavle", + "copyFailed": "Kopiering til utklippstavlen mislyktes" }, "layout": { "collections": "Samlinger", @@ -1480,7 +1481,8 @@ "methodPlaceholder": "Metode", "urlPlaceholder": "https://api.eksempel.no/v1/...", "sending": "Sender...", - "send": "Send" + "send": "Send", + "invalidJsonBodyHelp": "Kan ikke sende: JSON-kroppen er ugyldig" }, "requestTabs": { "params": "Parametere", @@ -1528,7 +1530,9 @@ "previewTab": "Forhåndsvisning", "headersTab": "Overskrifter", "binaryRawView": "Binære data kan ikke vises i råvisning.", - "errorStatusLabel": "Feil" + "errorStatusLabel": "Feil", + "truncatedBanner": "Showing first {shownKb} KB of a large response.", + "downloadFullBody": "Download full body" }, "tabBar": { "addTabAria": "Ny fane" @@ -1613,6 +1617,21 @@ "labelFolder": "Mappe", "placeholderFolder": "Velg en mappe", "save": "Lagre" + }, + "shortcuts": { + "ariaLabel": "Keyboard shortcuts", + "title": "Keyboard shortcuts", + "newTab": "New tab", + "closeTab": "Close tab", + "sendRequest": "Send request" + }, + "toolbar": { + "moreActions": "More actions", + "code": "Generate code", + "save": "Save request", + "importCurl": "Import cURL", + "environments": "Environments", + "shortcuts": "Keyboard shortcuts" } }, "RichEditor": { diff --git a/apps/web/messages/nl.json b/apps/web/messages/nl.json index 7d812b18..d6a8cddd 100644 --- a/apps/web/messages/nl.json +++ b/apps/web/messages/nl.json @@ -1467,7 +1467,8 @@ "requestFailed": "Verzoek mislukt: {message}", "curlPasted": "cURL geplakt en succesvol verwerkt", "responseCopied": "Antwoord gekopieerd naar klembord", - "codeCopied": "Code gekopieerd naar klembord" + "codeCopied": "Code gekopieerd naar klembord", + "copyFailed": "Kopiëren naar klembord mislukt" }, "layout": { "collections": "Collecties", @@ -1480,7 +1481,8 @@ "methodPlaceholder": "Methode", "urlPlaceholder": "https://api.voorbeeld.nl/v1/...", "sending": "Verzenden...", - "send": "Verzenden" + "send": "Verzenden", + "invalidJsonBodyHelp": "Kan niet verzenden: de JSON-body is ongeldig" }, "requestTabs": { "params": "Parameters", @@ -1528,7 +1530,9 @@ "previewTab": "Voorbeeld", "headersTab": "Headers", "binaryRawView": "Binaire gegevens kunnen niet worden weergegeven in ruwe weergave.", - "errorStatusLabel": "Fout" + "errorStatusLabel": "Fout", + "truncatedBanner": "Showing first {shownKb} KB of a large response.", + "downloadFullBody": "Download full body" }, "tabBar": { "addTabAria": "Nieuw tabblad" @@ -1613,6 +1617,21 @@ "labelFolder": "Map", "placeholderFolder": "Selecteer een map", "save": "Opslaan" + }, + "shortcuts": { + "ariaLabel": "Keyboard shortcuts", + "title": "Keyboard shortcuts", + "newTab": "New tab", + "closeTab": "Close tab", + "sendRequest": "Send request" + }, + "toolbar": { + "moreActions": "More actions", + "code": "Generate code", + "save": "Save request", + "importCurl": "Import cURL", + "environments": "Environments", + "shortcuts": "Keyboard shortcuts" } }, "RichEditor": { diff --git a/apps/web/messages/pl.json b/apps/web/messages/pl.json index 5022e838..bf385289 100644 --- a/apps/web/messages/pl.json +++ b/apps/web/messages/pl.json @@ -1467,7 +1467,8 @@ "requestFailed": "Request failed: {message}", "curlPasted": "cURL pasted and parsed successfully", "responseCopied": "Response copied to clipboard", - "codeCopied": "Code copied to clipboard" + "codeCopied": "Code copied to clipboard", + "copyFailed": "Kopiowanie do schowka nie powiodło się" }, "layout": { "collections": "Collections", @@ -1480,7 +1481,8 @@ "methodPlaceholder": "Method", "urlPlaceholder": "https://api.example.com/v1/...", "sending": "Sending...", - "send": "Send" + "send": "Send", + "invalidJsonBodyHelp": "Nie można wysłać: treść JSON jest nieprawidłowa" }, "requestTabs": { "params": "Params", @@ -1528,7 +1530,9 @@ "previewTab": "Preview", "headersTab": "Headers", "binaryRawView": "Binary data cannot be displayed in raw view.", - "errorStatusLabel": "Error" + "errorStatusLabel": "Error", + "truncatedBanner": "Showing first {shownKb} KB of a large response.", + "downloadFullBody": "Download full body" }, "tabBar": { "addTabAria": "New tab" @@ -1613,6 +1617,21 @@ "labelFolder": "Folder", "placeholderFolder": "Select a folder", "save": "Save" + }, + "shortcuts": { + "ariaLabel": "Keyboard shortcuts", + "title": "Keyboard shortcuts", + "newTab": "New tab", + "closeTab": "Close tab", + "sendRequest": "Send request" + }, + "toolbar": { + "moreActions": "More actions", + "code": "Generate code", + "save": "Save request", + "importCurl": "Import cURL", + "environments": "Environments", + "shortcuts": "Keyboard shortcuts" } }, "RichEditor": { diff --git a/apps/web/messages/pt-BR.json b/apps/web/messages/pt-BR.json index df10541a..0c4a441c 100644 --- a/apps/web/messages/pt-BR.json +++ b/apps/web/messages/pt-BR.json @@ -1467,7 +1467,8 @@ "requestFailed": "Requisição falhou: {message}", "curlPasted": "cURL colado e analisado com sucesso", "responseCopied": "Resposta copiada para a área de transferência", - "codeCopied": "Código copiado para a área de transferência" + "codeCopied": "Código copiado para a área de transferência", + "copyFailed": "Falha ao copiar para a área de transferência" }, "layout": { "collections": "Coleções", @@ -1480,7 +1481,8 @@ "methodPlaceholder": "Método", "urlPlaceholder": "https://api.exemplo.com/v1/...", "sending": "Enviando...", - "send": "Enviar" + "send": "Enviar", + "invalidJsonBodyHelp": "Não é possível enviar: o corpo JSON é inválido" }, "requestTabs": { "params": "Parâmetros", @@ -1528,7 +1530,9 @@ "previewTab": "Pré-visualização", "headersTab": "Cabeçalhos", "binaryRawView": "Dados binários não podem ser exibidos na visualização bruta.", - "errorStatusLabel": "Erro" + "errorStatusLabel": "Erro", + "truncatedBanner": "Showing first {shownKb} KB of a large response.", + "downloadFullBody": "Download full body" }, "tabBar": { "addTabAria": "Nova aba" @@ -1613,6 +1617,21 @@ "labelFolder": "Pasta", "placeholderFolder": "Selecione uma pasta", "save": "Salvar" + }, + "shortcuts": { + "ariaLabel": "Keyboard shortcuts", + "title": "Keyboard shortcuts", + "newTab": "New tab", + "closeTab": "Close tab", + "sendRequest": "Send request" + }, + "toolbar": { + "moreActions": "More actions", + "code": "Generate code", + "save": "Save request", + "importCurl": "Import cURL", + "environments": "Environments", + "shortcuts": "Keyboard shortcuts" } }, "RichEditor": { diff --git a/apps/web/messages/pt.json b/apps/web/messages/pt.json index 11d09140..fa6fdad4 100644 --- a/apps/web/messages/pt.json +++ b/apps/web/messages/pt.json @@ -1467,7 +1467,8 @@ "requestFailed": "Pedido falhou: {message}", "curlPasted": "cURL colado e analisado com sucesso", "responseCopied": "Resposta copiada para a área de transferência", - "codeCopied": "Código copiado para a área de transferência" + "codeCopied": "Código copiado para a área de transferência", + "copyFailed": "Falha ao copiar para a área de transferência" }, "layout": { "collections": "Coleções", @@ -1480,7 +1481,8 @@ "methodPlaceholder": "Método", "urlPlaceholder": "https://api.exemplo.com/v1/...", "sending": "A enviar...", - "send": "Enviar" + "send": "Enviar", + "invalidJsonBodyHelp": "Não é possível enviar: o corpo JSON é inválido" }, "requestTabs": { "params": "Parâmetros", @@ -1528,7 +1530,9 @@ "previewTab": "Pré-visualização", "headersTab": "Cabeçalhos", "binaryRawView": "Dados binários não podem ser apresentados na vista em bruto.", - "errorStatusLabel": "Erro" + "errorStatusLabel": "Erro", + "truncatedBanner": "Showing first {shownKb} KB of a large response.", + "downloadFullBody": "Download full body" }, "tabBar": { "addTabAria": "Novo separador" @@ -1613,6 +1617,21 @@ "labelFolder": "Pasta", "placeholderFolder": "Seleciona uma pasta", "save": "Guardar" + }, + "shortcuts": { + "ariaLabel": "Keyboard shortcuts", + "title": "Keyboard shortcuts", + "newTab": "New tab", + "closeTab": "Close tab", + "sendRequest": "Send request" + }, + "toolbar": { + "moreActions": "More actions", + "code": "Generate code", + "save": "Save request", + "importCurl": "Import cURL", + "environments": "Environments", + "shortcuts": "Keyboard shortcuts" } }, "RichEditor": { diff --git a/apps/web/messages/ru.json b/apps/web/messages/ru.json index 28439ca6..eceb9ac5 100644 --- a/apps/web/messages/ru.json +++ b/apps/web/messages/ru.json @@ -1467,7 +1467,8 @@ "requestFailed": "Запрос не выполнен: {message}", "curlPasted": "cURL вставлен и успешно разобран", "responseCopied": "Ответ скопирован в буфер обмена", - "codeCopied": "Код скопирован в буфер обмена" + "codeCopied": "Код скопирован в буфер обмена", + "copyFailed": "Не удалось скопировать в буфер обмена" }, "layout": { "collections": "Коллекции", @@ -1480,7 +1481,8 @@ "methodPlaceholder": "Метод", "urlPlaceholder": "https://api.example.com/v1/...", "sending": "Отправка...", - "send": "Отправить" + "send": "Отправить", + "invalidJsonBodyHelp": "Невозможно отправить: тело JSON недействительно" }, "requestTabs": { "params": "Параметры", @@ -1528,7 +1530,9 @@ "previewTab": "Предпросмотр", "headersTab": "Заголовки", "binaryRawView": "Бинарные данные не могут быть отображены в режиме исходного текста.", - "errorStatusLabel": "Ошибка" + "errorStatusLabel": "Ошибка", + "truncatedBanner": "Showing first {shownKb} KB of a large response.", + "downloadFullBody": "Download full body" }, "tabBar": { "addTabAria": "Новая вкладка" @@ -1613,6 +1617,21 @@ "labelFolder": "Папка", "placeholderFolder": "Выберите папку", "save": "Сохранить" + }, + "shortcuts": { + "ariaLabel": "Keyboard shortcuts", + "title": "Keyboard shortcuts", + "newTab": "New tab", + "closeTab": "Close tab", + "sendRequest": "Send request" + }, + "toolbar": { + "moreActions": "More actions", + "code": "Generate code", + "save": "Save request", + "importCurl": "Import cURL", + "environments": "Environments", + "shortcuts": "Keyboard shortcuts" } }, "RichEditor": { diff --git a/apps/web/messages/sv.json b/apps/web/messages/sv.json index b021a7de..4f7a3087 100644 --- a/apps/web/messages/sv.json +++ b/apps/web/messages/sv.json @@ -1467,7 +1467,8 @@ "requestFailed": "Förfrågan misslyckades: {message}", "curlPasted": "cURL inklistrad och tolkad", "responseCopied": "Svar kopierat till urklipp", - "codeCopied": "Kod kopierad till urklipp" + "codeCopied": "Kod kopierad till urklipp", + "copyFailed": "Kopiering till urklipp misslyckades" }, "layout": { "collections": "Samlingar", @@ -1480,7 +1481,8 @@ "methodPlaceholder": "Metod", "urlPlaceholder": "https://api.example.com/v1/...", "sending": "Skickar...", - "send": "Skicka" + "send": "Skicka", + "invalidJsonBodyHelp": "Kan inte skicka: JSON-kroppen är ogiltig" }, "requestTabs": { "params": "Parametrar", @@ -1528,7 +1530,9 @@ "previewTab": "Förhandsgranskning", "headersTab": "Rubriker", "binaryRawView": "Binärdata kan inte visas i råvy.", - "errorStatusLabel": "Fel" + "errorStatusLabel": "Fel", + "truncatedBanner": "Showing first {shownKb} KB of a large response.", + "downloadFullBody": "Download full body" }, "tabBar": { "addTabAria": "Ny flik" @@ -1613,6 +1617,21 @@ "labelFolder": "Mapp", "placeholderFolder": "Välj en mapp", "save": "Spara" + }, + "shortcuts": { + "ariaLabel": "Keyboard shortcuts", + "title": "Keyboard shortcuts", + "newTab": "New tab", + "closeTab": "Close tab", + "sendRequest": "Send request" + }, + "toolbar": { + "moreActions": "More actions", + "code": "Generate code", + "save": "Save request", + "importCurl": "Import cURL", + "environments": "Environments", + "shortcuts": "Keyboard shortcuts" } }, "RichEditor": { diff --git a/apps/web/messages/tr.json b/apps/web/messages/tr.json index a7f0ea6e..8eda2c1f 100644 --- a/apps/web/messages/tr.json +++ b/apps/web/messages/tr.json @@ -1467,7 +1467,8 @@ "requestFailed": "İstek başarısız: {message}", "curlPasted": "cURL yapıştırıldı ve başarıyla ayrıştırıldı", "responseCopied": "Yanıt panoya kopyalandı", - "codeCopied": "Kod panoya kopyalandı" + "codeCopied": "Kod panoya kopyalandı", + "copyFailed": "Panoya kopyalanamadı" }, "layout": { "collections": "Koleksiyonlar", @@ -1480,7 +1481,8 @@ "methodPlaceholder": "Yöntem", "urlPlaceholder": "https://api.ornek.com/v1/...", "sending": "Gönderiliyor...", - "send": "Gönder" + "send": "Gönder", + "invalidJsonBodyHelp": "Gönderilemedi: JSON gövdesi geçersiz" }, "requestTabs": { "params": "Parametreler", @@ -1528,7 +1530,9 @@ "previewTab": "Önizleme", "headersTab": "Başlıklar", "binaryRawView": "İkili veriler ham görünümde gösterilemiyor.", - "errorStatusLabel": "Hata" + "errorStatusLabel": "Hata", + "truncatedBanner": "Showing first {shownKb} KB of a large response.", + "downloadFullBody": "Download full body" }, "tabBar": { "addTabAria": "Yeni sekme" @@ -1613,6 +1617,21 @@ "labelFolder": "Klasör", "placeholderFolder": "Bir klasör seçin", "save": "Kaydet" + }, + "shortcuts": { + "ariaLabel": "Keyboard shortcuts", + "title": "Keyboard shortcuts", + "newTab": "New tab", + "closeTab": "Close tab", + "sendRequest": "Send request" + }, + "toolbar": { + "moreActions": "More actions", + "code": "Generate code", + "save": "Save request", + "importCurl": "Import cURL", + "environments": "Environments", + "shortcuts": "Keyboard shortcuts" } }, "RichEditor": { diff --git a/apps/web/messages/uk.json b/apps/web/messages/uk.json index 9617ba93..00a24344 100644 --- a/apps/web/messages/uk.json +++ b/apps/web/messages/uk.json @@ -1467,7 +1467,8 @@ "requestFailed": "Запит не вдався: {message}", "curlPasted": "cURL вставлено та розібрано успішно", "responseCopied": "Відповідь скопійовано до буфера обміну", - "codeCopied": "Код скопійовано до буфера обміну" + "codeCopied": "Код скопійовано до буфера обміну", + "copyFailed": "Не вдалося скопіювати до буфера обміну" }, "layout": { "collections": "Колекції", @@ -1480,7 +1481,8 @@ "methodPlaceholder": "Метод", "urlPlaceholder": "https://api.example.com/v1/...", "sending": "Надсилання...", - "send": "Надіслати" + "send": "Надіслати", + "invalidJsonBodyHelp": "Неможливо надіслати: тіло JSON недійсне" }, "requestTabs": { "params": "Параметри", @@ -1528,7 +1530,9 @@ "previewTab": "Перегляд", "headersTab": "Заголовки", "binaryRawView": "Двійкові дані не можна відобразити у вигляді тексту.", - "errorStatusLabel": "Помилка" + "errorStatusLabel": "Помилка", + "truncatedBanner": "Showing first {shownKb} KB of a large response.", + "downloadFullBody": "Download full body" }, "tabBar": { "addTabAria": "Нова вкладка" @@ -1613,6 +1617,21 @@ "labelFolder": "Тека", "placeholderFolder": "Обрати теку", "save": "Зберегти" + }, + "shortcuts": { + "ariaLabel": "Keyboard shortcuts", + "title": "Keyboard shortcuts", + "newTab": "New tab", + "closeTab": "Close tab", + "sendRequest": "Send request" + }, + "toolbar": { + "moreActions": "More actions", + "code": "Generate code", + "save": "Save request", + "importCurl": "Import cURL", + "environments": "Environments", + "shortcuts": "Keyboard shortcuts" } }, "RichEditor": { diff --git a/apps/web/messages/vi.json b/apps/web/messages/vi.json index f70c457b..6620d695 100644 --- a/apps/web/messages/vi.json +++ b/apps/web/messages/vi.json @@ -1467,7 +1467,8 @@ "requestFailed": "Yêu cầu thất bại: {message}", "curlPasted": "Đã dán và phân tích cURL thành công", "responseCopied": "Đã sao chép phản hồi vào bộ nhớ tạm", - "codeCopied": "Đã sao chép mã vào bộ nhớ tạm" + "codeCopied": "Đã sao chép mã vào bộ nhớ tạm", + "copyFailed": "Sao chép vào clipboard không thành công" }, "layout": { "collections": "Bộ sưu tập", @@ -1480,7 +1481,8 @@ "methodPlaceholder": "Phương thức", "urlPlaceholder": "https://api.example.com/v1/...", "sending": "Đang gửi...", - "send": "Gửi" + "send": "Gửi", + "invalidJsonBodyHelp": "Không thể gửi: nội dung JSON không hợp lệ" }, "requestTabs": { "params": "Tham số", @@ -1528,7 +1530,9 @@ "previewTab": "Xem trước", "headersTab": "Headers", "binaryRawView": "Dữ liệu nhị phân không thể hiển thị ở chế độ raw.", - "errorStatusLabel": "Lỗi" + "errorStatusLabel": "Lỗi", + "truncatedBanner": "Showing first {shownKb} KB of a large response.", + "downloadFullBody": "Download full body" }, "tabBar": { "addTabAria": "Tab mới" @@ -1613,6 +1617,21 @@ "labelFolder": "Thư mục", "placeholderFolder": "Chọn thư mục", "save": "Lưu" + }, + "shortcuts": { + "ariaLabel": "Keyboard shortcuts", + "title": "Keyboard shortcuts", + "newTab": "New tab", + "closeTab": "Close tab", + "sendRequest": "Send request" + }, + "toolbar": { + "moreActions": "More actions", + "code": "Generate code", + "save": "Save request", + "importCurl": "Import cURL", + "environments": "Environments", + "shortcuts": "Keyboard shortcuts" } }, "RichEditor": { diff --git a/apps/web/messages/zh.json b/apps/web/messages/zh.json index 44f5b97b..be30441d 100644 --- a/apps/web/messages/zh.json +++ b/apps/web/messages/zh.json @@ -1467,7 +1467,8 @@ "requestFailed": "请求失败:{message}", "curlPasted": "cURL 已粘贴并解析成功", "responseCopied": "响应已复制到剪贴板", - "codeCopied": "代码已复制到剪贴板" + "codeCopied": "代码已复制到剪贴板", + "copyFailed": "复制到剪贴板失败" }, "layout": { "collections": "集合", @@ -1480,7 +1481,8 @@ "methodPlaceholder": "方法", "urlPlaceholder": "https://api.example.com/v1/...", "sending": "发送中…", - "send": "发送" + "send": "发送", + "invalidJsonBodyHelp": "无法发送:JSON 正文无效" }, "requestTabs": { "params": "参数", @@ -1528,7 +1530,9 @@ "previewTab": "预览", "headersTab": "响应头", "binaryRawView": "二进制数据无法在原始视图中显示。", - "errorStatusLabel": "错误" + "errorStatusLabel": "错误", + "truncatedBanner": "Showing first {shownKb} KB of a large response.", + "downloadFullBody": "Download full body" }, "tabBar": { "addTabAria": "新标签" @@ -1613,6 +1617,21 @@ "labelFolder": "文件夹", "placeholderFolder": "选择文件夹", "save": "保存" + }, + "shortcuts": { + "ariaLabel": "Keyboard shortcuts", + "title": "Keyboard shortcuts", + "newTab": "New tab", + "closeTab": "Close tab", + "sendRequest": "Send request" + }, + "toolbar": { + "moreActions": "More actions", + "code": "Generate code", + "save": "Save request", + "importCurl": "Import cURL", + "environments": "Environments", + "shortcuts": "Keyboard shortcuts" } }, "RichEditor": { diff --git a/apps/web/package.json b/apps/web/package.json index f3ebfd0f..bc276195 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -16,6 +16,7 @@ "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", + "@hookform/resolvers": "^3", "@monaco-editor/react": "^4.7.0", "@pdfsmaller/pdf-decrypt": "^1.0.1", "@pdfsmaller/pdf-encrypt": "^1.0.2", @@ -45,6 +46,7 @@ "@radix-ui/react-tooltip": "^1.2.8", "@space-man/react-theme-animation": "^1.1.1", "@tabler/icons-react": "^3.35.0", + "@tanstack/react-query": "^5", "@tanstack/react-virtual": "^3.14.3", "@vercel/analytics": "^1.5.0", "@vercel/speed-insights": "^1.2.0", @@ -100,7 +102,9 @@ "react-dom": "^19.2.0", "react-firebase-hooks": "^5.1.1", "react-github-calendar": "^5.0.6", + "react-hook-form": "^7", "react-resizable-panels": "^3.0.6", + "react-window": "^2.2.7", "reflect-metadata": "^0.2.2", "smol-toml": "^1.6.1", "sonner": "^2.0.7", @@ -116,6 +120,7 @@ "vanilla-jsoneditor": "^3.12.0", "vaul": "^1.1.2", "xlsx": "^0.18.5", + "zod": "^3", "zustand": "^5.0.8" }, "devDependencies": { @@ -133,6 +138,7 @@ "@types/qrcode": "^1.5.6", "@types/react": "^19", "@types/react-dom": "^19", + "@types/react-window": "^2.0.0", "@types/turndown": "^5.0.6", "@vitalets/google-translate-api": "^9.2.1", "autoprefixer": "^10.4.21", diff --git a/apps/web/src/app/app/api-client/page.tsx b/apps/web/src/app/app/api-client/page.tsx index 6e4d5406..ada5fdd8 100644 --- a/apps/web/src/app/app/api-client/page.tsx +++ b/apps/web/src/app/app/api-client/page.tsx @@ -1,8 +1,12 @@ import { ApiClientLazy } from '@/components/app-tools/client-only-tool-loaders' +import { ApiClientProviders } from '@/components/api-client/context/api-client-context' + export default function ApiClientPage() { return ( -
- -
+ +
+ +
+
) } diff --git a/apps/web/src/app/app/to-do/KanbanCard.tsx b/apps/web/src/app/app/to-do/KanbanCard.tsx index 3ae4d8cf..ad9baa0a 100644 --- a/apps/web/src/app/app/to-do/KanbanCard.tsx +++ b/apps/web/src/app/app/to-do/KanbanCard.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, lazy, Suspense } from "react"; import { useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { GripVertical, Edit, Calendar, Tag, CheckCircle2, MoreHorizontal, Trash2, Copy, Check, Play, Pause, Timer, Archive, ArchiveRestore } from "lucide-react"; @@ -8,8 +8,11 @@ import { formatElapsed, getElapsedMinutes } from "@/app/app/to-do/utils/taskTime import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Task } from "@/app/app/to-do/types/Task"; -import TaskEditDialog from "./TaskEditDialog"; -import { differenceInDays, isPast, parseISO, format, isValid, parse } from "date-fns"; + +const TaskEditDialog = lazy(() => import("./TaskEditDialog")); +import { LazyBoundary } from "./components/LazyBoundary"; +import { differenceInDays, isPast } from "date-fns"; +import { safeParseDate } from "@/app/app/to-do/utils/taskDate"; import { cn } from "@/lib/utils"; import { Tooltip, @@ -27,109 +30,6 @@ import { import { STATUS_CONFIG, PRIORITY_CONFIG } from "./config/constants"; import { useProjectContext } from "@/app/app/to-do/context/ProjectContext"; -// Helper to safely parse and format dates -const safeFormatDate = (dateString: string | undefined, formatStr: string): string => { - if (!dateString || dateString === "Unknown") return dateString || "Unknown"; - - // Try parsing as ISO first (for new data) - try { - const isoDate = parseISO(dateString); - if (isValid(isoDate)) { - return format(isoDate, formatStr); - } - } catch { - // Not an ISO string, continue - } - - // Try parsing the formatted date string from TaskContext: "dd MMM yyyy, hh:mm a" - try { - const parsedDate = parse(dateString, "dd MMM yyyy, hh:mm a", new Date()); - if (isValid(parsedDate)) { - return format(parsedDate, formatStr); - } - } catch { - // Not in that format, continue - } - - // Try parsing just the date part "dd MMM yyyy" - try { - const parts = dateString.split(','); - if (parts.length > 0) { - const datePart = parts[0].trim(); // "dd MMM yyyy" - const parsedDate = parse(datePart, "dd MMM yyyy", new Date()); - if (isValid(parsedDate)) { - return format(parsedDate, formatStr); - } - } - } catch { - // Fall through - } - - // Try parsing as Date object (fallback) - try { - const date = new Date(dateString); - if (isValid(date)) { - return format(date, formatStr); - } - } catch { - // Not a valid date string - } - - // If all parsing fails, return a shortened version of the string - return dateString.length > 15 ? dateString.substring(0, 15) + "..." : dateString; -}; - -// Helper to safely parse date for calculations -const safeParseDate = (dateString: string | undefined): Date | null => { - if (!dateString) return null; - - // Try parsing as ISO first - try { - const isoDate = parseISO(dateString); - if (isValid(isoDate)) { - return isoDate; - } - } catch { - // Not an ISO string - } - - // Try parsing the formatted date string from TaskContext: "dd MMM yyyy, hh:mm a" - try { - const parsedDate = parse(dateString, "dd MMM yyyy, hh:mm a", new Date()); - if (isValid(parsedDate)) { - return parsedDate; - } - } catch { - // Not in that format - } - - // Try parsing just the date part "dd MMM yyyy" - try { - const parts = dateString.split(','); - if (parts.length > 0) { - const datePart = parts[0].trim(); - const parsedDate = parse(datePart, "dd MMM yyyy", new Date()); - if (isValid(parsedDate)) { - return parsedDate; - } - } - } catch { - // Fall through - } - - // Try parsing as Date object (fallback) - try { - const date = new Date(dateString); - if (isValid(date)) { - return date; - } - } catch { - // Not a valid date - } - - return null; -}; - interface KanbanCardProps { task: Task; onUpdateTask: (id: string, updates: Partial) => Promise; @@ -201,11 +101,11 @@ export default function KanbanCard({ task, onUpdateTask, onDeleteTask }: KanbanC {...attributes} {...listeners} className={cn( - "group relative p-3 md:p-4 rounded-xl border transition-all duration-300", - "hover:shadow-md hover:scale-[1.02] bg-card", + "group relative p-3 md:p-4 rounded-xl border transition-all duration-200", + "hover:shadow-md hover:-translate-y-0.5 bg-card", "cursor-grab active:cursor-grabbing border-border", "hover:border-primary/30", - isDragging && "shadow-2xl scale-110 z-50 rotate-2 opacity-90", + isDragging && "shadow-2xl scale-105 z-50 rotate-1 opacity-90", task.status === "completed" && "opacity-75 bg-muted/30" )} role="button" @@ -503,12 +403,18 @@ export default function KanbanCard({ task, onUpdateTask, onDeleteTask }: KanbanC - + {isEditDialogOpen && ( + + + + + + )} ); } diff --git a/apps/web/src/app/app/to-do/KanbanColumn.tsx b/apps/web/src/app/app/to-do/KanbanColumn.tsx index 7aa44134..a784fa68 100644 --- a/apps/web/src/app/app/to-do/KanbanColumn.tsx +++ b/apps/web/src/app/app/to-do/KanbanColumn.tsx @@ -1,7 +1,9 @@ "use client"; +import { useRef } from "react"; import { useDroppable } from "@dnd-kit/core"; import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable"; +import { useVirtualizer } from "@tanstack/react-virtual"; import { Task } from "@/app/app/to-do/types/Task"; import KanbanCard from "./KanbanCard"; import { LucideIcon } from "lucide-react"; @@ -18,6 +20,9 @@ interface KanbanColumnProps { onDeleteTask: (id: string) => void; } +const VIRTUALIZATION_THRESHOLD = 30; +const ESTIMATED_CARD_HEIGHT = 160; + export default function KanbanColumn({ id, title, @@ -28,12 +33,19 @@ export default function KanbanColumn({ }: KanbanColumnProps) { const tKanban = useTranslations("Tasks.kanban"); const tStatus = useTranslations("Tasks.status"); - const { setNodeRef, isOver } = useDroppable({ - id, - }); + const { setNodeRef, isOver } = useDroppable({ id }); const config = STATUS_CONFIG[id]; const taskIds = tasks.map((task) => task.id); + const shouldVirtualize = tasks.length > VIRTUALIZATION_THRESHOLD; + + const scrollRef = useRef(null); + const virtualizer = useVirtualizer({ + count: shouldVirtualize ? tasks.length : 0, + getScrollElement: () => scrollRef.current, + estimateSize: () => ESTIMATED_CARD_HEIGHT, + overscan: 6, + }); return (
- {/* Enhanced Column Header */} + {/* Column Header */}
-
- -
-

+ +

{title}

- {/* Tasks - Enhanced */} + {/* Tasks */} -
+
{tasks.length === 0 ? (
)}
+ ) : shouldVirtualize ? ( +
+ {virtualizer.getVirtualItems().map((virtualRow) => { + const task = tasks[virtualRow.index]; + return ( +
+ +
+ ); + })} +
) : ( tasks.map((task, index) => (
import("@/app/app/to-do/KanbanBoard")); +const ExportImportDialog = lazy(() => import("@/app/app/to-do/ExportImportDialog")); +const TaskCommandPalette = lazy(() => + import("@/app/app/to-do/components/TaskCommandPalette").then((m) => ({ + default: m.TaskCommandPalette, + })) +); import { useTaskContext } from "@/app/app/to-do/context/TaskContext"; import { useProjectContext } from "@/app/app/to-do/context/ProjectContext"; import { ListTodo, Circle, LayoutGrid, List, Search, X, Plus, Folder, Archive, ArchiveRestore } from "lucide-react"; @@ -55,6 +62,7 @@ export const TaskContainer = () => { const [searchQuery, setSearchQuery] = useState(""); const [isDrawerOpen, setIsDrawerOpen] = useState(false); + const [isPaletteOpen, setIsPaletteOpen] = useState(false); const searchInputRef = useRef(null); const taskFormInputRef = useRef(null); const { @@ -98,42 +106,54 @@ export const TaskContainer = () => { // Filter tasks based on filterStatus for list view // For kanban view, always show all tasks (filtering is handled by columns) - const filteredTasks = viewMode === "kanban" || filterStatus === "all" - ? searchFilteredTasks - : searchFilteredTasks.filter(task => task.status === filterStatus); - - const sortedTasks = [...filteredTasks].sort( - (a: { status: "ongoing" | "not-started" | "completed" }, - b: { status: "ongoing" | "not-started" | "completed" }) => { - const statusOrder: { [key in "ongoing" | "not-started" | "completed"]: number } = { - ongoing: 1, - "not-started": 2, - completed: 3, - }; - return statusOrder[a.status] - statusOrder[b.status]; - } + const filteredTasks = useMemo( + () => + viewMode === "kanban" || filterStatus === "all" + ? searchFilteredTasks + : searchFilteredTasks.filter((task) => task.status === filterStatus), + [searchFilteredTasks, viewMode, filterStatus] ); + const sortedTasks = useMemo(() => { + const statusOrder: { ongoing: number; "not-started": number; completed: number } = { + ongoing: 1, + "not-started": 2, + completed: 3, + }; + return [...filteredTasks].sort((a, b) => statusOrder[a.status] - statusOrder[b.status]); + }, [filteredTasks]); + // Calculate statistics using all tasks stats const completionRate = allTaskStats.total > 0 ? Math.round((allTaskStats.completed / allTaskStats.total) * 100) : 0; - const handleAddTask = (taskText: string) => { - addTask(taskText); - setIsDrawerOpen(false); - }; + const handleAddTask = useCallback( + (taskText: string) => { + addTask(taskText); + setIsDrawerOpen(false); + }, + [addTask] + ); const [isExportDialogOpen, setIsExportDialogOpen] = useState(false); const hasStatusFilter = filterStatus !== "all"; const hasProjectFilter = filterProject !== "all"; const hasSearchFilter = searchQuery.trim().length > 0; const hasActiveFilters = hasStatusFilter || hasProjectFilter || hasSearchFilter; - const activeProject = projects.find((project) => project.id === filterProject); + const activeProject = useMemo( + () => projects.find((project) => project.id === filterProject), + [projects, filterProject] + ); - const clearAllFilters = () => { + const liveStatsMessage = useMemo(() => { + const { total, completed, ongoing, notStarted } = allTaskStats; + return `${total} task${total === 1 ? "" : "s"}: ${notStarted} not started, ${ongoing} ongoing, ${completed} completed`; + }, [allTaskStats]); + + const clearAllFilters = useCallback(() => { setSearchQuery(""); setFilterStatus("all"); setFilterProject("all"); - }; + }, [setFilterStatus, setFilterProject]); useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { @@ -145,7 +165,7 @@ export const TaskContainer = () => { if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") { event.preventDefault(); - searchInputRef.current?.focus(); + setIsPaletteOpen((open) => !open); return; } @@ -171,10 +191,16 @@ export const TaskContainer = () => { }, [hasSearchFilter, isMobile]); return ( -
+
+
+ {liveStatsMessage} +
{/* Mobile-specific Header */} {isMobile && ( -
+
@@ -202,8 +228,8 @@ export const TaskContainer = () => { {/* Title & Stats */}
-

{tPage("myTasksTitle")}

-

+

{tPage("myTasksTitle")}

+

{tPage("statsLine", { total: allTaskStats.total, percent: completionRate })}

@@ -288,10 +314,10 @@ export const TaskContainer = () => {
-

+

{tPage("myTasksTitle")}

-

+

{tPage("statsLineDesktop", { total: allTaskStats.total, percent: completionRate })}

@@ -302,9 +328,9 @@ export const TaskContainer = () => {
- {allTaskStats.total} + {allTaskStats.total}
- {tFilters("total")} + {tFilters("total")}
{Object.values(STATUS_CONFIG).map((config) => { @@ -325,9 +351,9 @@ export const TaskContainer = () => { >
- {count} + {count}
- {tStatus(`${config.id}.label` as any)} + {tStatus(`${config.id}.label` as any)}
); })} @@ -555,13 +581,23 @@ export const TaskContainer = () => { {viewMode === "kanban" ? ( - + + +
+
+ } + > + +
+
) : ( @@ -640,12 +676,35 @@ export const TaskContainer = () => {
- + {isExportDialogOpen && ( + + + + + + )} + + {isPaletteOpen && ( + + + { + if (isMobile) setIsDrawerOpen(true); + else taskFormInputRef.current?.focus(); + }} + /> + + + )}
); }; diff --git a/apps/web/src/app/app/to-do/TaskEditDialog.tsx b/apps/web/src/app/app/to-do/TaskEditDialog.tsx index 23fceeb2..eca94b1c 100644 --- a/apps/web/src/app/app/to-do/TaskEditDialog.tsx +++ b/apps/web/src/app/app/to-do/TaskEditDialog.tsx @@ -1,6 +1,7 @@ "use client"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useCallback } from "react"; +import { z } from "zod"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Drawer, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerClose } from "@/components/ui/drawer"; import { Button } from "@/components/ui/button"; @@ -33,6 +34,42 @@ const priorityConfig = { low: { label: "Low", icon: Zap, color: "text-blue-500", bgColor: "bg-blue-50 dark:bg-blue-950", borderColor: "border-blue-500" }, }; +function hexToRgb(hex: string): [number, number, number] | null { + const m = hex.replace("#", "").match(/^([\da-f]{6}|[\da-f]{3})$/i); + if (!m) return null; + const h = m[1].length === 3 + ? m[1].split("").map((c) => c + c).join("") + : m[1]; + return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)]; +} + +function relLuminance([r, g, b]: [number, number, number]): number { + const srgb = [r, g, b].map((v) => { + const x = v / 255; + return x <= 0.03928 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4); + }); + return 0.2126 * srgb[0] + 0.7152 * srgb[1] + 0.0722 * srgb[2]; +} + +function contrastVsWhite(hex: string): number | null { + const rgb = hexToRgb(hex); + if (!rgb) return null; + const l = relLuminance(rgb); + return (1.0 + 0.05) / (l + 0.05); +} + +const taskEditSchema = z.object({ + text: z.string().trim().min(1, "Title is required").max(200, "Max 200 characters"), + description: z.string().max(2000, "Max 2000 characters").optional(), + timeEstimate: z + .number() + .int("Must be a whole number") + .positive("Must be positive") + .optional(), +}); + +type TaskEditErrors = Partial>; + const predefinedTags = [ { name: "Work", color: "#3b82f6" }, { name: "Personal", color: "#10b981" }, @@ -54,6 +91,7 @@ export default function TaskEditDialog({ task, open, onOpenChange, onSave }: Tas const [customTagColor, setCustomTagColor] = useState("#3b82f6"); const [isSaving, setIsSaving] = useState(false); const [selectedDate, setSelectedDate] = useState(undefined); + const [errors, setErrors] = useState({}); const isMobile = useIsMobile(); const { projects } = useProjectContext(); @@ -70,16 +108,44 @@ export default function TaskEditDialog({ task, open, onOpenChange, onSave }: Tas timeEstimate: task.timeEstimate, projectId: task.projectId, }); + setErrors({}); if (task.dueDate) { setSelectedDate(new Date(task.dueDate)); } } }, [open, task]); + const clearError = useCallback((field: keyof TaskEditErrors) => { + setErrors((prev) => { + if (!prev[field]) return prev; + const next = { ...prev }; + delete next[field]; + return next; + }); + }, []); + const handleSave = async () => { + const result = taskEditSchema.safeParse({ + text: editedTask.text ?? "", + description: editedTask.description, + timeEstimate: editedTask.timeEstimate, + }); + + if (!result.success) { + const fieldErrors: TaskEditErrors = {}; + for (const issue of result.error.issues) { + const path = issue.path[0] as keyof TaskEditErrors | undefined; + if (path && !fieldErrors[path]) { + fieldErrors[path] = issue.message; + } + } + setErrors(fieldErrors); + return; + } + setIsSaving(true); try { - await onSave(editedTask); + await onSave({ ...editedTask, text: result.data.text }); onOpenChange(false); } catch (error) { console.error("Failed to save task:", error); @@ -139,14 +205,25 @@ export default function TaskEditDialog({ task, open, onOpenChange, onSave }: Tas
{/* Task Title */}
- + setEditedTask({ ...editedTask, text: e.target.value })} + onChange={(e) => { + setEditedTask({ ...editedTask, text: e.target.value }); + clearError("text"); + }} placeholder={t("taskTitlePlaceholder")} - className="text-base" + className={cn("text-base", errors.text && "border-destructive focus-visible:ring-destructive/30")} + aria-invalid={!!errors.text} + aria-describedby={errors.text ? "task-title-error" : undefined} /> + {errors.text && ( +

{errors.text}

+ )}
{/* Description */} @@ -155,10 +232,18 @@ export default function TaskEditDialog({ task, open, onOpenChange, onSave }: Tas